From a752fb297e532bd459233286e32214dc3c7c3431 Mon Sep 17 00:00:00 2001 From: Kyle Thornton Date: Fri, 22 Nov 2024 12:32:16 -0800 Subject: [PATCH 1/6] Adding AnyABI loader --- src/loaders.ts | 71 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/src/loaders.ts b/src/loaders.ts index 13649ab..f5e39e4 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -30,11 +30,11 @@ import * as errors from "./errors.js"; export type ContractResult = { abi: any[]; name: string | null; - evmVersion: string; - compilerVersion: string; - runs: number; - ok: boolean; // False if no result is found + evmVersion?: string; + compilerVersion?: string; + runs?: number; + /** * getSources returns the imports -> source code mapping for the contract, if available. @@ -425,7 +425,6 @@ export interface SourcifyContractMetadata { version: number; } - export class BlockscoutABILoader implements ABILoader { readonly name = "BlockscoutABILoader"; @@ -602,6 +601,68 @@ export type BlockscoutContractResult = { has_methods_write_proxy?: boolean; }; + +function isAnyABINotFound(error: any): boolean { + return ( + error.message === "Failed to fetch" || + error.message === "ABI not found" || + error.status === 404 + ); +} + +// https://anyabi.xyz/ +export class AnyABILoader implements ABILoader { + readonly name = "AnyABILoader"; + + chainId?: number; + + constructor(config?: { chainId?: number }) { + this.chainId = config?.chainId ?? 1; + } + + async #fetchAnyABI(address: string): Promise { + const url = "https://anyabi.xyz/api/get-abi/" + this.chainId + "/" + address; + try { + const r = await fetchJSON(url); + const { abi, name }: { abi: any[]; name: string } = r; + + return { + abi: abi, + name: name, + ok: true, + loader: this, + loaderResult: r, + }; + } catch (err: any) { + if (!isAnyABINotFound(err)) return emptyContractResult; + throw new AnyABILoaderError("AnyABILoader load contract error: " + err.message, { + context: { url }, + cause: err, + }); + } + } + + async getContract(address: string): Promise { + { + const r = await this.#fetchAnyABI(address); + if (r.ok) return r; + } + + return emptyContractResult; + } + + async loadABI(address: string): Promise { + { + const r = await this.#fetchAnyABI(address); + if (r.ok) return r.abi; + } + + return []; + } +} + +export class AnyABILoaderError extends errors.LoaderError { }; + export interface SignatureLookup { loadFunctions(selector: string): Promise; loadEvents(hash: string): Promise; From f0399cd5b75d902598667f5421c70de2d4f59a41 Mon Sep 17 00:00:00 2001 From: Kyle Thornton Date: Fri, 22 Nov 2024 14:04:58 -0800 Subject: [PATCH 2/6] Adding EtherscanV2 --- src/loaders.ts | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/loaders.ts b/src/loaders.ts index f5e39e4..c3d122a 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -157,7 +157,7 @@ export class MultiABILoaderError extends errors.LoaderError { }; export class EtherscanABILoader implements ABILoader { - readonly name = "EtherscanABILoader"; + readonly name: string = "EtherscanABILoader"; apiKey?: string; baseURL: string; @@ -189,11 +189,19 @@ export class EtherscanABILoader implements ABILoader { } async getContract(address: string): Promise { - let url = this.baseURL + '?module=contract&action=getsourcecode&address=' + address; - if (this.apiKey) url += "&apikey=" + this.apiKey; + const url = new URL(this.baseURL) + const params = { + module: "contract", + action: "getsourcecode", + address: address, + ...(this.apiKey && { apikey: this.apiKey }), + }; + + // Using .set() to overwrite any default values that may be present in baseURL + Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value)); try { - const r = await fetchJSON(url); + const r = await fetchJSON(url.toString()); if (r.status === "0") { if (r.result === "Contract source code not verified") return emptyContractResult; throw new Error(r.result); // This gets wrapped below @@ -236,11 +244,19 @@ export class EtherscanABILoader implements ABILoader { } async loadABI(address: string): Promise { - let url = this.baseURL + '?module=contract&action=getabi&address=' + address; - if (this.apiKey) url += "&apikey=" + this.apiKey; + const url = new URL(this.baseURL) + const params = { + module: "contract", + action: "getabi", + address: address, + ...(this.apiKey && { apikey: this.apiKey }), + }; + + // Using .set() to overwrite any default values that may be present in baseURL + Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value)); try { - const r = await fetchJSON(url); + const r = await fetchJSON(url.toString()); if (r.status === "0") { if (r.result === "Contract source code not verified") return []; @@ -277,6 +293,14 @@ export type EtherscanContractResult = { SwarmSource: string; } +export class EtherscanV2ABILoader extends EtherscanABILoader { + readonly name: string = "EtherscanV2ABILoader"; + constructor(config: { apiKey: string, chainId?: number }) { + // chainId is a required parameter in v2, as is an API key + super({ apiKey: config.apiKey, baseURL: `https://api.etherscan.io/v2/api?chainid=${config?.chainId ?? 1}` }); + } +} + function isSourcifyNotFound(error: any): boolean { return ( From ebe29b5eb71b390e7f549566424a2d86d6cf74bf Mon Sep 17 00:00:00 2001 From: Kyle Thornton Date: Fri, 22 Nov 2024 15:22:23 -0800 Subject: [PATCH 3/6] The error status is inside err.cause --- src/loaders.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/loaders.ts b/src/loaders.ts index c3d122a..ea440e2 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -121,7 +121,7 @@ export class MultiABILoader implements ABILoader { return r; } } catch (err: any) { - if (err.status === 404) continue; + if (err.cause?.status === 404) continue; throw new MultiABILoaderError("MultiABILoader getContract error: " + err.message, { context: { loader, address }, @@ -143,6 +143,8 @@ export class MultiABILoader implements ABILoader { return r; } } catch (err: any) { + if (err.cause?.status === 404) continue; + throw new MultiABILoaderError("MultiABILoader loadABI error: " + err.message, { context: { loader, address }, cause: err, From 30a4ef1957d7c2aed0fb749e1ec4aa2a0d95644b Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 2 Dec 2024 15:33:11 -0700 Subject: [PATCH 4/6] src/__tests__/loaders.test.ts: Instrument AnyABI and EtherscanV2 and Multiloader --- src/__tests__/loaders.test.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/__tests__/loaders.test.ts b/src/__tests__/loaders.test.ts index 25341ed..18d3bcb 100644 --- a/src/__tests__/loaders.test.ts +++ b/src/__tests__/loaders.test.ts @@ -6,7 +6,9 @@ import { SourcifyABILoader, EtherscanABILoader, + EtherscanV2ABILoader, BlockscoutABILoader, + AnyABILoader, MultiABILoader, OpenChainSignatureLookup, @@ -237,9 +239,20 @@ describe_cached("loaders: ABILoader suite", async ({ env }) => { } const uniswapV2Router = "0x7a250d5630b4cf539739df2c5dacb4c659f2488d"; - makeTest(new SourcifyABILoader(), uniswapV2Router); - makeTest(new EtherscanABILoader({ apiKey: env["ETHERSCAN_API_KEY"] }), uniswapV2Router); - makeTest(new BlockscoutABILoader({ apiKey: env["BLOCKSCOUT_API_KEY"] }), uniswapV2Router); + + const loaders = [ + new SourcifyABILoader(), + new EtherscanABILoader({ apiKey: env["ETHERSCAN_API_KEY"] }), + new EtherscanV2ABILoader({ apiKey: env["ETHERSCAN_API_KEY"] }), + new BlockscoutABILoader({ apiKey: env["BLOCKSCOUT_API_KEY"] }), + new AnyABILoader(), + ]; + + for (const loader of loaders) { + makeTest(loader, uniswapV2Router); + } + + makeTest(new MultiABILoader(loaders), uniswapV2Router); }); From cf95d31c664798f67a9519f71eaba4eabc2a4677 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 2 Dec 2024 15:41:53 -0700 Subject: [PATCH 5/6] src/loaders.ts: Fix AnyABILoader to detect not-found properly --- src/loaders.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/loaders.ts b/src/loaders.ts index ea440e2..a5e2938 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -630,9 +630,9 @@ export type BlockscoutContractResult = { function isAnyABINotFound(error: any): boolean { return ( - error.message === "Failed to fetch" || - error.message === "ABI not found" || - error.status === 404 + error.status === 404 || + // "ABI not found" or "Not found" + /not found/i.test(error.message) ); } @@ -660,7 +660,7 @@ export class AnyABILoader implements ABILoader { loaderResult: r, }; } catch (err: any) { - if (!isAnyABINotFound(err)) return emptyContractResult; + if (isAnyABINotFound(err)) return emptyContractResult; throw new AnyABILoaderError("AnyABILoader load contract error: " + err.message, { context: { url }, cause: err, From 4d7cbeddc5fd9008a331da35ad8d1d3dc5f30591 Mon Sep 17 00:00:00 2001 From: Andrey Petrov Date: Mon, 2 Dec 2024 15:52:06 -0700 Subject: [PATCH 6/6] docs: Improve loader docstrings --- src/auto.ts | 2 +- src/loaders.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/auto.ts b/src/auto.ts index 39a07cc..36f7601 100644 --- a/src/auto.ts +++ b/src/auto.ts @@ -97,7 +97,7 @@ export type AutoloadConfig = { /** - * Load full contract metadata result, include it in {@link AutoloadResult.ContractResult} if successful. + * Load full contract metadata result, include it in {@link AutoloadResult.contractResult} if successful. * * This changes the behaviour of autoload to use {@link ABILoader.getContract} instead of {@link ABILoader.loadABI}, * which returns a larger superset result including all of the available verified contract metadata. diff --git a/src/loaders.ts b/src/loaders.ts index a5e2938..af397a3 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -97,7 +97,7 @@ export interface ABILoader { getContract(address: string): Promise } -// Load ABIs from multiple providers until a result is found. +/** Load ABIs from multiple providers until a result is found. */ export class MultiABILoader implements ABILoader { readonly name: string = "MultiABILoader"; @@ -158,6 +158,7 @@ export class MultiABILoader implements ABILoader { export class MultiABILoaderError extends errors.LoaderError { }; +/** Etherscan v1 API loader */ export class EtherscanABILoader implements ABILoader { readonly name: string = "EtherscanABILoader"; @@ -295,6 +296,8 @@ export type EtherscanContractResult = { SwarmSource: string; } + +/** Etherscan v2 API loader */ export class EtherscanV2ABILoader extends EtherscanABILoader { readonly name: string = "EtherscanV2ABILoader"; constructor(config: { apiKey: string, chainId?: number }) { @@ -451,6 +454,7 @@ export interface SourcifyContractMetadata { version: number; } +/** Blockscout API loader: https://docs.blockscout.com/ */ export class BlockscoutABILoader implements ABILoader { readonly name = "BlockscoutABILoader"; @@ -636,7 +640,7 @@ function isAnyABINotFound(error: any): boolean { ); } -// https://anyabi.xyz/ +/** https://anyabi.xyz/ */ export class AnyABILoader implements ABILoader { readonly name = "AnyABILoader"; @@ -723,7 +727,7 @@ export class MultiSignatureLookup implements SignatureLookup { } } -// https://www.4byte.directory/ +/** https://www.4byte.directory/ */ export class FourByteSignatureLookup implements SignatureLookup { async load(url: string): Promise { try { @@ -750,8 +754,10 @@ export class FourByteSignatureLookup implements SignatureLookup { export class FourByteSignatureLookupError extends errors.LoaderError { }; -// openchain.xyz -// Formerly: https://sig.eth.samczsun.com/ +/** + * https://openchain.xyz/ + * Formerly: https://sig.eth.samczsun.com/ + */ export class OpenChainSignatureLookup implements SignatureLookup { async load(url: string): Promise { try {