From e9206b6a52e36c13c8686fb5d0c1a6d76ae66151 Mon Sep 17 00:00:00 2001 From: Etienne Donneger Date: Fri, 10 Jan 2025 12:31:06 -0500 Subject: [PATCH] Add support for Sourcify contract information lookup - Contract name, ABI and creation transaction hash (start block) - Runs before the registry lookup and replaces default values (not interactive) --- packages/cli/src/command-helpers/contracts.ts | 52 +++++++++++++++++++ packages/cli/src/commands/add.ts | 16 +++++- packages/cli/src/commands/init.ts | 35 ++++++++++++- 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/command-helpers/contracts.ts b/packages/cli/src/command-helpers/contracts.ts index eb6e3564..ee1ba2e7 100644 --- a/packages/cli/src/command-helpers/contracts.ts +++ b/packages/cli/src/command-helpers/contracts.ts @@ -151,6 +151,58 @@ export class ContractService { throw new Error(`Failed to fetch contract name for ${address}`); } + async getFromSourcify( + ABICtor: typeof ABI, + networkId: string, + address: string, + ): Promise<{ abi: ABI; startBlock: string; name: string } | null> { + try { + const network = this.registry.getNetworkById(networkId); + if (!network) throw new Error(`Invalid network ${networkId}`); + + const url = `https://sourcify.dev/server/files/any/${network.caip2Id.split(':')[1]}/${address}`; // chainId currently missing from registry + + const json: + | { + status: string; + files: { name: string; path: string; content: string }[]; + } + | { error: string } = await ( + await fetch(url).catch(error => { + throw new Error(`Sourcify API is unreachable: ${error}`); + }) + ).json(); + + if (json) { + if ('error' in json) throw new Error(`Sourcify API error: ${json.error}`); + + let metadata: any = json.files.find(e => e.name === 'metadata.json')?.content; + if (!metadata) throw new Error('Contract is missing metadata'); + + const tx_hash = json.files.find(e => e.name === 'creator-tx-hash.txt')?.content; + if (!tx_hash) throw new Error('Contract is missing tx creation hash'); + + const tx = await this.fetchTransactionByHash(networkId, tx_hash); + if (!tx?.blockNumber) + throw new Error(`Can't fetch blockNumber from tx: ${JSON.stringify(tx)}`); + + metadata = JSON.parse(metadata); + const contractName = Object.values(metadata.settings.compilationTarget)[0] as string; + return { + abi: new ABICtor(contractName, undefined, immutable.fromJS(metadata.output.abi)) as ABI, + startBlock: Number(tx.blockNumber).toString(), + name: contractName, + }; + } + + throw new Error(`No result: ${JSON.stringify(json)}`); + } catch (error) { + logger(`Failed to fetch from Sourcify: ${error}`); + } + + return null; + } + private async fetchTransactionByHash(networkId: string, txHash: string) { const urls = this.getRpcUrls(networkId); if (!urls.length) { diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index 776280be..826781ca 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -80,12 +80,24 @@ export default class AddCommand extends Command { if (isLocalHost) this.warn('`localhost` network detected, prompting user for inputs'); const registry = await loadRegistry(); const contractService = new ContractService(registry); + const sourcifyContractInfo = await contractService.getFromSourcify( + EthereumABI, + network, + address, + ); let startBlock = startBlockFlag ? parseInt(startBlockFlag).toString() : startBlockFlag; let contractName = contractNameFlag || DEFAULT_CONTRACT_NAME; - let ethabi = null; - if (abi) { + + if (sourcifyContractInfo) { + startBlock ??= sourcifyContractInfo.startBlock; + contractName = + contractName == DEFAULT_CONTRACT_NAME ? sourcifyContractInfo.name : contractName; + ethabi ??= sourcifyContractInfo.abi; + } + + if (!ethabi && abi) { ethabi = EthereumABI.load(contractName, abi); } else { try { diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index f50927eb..230c8ebc 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -2,6 +2,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { filesystem, print, prompt, system } from 'gluegun'; +import immutable from 'immutable'; import { Args, Command, Flags } from '@oclif/core'; import { Network } from '@pinax/graph-networks-registry'; import { appendApiVersionForGraph } from '../command-helpers/compiler.js'; @@ -200,6 +201,11 @@ export default class InitCommand extends Command { if ((fromContract || spkgPath) && protocol && subgraphName && directory && network && node) { const registry = await loadRegistry(); const contractService = new ContractService(registry); + const sourcifyContractInfo = await contractService.getFromSourcify( + EthereumABI, + network, + fromContract!, + ); if (!protocolChoices.includes(protocol as ProtocolName)) { this.error( @@ -222,7 +228,13 @@ export default class InitCommand extends Command { } } else { try { - abi = await contractService.getABI(ABI, network, fromContract!); + abi = sourcifyContractInfo + ? new EthereumABI( + DEFAULT_CONTRACT_NAME, + undefined, + immutable.fromJS(sourcifyContractInfo.abi), + ) + : await contractService.getABI(ABI, network, fromContract!); } catch (e) { this.exit(1); } @@ -448,7 +460,7 @@ async function processInitForm( ]; }; - let network = networks[0]; + let network: Network = networks[0]; let protocolInstance: Protocol = new Protocol('ethereum'); let isComposedSubgraph = false; let isSubstreams = false; @@ -611,6 +623,23 @@ async function processInitForm( return address; } + const sourcifyContractInfo = await contractService.getFromSourcify( + EthereumABI, + network.id, + address, + ); + if (sourcifyContractInfo) { + initStartBlock ??= sourcifyContractInfo.startBlock; + initContractName ??= sourcifyContractInfo.name; + initAbi ??= sourcifyContractInfo.abi; + initDebugger.extend('processInitForm')( + "infoFromSourcify: '%s'/'%s'/'%s'", + initStartBlock, + initContractName, + initAbi?.name, + ); + } + // If ABI is not provided, try to fetch it from Etherscan API if (protocolInstance.hasABIs() && !initAbi) { abiFromApi = await retryWithPrompt(() => @@ -622,6 +651,8 @@ async function processInitForm( ), ); initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name); + } else { + abiFromApi = initAbi; } // If startBlock is not provided, try to fetch it from Etherscan API if (!initStartBlock) {