diff --git a/packages/cli/src/command-helpers/abi.ts b/packages/cli/src/command-helpers/abi.ts index 8c17884eb..2709929f0 100644 --- a/packages/cli/src/command-helpers/abi.ts +++ b/packages/cli/src/command-helpers/abi.ts @@ -7,25 +7,44 @@ import { withSpinner } from './spinner'; const logger = debugFactory('graph-cli:abi-helpers'); -export const loadAbiFromEtherscan = async ( +export const loadAbiFromSourcify = async ( ABICtor: typeof ABI, network: string, address: string, ): Promise => await withSpinner( - `Fetching ABI from Etherscan`, - `Failed to fetch ABI from Etherscan`, - `Warnings while fetching ABI from Etherscan`, + `Fetching ABI from Sourcify`, + `Failed to fetch ABI from Sourcify`, + `Warnings while fetching ABI from Sourcify`, async () => { - const scanApiUrl = getEtherscanLikeAPIUrl(network); - const result = await fetch(`${scanApiUrl}?module=contract&action=getabi&address=${address}`); + const chainId = await getSourcifyChainId(network); + const result = await fetch(`https://repo.sourcify.dev/contracts/full_match/${chainId}/${address}/metadata.json`); const json = await result.json(); // Etherscan returns a JSON object that has a `status`, a `message` and // a `result` field. The `status` is '0' in case of errors and '1' in // case of success - if (json.status === '1') { - return new ABICtor('Contract', undefined, immutable.fromJS(JSON.parse(json.result))); + if (result.ok) { + return new ABICtor('Contract', undefined, immutable.fromJS(json.output.abi)); + } + throw new Error('ABI not found, try loading it from a local file'); + }, + ); + +export const loadAbiFromEtherscan = async ( + ABICtor: typeof ABI, + network: string, + address: string, +): Promise => + await withSpinner( + `Fetching ABI from Sourcify`, + `Failed to fetch ABI from Sourcify`, + `Warnings while fetching ABI from Sourcify`, + async () => { + const json = await fetchMetadataFromSourcify(network, address); + + if (json) { + return new ABICtor('Contract', undefined, immutable.fromJS(json.output.abi)); } throw new Error('ABI not found, try loading it from a local file'); }, @@ -51,7 +70,7 @@ export const loadContractNameForAddress = async ( await withSpinner( `Fetching Contract Name`, `Failed to fetch Contract Name`, - `Warnings while fetching contract name from Etherscan`, + `Warnings while fetching contract name from Sourcify`, async () => { return getContractNameForAddress(network, address); }, @@ -124,19 +143,19 @@ export const fetchTransactionByHashFromRPC = async ( } }; -export const fetchSourceCodeFromEtherscan = async ( +export const fetchMetadataFromSourcify = async ( network: string, address: string, ): Promise => { - const scanApiUrl = getEtherscanLikeAPIUrl(network); + const chainId = await getSourcifyChainId(network); const result = await fetch( - `${scanApiUrl}?module=contract&action=getsourcecode&address=${address}`, + `https://repo.sourcify.dev/contracts/full_match/${chainId}/${address}/metadata.json`, ); const json = await result.json(); - if (json.status === '1') { + if (result.ok) { return json; } - throw new Error('Failed to fetch contract source code'); + throw new Error('Failed to fetch metadata for address'); }; export const getContractNameForAddress = async ( @@ -144,8 +163,8 @@ export const getContractNameForAddress = async ( address: string, ): Promise => { try { - const contractSourceCode = await fetchSourceCodeFromEtherscan(network, address); - const contractName = contractSourceCode.result[0].ContractName; + const json = await fetchMetadataFromSourcify(network, address); + const contractName = Object.values(json.settings.compilationTarget)[0] as string; logger('Successfully getContractNameForAddress. contractName: %s', contractName); return contractName; } catch (error) { @@ -200,6 +219,19 @@ export const loadAbiFromBlockScout = async ( }, ); +const getSourcifyChainId = async (network: string) => { + const result = await fetch('https://sourcify.dev/server/chains'); + const json = await result.json(); + + // Can fail if network name doesn't follow https://chainlist.org name convention + const match = json.find((e: any) => e.name.toLowerCase().includes(network.replace('-', ' '))); + + if (match) + return match.chainId; + else + throw new Error(`Could not find chain id for "${network}"`); +}; + const getEtherscanLikeAPIUrl = (network: string) => { switch (network) { case 'mainnet': diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index 2bbd5f9cd..494b7867f 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -4,7 +4,7 @@ import { Args, Command, Flags } from '@oclif/core'; import { CLIError } from '@oclif/core/lib/errors'; import { loadAbiFromBlockScout, - loadAbiFromEtherscan, + loadAbiFromSourcify, loadContractNameForAddress, loadStartBlockForContract, } from '../command-helpers/abi'; @@ -96,7 +96,7 @@ export default class AddCommand extends Command { } else if (network === 'poa-core') { ethabi = await loadAbiFromBlockScout(EthereumABI, network, address); } else { - ethabi = await loadAbiFromEtherscan(EthereumABI, network, address); + ethabi = await loadAbiFromSourcify(EthereumABI, network, address); } try { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 2712d78db..17c960d20 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -6,7 +6,7 @@ import { filesystem, prompt, system } from 'gluegun'; import { Args, Command, Flags, ux } from '@oclif/core'; import { loadAbiFromBlockScout, - loadAbiFromEtherscan, + loadAbiFromSourcify, loadContractNameForAddress, loadStartBlockForContract, } from '../command-helpers/abi'; @@ -292,7 +292,7 @@ export default class InitCommand extends Command { if (network === 'poa-core') { abi = await loadAbiFromBlockScout(ABI, network, fromContract); } else { - abi = await loadAbiFromEtherscan(ABI, network, fromContract); + abi = await loadAbiFromSourcify(ABI, network, fromContract); } } catch (e) { process.exitCode = 1; @@ -544,7 +544,7 @@ async function processInitForm( } | undefined > { - let abiFromEtherscan: EthereumABI | undefined = undefined; + let abiFromSourcify: EthereumABI | undefined = undefined; try { const { protocol } = await prompt.ask<{ protocol: ProtocolName }>({ @@ -705,17 +705,11 @@ async function processInitForm( const ABI = protocolInstance.getABI(); - // Try loading the ABI from Etherscan, if none was provided + // Try loading the ABI from Sourcify, if none was provided if (protocolInstance.hasABIs() && !initAbi) { - if (network === 'poa-core') { - abiFromEtherscan = await retryWithPrompt(() => - loadAbiFromBlockScout(ABI, network, value), - ); - } else { - abiFromEtherscan = await retryWithPrompt(() => - loadAbiFromEtherscan(ABI, network, value), - ); - } + abiFromSourcify = await retryWithPrompt(() => + loadAbiFromSourcify(ABI, network, value), + ); } // If startBlock is not set, try to load it. if (!initStartBlock) { @@ -765,11 +759,11 @@ async function processInitForm( skip: () => !protocolInstance.hasABIs() || initFromExample !== undefined || - abiFromEtherscan !== undefined || + abiFromSourcify !== undefined || isSubstreams || !!initAbiPath, validate: async (value: string) => { - if (initFromExample || abiFromEtherscan || !protocolInstance.hasABIs()) { + if (initFromExample || abiFromSourcify || !protocolInstance.hasABIs()) { return true; } @@ -791,7 +785,7 @@ async function processInitForm( } }, result: async (value: string) => { - if (initFromExample || abiFromEtherscan || !protocolInstance.hasABIs()) { + if (initFromExample || abiFromSourcify || !protocolInstance.hasABIs()) { return null; } const ABI = protocolInstance.getABI(); @@ -853,7 +847,7 @@ async function processInitForm( ]); return { - abi: abiFromEtherscan || abiFromFile, + abi: abiFromSourcify || abiFromFile, protocolInstance, subgraphName, directory,