Skip to content

Commit

Permalink
Merge pull request #152 from yohamta/blockscout-support
Browse files Browse the repository at this point in the history
loaders: Add Blockscout ABI support
  • Loading branch information
shazow authored Nov 21, 2024
2 parents fd61839 + 2792d46 commit 96bba57
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 1 deletion.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ WhatsABI is perfect for building procedural frontends, embedding in wallets, blo
**What can WhatsABI do**?
- Return selectors from bytecode.
- Look up function signatures from selectors.
- Helpers for looking up ABI and signatures from public databases (like Sourcify, Etherscan, OpenChain, 4Byte).
- Helpers for looking up ABI and signatures from public databases (like Sourcify, Etherscan, Blockscout, OpenChain, 4Byte).
- ✨ Resolve proxy contracts!
- Small bundle (less than 15 KB) that works with Ethers.js, Viem, and others.

Expand Down Expand Up @@ -93,6 +93,9 @@ const loader = new whatsabi.loaders.MultiABILoader([
new whatsabi.loaders.EtherscanABILoader({
apiKey: "...", // Replace the value with your Etherscan API key
}),
new whatsabi.loaders.BlockscoutABILoader({
apiKey: "...", // Replace the value with your Blockscout API key
}),
]);
const { abi, name, /* ... other metadata */ } = await loader.getContract(address));
```
Expand Down Expand Up @@ -217,6 +220,7 @@ console.log("Resolved to:", address);
$ cat .env # Write an .env file with your keys, or `cp .env.example .env`
export INFURA_API_KEY="..."
export ETHERSCAN_API_KEY="..."
export BLOCKSCOUT_API_KEY="..."
$ nix develop # Or use your system's package manager to install node/ts/etc
[dev] $ npm install
[dev] $ ONLINE=1 make test
Expand Down
47 changes: 47 additions & 0 deletions src/__tests__/loaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
SamczunSignatureLookup,
FourByteSignatureLookup,
MultiABILoader,
BlockscoutABILoader,
} from "../loaders";
import { selectorsFromABI } from "../index";

Expand Down Expand Up @@ -54,6 +55,16 @@ describe('loaders module', () => {
expect(selectors).toContain(sig);
}, SLOW_ETHERSCAN_TIMEOUT)

online_test('BlockscoutABILoader', async () => {
const loader = new BlockscoutABILoader({
apiKey: process.env["BLOCKSCOUT_API_KEY"],
});
const abi = await loader.loadABI("0x7a250d5630b4cf539739df2c5dacb4c659f2488d");
const selectors = Object.values(selectorsFromABI(abi));
const sig = "swapExactETHForTokens(uint256,address[],address,uint256)";
expect(selectors).toContain(sig);
})

online_test('MultiABILoader', async () => {
// A contract that is verified on etherscan but not sourcify
const address = "0xa9a57f7d2A54C1E172a7dC546fEE6e03afdD28E2";
Expand Down Expand Up @@ -123,6 +134,42 @@ describe('loaders module', () => {
expect(result.loaderResult?.Implementation).toMatch(/^0x[0-9a-f]{40}$/);
}, SLOW_ETHERSCAN_TIMEOUT)

online_test('BlockscoutABILoader_getContract', async () => {
const loader = new BlockscoutABILoader({
apiKey: process.env["BLOCKSCOUT_API_KEY"],
});
const result = await loader.getContract("0x7a250d5630b4cf539739df2c5dacb4c659f2488d");
const selectors = Object.values(selectorsFromABI(result.abi));
const sig = "swapExactETHForTokens(uint256,address[],address,uint256)";
expect(selectors).toContain(sig);
expect(result.name).toStrictEqual("UniswapV2Router02")
expect(result.loader?.name).toStrictEqual("BlockscoutABILoader");
expect(result.loaderResult?.source_code).toBeDefined();
expect(result.loaderResult?.compiler_settings).toBeDefined();

const sources = result.getSources && await result.getSources();
expect(sources && sources[0].content).toContain("pragma solidity");
})

online_test('BlockscoutABILoader_getContract_missing', async () => {
const loader = new BlockscoutABILoader({
apiKey: process.env["BLOCKSCOUT_API_KEY"],
});
const r = await loader.getContract("0x0000000000000000000000000000000000000000");
expect(r.ok).toBeFalsy();
})

online_test('BlockscoutABILoader_getContract_UniswapV3Factory', async () => {
const loader = new BlockscoutABILoader({
apiKey: process.env["BLOCKSCOUT_API_KEY"],
});
const { abi, name } = await loader.getContract("0x1F98431c8aD98523631AE4a59f267346ea31F984");
const selectors = Object.values(selectorsFromABI(abi));
const sig = "owner()";
expect(selectors).toContain(sig);
expect(name).toEqual("UniswapV3Factory");
})

online_test('MultiABILoader_getContract', async () => {
// A contract that is verified on etherscan but not sourcify
const address = "0xa9a57f7d2A54C1E172a7dC546fEE6e03afdD28E2";
Expand Down
177 changes: 177 additions & 0 deletions src/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,183 @@ export interface SourcifyContractMetadata {
version: number;
}


export class BlockscoutABILoader implements ABILoader {
readonly name = "BlockscoutABILoader";

apiKey?: string;
baseURL: string;

constructor(config?: { apiKey?: string; baseURL?: string }) {
if (config === undefined) config = {};
this.apiKey = config.apiKey;
this.baseURL = config.baseURL || "https://eth.blockscout.com/api";
}

/** Blockscout helper for converting the result arg to a decoded ContractSources. */
#toContractSources(result: {
file_path?: string;
source_code?: string;
additional_sources?: Array<{ file_path: string; source_code: string }>;
}): ContractSources {
const sources: ContractSources = [];

if (result.source_code) {
sources.push({
path: result.file_path,
content: result.source_code,
});
}

result.additional_sources?.forEach((source) => {
sources.push({
path: source.file_path,
content: source.source_code,
});
});

return sources;
}

async getContract(address: string): Promise<ContractResult> {
let url = this.baseURL + `/v2/smart-contracts/${address}`;
if (this.apiKey) url += "?apikey=" + this.apiKey;

try {
const r = await fetch(url);
const result = (await r.json()) as BlockscoutContractResult;

if (
!result.abi ||
!result.name ||
!result.compiler_version ||
!result.source_code
) {
return emptyContractResult;
}

return {
abi: result.abi,
name: result.name,
evmVersion: result.evm_version || "",
compilerVersion: result.compiler_version,
runs: result.optimization_runs || 200,

getSources: async () => {
try {
return this.#toContractSources(result);
} catch (err: any) {
throw new BlockscoutABILoaderError(
"BlockscoutABILoader getContract getSources error: " +
err.message,
{
context: { url, address },
cause: err,
}
);
}
},

ok: true,
loader: this,
loaderResult: result,
};
} catch (err: any) {
throw new BlockscoutABILoaderError(
"BlockscoutABILoader getContract error: " + err.message,
{
context: { url, address },
cause: err,
}
);
}
}

async loadABI(address: string): Promise<any[]> {
let url = this.baseURL + `/v2/smart-contracts/${address}`;
if (this.apiKey) url += "?apikey=" + this.apiKey;

try {
const r = await fetch(url);
const result = (await r.json()) as BlockscoutContractResult;
if (!result.abi) {
throw new Error("ABI is not found");
}
return result.abi;
} catch (err: any) {
throw new BlockscoutABILoaderError(
"BlockscoutABILoader loadABI error: " + err.message,
{
context: { url, address },
cause: err,
}
);
}
}
}

export class BlockscoutABILoaderError extends errors.LoaderError {}

/// Blockscout Contract Source API response
export type BlockscoutContractResult = {
// Basic contract information
name?: string;
language?: string;
license_type?: string;

// Verification status
is_verified?: boolean;
is_fully_verified?: boolean;
is_partially_verified?: boolean;
is_verified_via_sourcify?: boolean;
is_verified_via_eth_bytecode_db?: boolean;
verified_twin_address_hash?: string;
certified?: boolean;

// Source code and files
source_code?: string;
source_code_html?: string;
file_path?: string;
file_path_html?: string;
additional_sources?: Array<{ file_path: string; source_code: string }>;
sourcify_repo_url?: string | null;

// Bytecode and implementation
creation_bytecode?: string;
deployed_bytecode?: string;
is_changed_bytecode?: boolean;
is_self_destructed?: boolean;

// Contract interface
abi?: any[];
has_methods_read?: boolean;
has_methods_write?: boolean;
has_custom_methods_read?: boolean;
has_custom_methods_write?: boolean;
can_be_visualized_via_sol2uml?: boolean;

// Constructor and initialization
constructor_args?: string;
decoded_constructor_args?: any[];

// Compiler settings
compiler_version?: string;
compiler_settings?: any;
evm_version?: string;
optimization_enabled?: boolean;
optimization_runs?: number | null;

// Contract type specifics
is_blueprint?: boolean;
is_vyper_contract?: boolean;

// Proxy-related
proxy_type?: string;
implementations?: Array<{ address?: string; name?: string }>;
has_methods_read_proxy?: boolean;
has_methods_write_proxy?: boolean;
};

export interface SignatureLookup {
loadFunctions(selector: string): Promise<string[]>;
loadEvents(hash: string): Promise<string[]>;
Expand Down

0 comments on commit 96bba57

Please sign in to comment.