|
1 | 1 | import { Token } from "../types"; |
2 | 2 | import { Erc20__factory } from "../types/contracts"; |
3 | 3 |
|
4 | | -export async function getToken (address: string) { |
| 4 | +export type TokenLookupContext = { |
| 5 | + handler?: string; |
| 6 | + blockHeight?: bigint | number | string; |
| 7 | + transactionHash?: string; |
| 8 | + logIndex?: bigint | number | string; |
| 9 | +}; |
| 10 | + |
| 11 | +type MetadataMethod = "name" | "symbol" | "decimals"; |
| 12 | + |
| 13 | +const FALLBACK_SYMBOL = "UNKNOWN"; |
| 14 | +const FALLBACK_DECIMALS = BigInt(0); |
| 15 | + |
| 16 | +export async function getToken (address: string, context: TokenLookupContext = {}) { |
5 | 17 | let token = await Token.get(address); |
6 | 18 |
|
7 | 19 | if (token) { |
8 | 20 | return token; |
9 | 21 | } |
10 | 22 |
|
11 | 23 | const erc20 = Erc20__factory.connect(address, api); |
12 | | - const name = await erc20.name(); |
13 | | - const symbol = await erc20.symbol(); |
14 | | - const decimals = await erc20.decimals(); |
| 24 | + const name = await readMetadata(address, "name", () => erc20.name(), context); |
| 25 | + const symbol = await readMetadata(address, "symbol", () => erc20.symbol(), context); |
| 26 | + const decimals = await readMetadata(address, "decimals", () => erc20.decimals(), context); |
15 | 27 |
|
16 | | - token = new Token(address, name, symbol, BigInt(decimals.toString())); |
| 28 | + token = new Token( |
| 29 | + address, |
| 30 | + name ?? fallbackName(address), |
| 31 | + symbol ?? FALLBACK_SYMBOL, |
| 32 | + decimals === undefined ? FALLBACK_DECIMALS : BigInt(decimals.toString()) |
| 33 | + ); |
17 | 34 |
|
18 | 35 | await token.save(); |
19 | 36 |
|
20 | 37 | return token; |
21 | 38 | } |
| 39 | + |
| 40 | +async function readMetadata<T> ( |
| 41 | + address: string, |
| 42 | + method: MetadataMethod, |
| 43 | + read: () => Promise<T>, |
| 44 | + context: TokenLookupContext |
| 45 | +): Promise<T | undefined> { |
| 46 | + try { |
| 47 | + return await read(); |
| 48 | + } catch (error) { |
| 49 | + if (!isContractMetadataFailure(error)) { |
| 50 | + throw error; |
| 51 | + } |
| 52 | + |
| 53 | + logger.warn(`Failed to read ERC20 metadata: address=${address} method=${method}${formatContext(context)} error=${formatError(error)}`); |
| 54 | + return undefined; |
| 55 | + } |
| 56 | +} |
| 57 | + |
| 58 | +function fallbackName (address: string): string { |
| 59 | + return `Unknown Token ${address}`; |
| 60 | +} |
| 61 | + |
| 62 | +function formatContext (context: TokenLookupContext): string { |
| 63 | + const parts: string[] = []; |
| 64 | + |
| 65 | + if (context.handler) parts.push(`handler=${context.handler}`); |
| 66 | + if (context.blockHeight !== undefined) parts.push(`block=${context.blockHeight.toString()}`); |
| 67 | + if (context.transactionHash) parts.push(`tx=${context.transactionHash}`); |
| 68 | + if (context.logIndex !== undefined) parts.push(`logIndex=${context.logIndex.toString()}`); |
| 69 | + |
| 70 | + return parts.length ? ` ${parts.join(" ")}` : ""; |
| 71 | +} |
| 72 | + |
| 73 | +function formatError (error: unknown): string { |
| 74 | + const code = errorCode(error); |
| 75 | + |
| 76 | + if (error instanceof Error) { |
| 77 | + return `${error.name}: ${error.message}${code ? ` code=${code}` : ""}`; |
| 78 | + } |
| 79 | + |
| 80 | + if (typeof error === "string") { |
| 81 | + return error; |
| 82 | + } |
| 83 | + |
| 84 | + return code ? `code=${code}` : "unknown"; |
| 85 | +} |
| 86 | + |
| 87 | +function errorCode (error: unknown): string | undefined { |
| 88 | + if (typeof error !== "object" || error === null || !("code" in error)) { |
| 89 | + return undefined; |
| 90 | + } |
| 91 | + |
| 92 | + const code = error.code; |
| 93 | + return typeof code === "string" ? code : undefined; |
| 94 | +} |
| 95 | + |
| 96 | +function isContractMetadataFailure (error: unknown): boolean { |
| 97 | + const code = errorCode(error); |
| 98 | + |
| 99 | + if (code === "CALL_EXCEPTION" || code === "BAD_DATA") { |
| 100 | + return true; |
| 101 | + } |
| 102 | + |
| 103 | + if (!(error instanceof Error)) { |
| 104 | + return false; |
| 105 | + } |
| 106 | + |
| 107 | + if (code !== undefined && code !== "INVALID_ARGUMENT") { |
| 108 | + return false; |
| 109 | + } |
| 110 | + |
| 111 | + return error.message.includes("data out-of-bounds") |
| 112 | + || error.message.includes("could not decode result data"); |
| 113 | +} |
0 commit comments