Skip to content

Commit e9206b6

Browse files
committed
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)
1 parent 93d87a8 commit e9206b6

File tree

3 files changed

+99
-4
lines changed

3 files changed

+99
-4
lines changed

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,58 @@ 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 url = `https://sourcify.dev/server/files/any/${network.caip2Id.split(':')[1]}/${address}`; // chainId currently missing from registry
164+
165+
const json:
166+
| {
167+
status: string;
168+
files: { name: string; path: string; content: string }[];
169+
}
170+
| { error: string } = await (
171+
await fetch(url).catch(error => {
172+
throw new Error(`Sourcify API is unreachable: ${error}`);
173+
})
174+
).json();
175+
176+
if (json) {
177+
if ('error' in json) throw new Error(`Sourcify API error: ${json.error}`);
178+
179+
let metadata: any = json.files.find(e => e.name === 'metadata.json')?.content;
180+
if (!metadata) throw new Error('Contract is missing metadata');
181+
182+
const tx_hash = json.files.find(e => e.name === 'creator-tx-hash.txt')?.content;
183+
if (!tx_hash) throw new Error('Contract is missing tx creation hash');
184+
185+
const tx = await this.fetchTransactionByHash(networkId, tx_hash);
186+
if (!tx?.blockNumber)
187+
throw new Error(`Can't fetch blockNumber from tx: ${JSON.stringify(tx)}`);
188+
189+
metadata = JSON.parse(metadata);
190+
const contractName = Object.values(metadata.settings.compilationTarget)[0] as string;
191+
return {
192+
abi: new ABICtor(contractName, undefined, immutable.fromJS(metadata.output.abi)) as ABI,
193+
startBlock: Number(tx.blockNumber).toString(),
194+
name: contractName,
195+
};
196+
}
197+
198+
throw new Error(`No result: ${JSON.stringify(json)}`);
199+
} catch (error) {
200+
logger(`Failed to fetch from Sourcify: ${error}`);
201+
}
202+
203+
return null;
204+
}
205+
154206
private async fetchTransactionByHash(networkId: string, txHash: string) {
155207
const urls = this.getRpcUrls(networkId);
156208
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: 33 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,23 @@ 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'/'%s'",
637+
initStartBlock,
638+
initContractName,
639+
initAbi?.name,
640+
);
641+
}
642+
614643
// If ABI is not provided, try to fetch it from Etherscan API
615644
if (protocolInstance.hasABIs() && !initAbi) {
616645
abiFromApi = await retryWithPrompt(() =>
@@ -622,6 +651,8 @@ async function processInitForm(
622651
),
623652
);
624653
initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name);
654+
} else {
655+
abiFromApi = initAbi;
625656
}
626657
// If startBlock is not provided, try to fetch it from Etherscan API
627658
if (!initStartBlock) {

0 commit comments

Comments
 (0)