From 6464dbd71d982e4372d6dfc15ba32e37cbc0bd44 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) - Non-functional yet as the `chainId` is required which is missing from the registry --- packages/cli/src/command-helpers/contracts.ts | 50 +++++++++++++++++++ packages/cli/src/commands/add.ts | 16 +++++- packages/cli/src/commands/init.ts | 27 +++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/command-helpers/contracts.ts b/packages/cli/src/command-helpers/contracts.ts index eb6e35642..700f86eb6 100644 --- a/packages/cli/src/command-helpers/contracts.ts +++ b/packages/cli/src/command-helpers/contracts.ts @@ -151,6 +151,56 @@ 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.chainId}/${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 contractName = Object.values(metadata.settings.compilationTarget)[0] as string; + metadata = JSON.parse(metadata); + return { + abi: new ABICtor(contractName, undefined, immutable.fromJS(metadata.output.abi)) as ABI, + startBlock: await this.fetchTransactionByHash(networkId, tx_hash), + 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 776280be4..826781ca6 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 f50927eb6..285e75d4c 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,17 @@ 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; + } + // If ABI is not provided, try to fetch it from Etherscan API if (protocolInstance.hasABIs() && !initAbi) { abiFromApi = await retryWithPrompt(() =>