Skip to content

Commit c5c9010

Browse files
committed
Add support for Sourcify contract information lookup
- Contract name, ABI and creation transaction hash (start block) from [Sourcify API](https://docs.sourcify.dev/docs/api/). - Runs before the registry lookup and replaces default values (not interactive) if not provided by the user. This means priority for CLI parameters looks like: user submitted (env/CLI args) > Sourcify API > Default values > Registry fetch
1 parent 93d87a8 commit c5c9010

File tree

3 files changed

+101
-4
lines changed

3 files changed

+101
-4
lines changed

packages/cli/src/command-helpers/contracts.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,61 @@ export class ContractService {
151151
throw new Error(`Failed to fetch contract name for ${address}`);
152152
}
153153

154+
async getFromSourcify(
155+
ABICtor: typeof ABI,
156+
networkId: string,
157+
address: string,
158+
): Promise<{ abi: ABI; startBlock: string; name: string } | null> {
159+
try {
160+
const network = this.registry.getNetworkById(networkId);
161+
if (!network) throw new Error(`Invalid network ${networkId}`);
162+
163+
const chainId = network.caip2Id.split(':')[1];
164+
if (!/^\d+$/.test(chainId))
165+
throw new Error(`Invalid chainId, Sourcify API expects integer value, got '${chainId}'`);
166+
167+
const url = `https://sourcify.dev/server/files/any/${chainId}/${address}`;
168+
const json:
169+
| {
170+
status: string;
171+
files: { name: string; path: string; content: string }[];
172+
}
173+
| { error: string } = await (
174+
await fetch(url).catch(error => {
175+
throw new Error(`Sourcify API is unreachable: ${error}`);
176+
})
177+
).json();
178+
179+
if (json) {
180+
if ('error' in json) throw new Error(`Sourcify API error: ${json.error}`);
181+
182+
let metadata: any = json.files.find(e => e.name === 'metadata.json')?.content;
183+
if (!metadata) throw new Error('Contract is missing metadata');
184+
185+
const tx_hash = json.files.find(e => e.name === 'creator-tx-hash.txt')?.content;
186+
if (!tx_hash) throw new Error('Contract is missing tx creation hash');
187+
188+
const tx = await this.fetchTransactionByHash(networkId, tx_hash);
189+
if (!tx?.blockNumber)
190+
throw new Error(`Can't fetch blockNumber from tx: ${JSON.stringify(tx)}`);
191+
192+
metadata = JSON.parse(metadata);
193+
const contractName = Object.values(metadata.settings.compilationTarget)[0] as string;
194+
return {
195+
abi: new ABICtor(contractName, undefined, immutable.fromJS(metadata.output.abi)) as ABI,
196+
startBlock: Number(tx.blockNumber).toString(),
197+
name: contractName,
198+
};
199+
}
200+
201+
throw new Error(`No result: ${JSON.stringify(json)}`);
202+
} catch (error) {
203+
logger(`Failed to fetch from Sourcify: ${error}`);
204+
}
205+
206+
return null;
207+
}
208+
154209
private async fetchTransactionByHash(networkId: string, txHash: string) {
155210
const urls = this.getRpcUrls(networkId);
156211
if (!urls.length) {

packages/cli/src/commands/add.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,24 @@ export default class AddCommand extends Command {
8080
if (isLocalHost) this.warn('`localhost` network detected, prompting user for inputs');
8181
const registry = await loadRegistry();
8282
const contractService = new ContractService(registry);
83+
const sourcifyContractInfo = await contractService.getFromSourcify(
84+
EthereumABI,
85+
network,
86+
address,
87+
);
8388

8489
let startBlock = startBlockFlag ? parseInt(startBlockFlag).toString() : startBlockFlag;
8590
let contractName = contractNameFlag || DEFAULT_CONTRACT_NAME;
86-
8791
let ethabi = null;
88-
if (abi) {
92+
93+
if (sourcifyContractInfo) {
94+
startBlock ??= sourcifyContractInfo.startBlock;
95+
contractName =
96+
contractName == DEFAULT_CONTRACT_NAME ? sourcifyContractInfo.name : contractName;
97+
ethabi ??= sourcifyContractInfo.abi;
98+
}
99+
100+
if (!ethabi && abi) {
89101
ethabi = EthereumABI.load(contractName, abi);
90102
} else {
91103
try {

packages/cli/src/commands/init.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fs from 'node:fs';
22
import os from 'node:os';
33
import path from 'node:path';
44
import { filesystem, print, prompt, system } from 'gluegun';
5+
import immutable from 'immutable';
56
import { Args, Command, Flags } from '@oclif/core';
67
import { Network } from '@pinax/graph-networks-registry';
78
import { appendApiVersionForGraph } from '../command-helpers/compiler.js';
@@ -200,6 +201,11 @@ export default class InitCommand extends Command {
200201
if ((fromContract || spkgPath) && protocol && subgraphName && directory && network && node) {
201202
const registry = await loadRegistry();
202203
const contractService = new ContractService(registry);
204+
const sourcifyContractInfo = await contractService.getFromSourcify(
205+
EthereumABI,
206+
network,
207+
fromContract!,
208+
);
203209

204210
if (!protocolChoices.includes(protocol as ProtocolName)) {
205211
this.error(
@@ -222,7 +228,13 @@ export default class InitCommand extends Command {
222228
}
223229
} else {
224230
try {
225-
abi = await contractService.getABI(ABI, network, fromContract!);
231+
abi = sourcifyContractInfo
232+
? new EthereumABI(
233+
DEFAULT_CONTRACT_NAME,
234+
undefined,
235+
immutable.fromJS(sourcifyContractInfo.abi),
236+
)
237+
: await contractService.getABI(ABI, network, fromContract!);
226238
} catch (e) {
227239
this.exit(1);
228240
}
@@ -448,7 +460,7 @@ async function processInitForm(
448460
];
449461
};
450462

451-
let network = networks[0];
463+
let network: Network = networks[0];
452464
let protocolInstance: Protocol = new Protocol('ethereum');
453465
let isComposedSubgraph = false;
454466
let isSubstreams = false;
@@ -611,6 +623,22 @@ async function processInitForm(
611623
return address;
612624
}
613625

626+
const sourcifyContractInfo = await contractService.getFromSourcify(
627+
EthereumABI,
628+
network.id,
629+
address,
630+
);
631+
if (sourcifyContractInfo) {
632+
initStartBlock ??= sourcifyContractInfo.startBlock;
633+
initContractName ??= sourcifyContractInfo.name;
634+
initAbi ??= sourcifyContractInfo.abi;
635+
initDebugger.extend('processInitForm')(
636+
"infoFromSourcify: '%s'/'%s'",
637+
initStartBlock,
638+
initContractName
639+
);
640+
}
641+
614642
// If ABI is not provided, try to fetch it from Etherscan API
615643
if (protocolInstance.hasABIs() && !initAbi) {
616644
abiFromApi = await retryWithPrompt(() =>
@@ -622,6 +650,8 @@ async function processInitForm(
622650
),
623651
);
624652
initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name);
653+
} else {
654+
abiFromApi = initAbi;
625655
}
626656
// If startBlock is not provided, try to fetch it from Etherscan API
627657
if (!initStartBlock) {

0 commit comments

Comments
 (0)