Skip to content

Commit 6464dbd

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) - Non-functional yet as the `chainId` is required which is missing from the registry
1 parent 93d87a8 commit 6464dbd

File tree

3 files changed

+89
-4
lines changed

3 files changed

+89
-4
lines changed

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,56 @@ 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) {
162+
throw new Error(`Invalid network ${networkId}`);
163+
}
164+
165+
const url = `https://sourcify.dev/server/files/any/${network.chainId}/${address}`; // chainId currently missing from registry
166+
167+
const json:
168+
| {
169+
status: string;
170+
files: { name: string; path: string; content: string; }[];
171+
}
172+
| { error: string; } = await (
173+
await fetch(url).catch(error => {
174+
throw new Error(`Sourcify API is unreachable: ${error}`);
175+
})
176+
).json();
177+
178+
if (json) {
179+
if ('error' in json) throw new Error(`Sourcify API error: ${json.error}`);
180+
181+
let metadata: any = json.files.find(e => e.name === 'metadata.json')?.content;
182+
if (!metadata) throw new Error('Contract is missing metadata');
183+
184+
const tx_hash = json.files.find(e => e.name === 'creator-tx-hash.txt')?.content;
185+
if (!tx_hash) throw new Error('Contract is missing tx creation hash');
186+
187+
const contractName = Object.values(metadata.settings.compilationTarget)[0] as string;
188+
metadata = JSON.parse(metadata);
189+
return {
190+
abi: new ABICtor(contractName, undefined, immutable.fromJS(metadata.output.abi)) as ABI,
191+
startBlock: await this.fetchTransactionByHash(networkId, tx_hash),
192+
name: contractName,
193+
};
194+
}
195+
196+
throw new Error(`No result: ${JSON.stringify(json)}`);
197+
} catch (error) {
198+
logger(`Failed to fetch from Sourcify: ${error}`);
199+
}
200+
201+
return null;
202+
}
203+
154204
private async fetchTransactionByHash(networkId: string, txHash: string) {
155205
const urls = this.getRpcUrls(networkId);
156206
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: 25 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,17 @@ 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+
}
636+
614637
// If ABI is not provided, try to fetch it from Etherscan API
615638
if (protocolInstance.hasABIs() && !initAbi) {
616639
abiFromApi = await retryWithPrompt(() =>

0 commit comments

Comments
 (0)