Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/spotty-geckos-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphprotocol/graph-cli': minor
---

Add support for Sourcify contract information lookup
55 changes: 55 additions & 0 deletions packages/cli/src/command-helpers/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
}
}

throw new Error(`Failed to fetch deploy contract transaction for ${address}`);

Check failure on line 125 in packages/cli/src/command-helpers/contracts.ts

View workflow job for this annotation

GitHub Actions / CLI / nodejs v22

src/command-helpers/contracts.test.ts > getStartBlockForContract > Returns the start block moonbeam 0x011E52E4E40CF9498c79273329E8827b21E2e581 505060

Error: Failed to fetch deploy contract transaction for 0x011E52E4E40CF9498c79273329E8827b21E2e581 ❯ ContractService.getStartBlock src/command-helpers/contracts.ts:125:11 ❯ test.timeout src/command-helpers/contracts.test.ts:97:30
}

async getContractName(networkId: string, address: string): Promise<string> {
Expand Down Expand Up @@ -151,6 +151,61 @@
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 chainId = network.caip2Id.split(':')[1];
if (!/^\d+$/.test(chainId))
throw new Error(`Invalid chainId, Sourcify API expects integer value, got '${chainId}'`);

const url = `https://sourcify.dev/server/files/any/${chainId}/${address}`;
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) {
Expand Down
16 changes: 14 additions & 2 deletions packages/cli/src/commands/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
29 changes: 27 additions & 2 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,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(
Expand All @@ -222,7 +227,9 @@ export default class InitCommand extends Command {
}
} else {
try {
abi = await contractService.getABI(ABI, network, fromContract!);
abi = sourcifyContractInfo
? sourcifyContractInfo.abi
: await contractService.getABI(ABI, network, fromContract!);
} catch (e) {
this.exit(1);
}
Expand Down Expand Up @@ -448,7 +455,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;
Expand Down Expand Up @@ -611,6 +618,22 @@ 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'",
initStartBlock,
initContractName,
);
}

// If ABI is not provided, try to fetch it from Etherscan API
if (protocolInstance.hasABIs() && !initAbi) {
abiFromApi = await retryWithPrompt(() =>
Expand All @@ -622,6 +645,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) {
Expand Down
Loading