Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Sourcify contract information lookup #1899

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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