@graphprotocol/graph-cli 0.94.0 → 0.95.0-alpha-20250122003255-e449cf69b90cf3813ea73e4b363230c38dc9257f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @graphprotocol/graph-cli
2
2
 
3
+ ## 0.95.0-alpha-20250122003255-e449cf69b90cf3813ea73e4b363230c38dc9257f
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1899](https://github.com/graphprotocol/graph-tooling/pull/1899)
8
+ [`8cdaf31`](https://github.com/graphprotocol/graph-tooling/commit/8cdaf31e78c7fb7bf7ef4f5380d0c72b2a05b08d)
9
+ Thanks [@0237h](https://github.com/0237h)! - Add support for Sourcify contract information lookup
10
+
11
+ ### Patch Changes
12
+
13
+ - [#1903](https://github.com/graphprotocol/graph-tooling/pull/1903)
14
+ [`bcaad5e`](https://github.com/graphprotocol/graph-tooling/commit/bcaad5e077e485224c06842491145d05215f4c0e)
15
+ Thanks [@0237h](https://github.com/0237h)! - Fix `import.meta.url` not being parsed as path
16
+ properly
17
+
3
18
  ## 0.94.0
4
19
 
5
20
  ### Minor Changes
@@ -10,5 +10,10 @@ export declare class ContractService {
10
10
  getABI(ABICtor: typeof ABI, networkId: string, address: string): Promise<ABI>;
11
11
  getStartBlock(networkId: string, address: string): Promise<string>;
12
12
  getContractName(networkId: string, address: string): Promise<string>;
13
+ getFromSourcify(ABICtor: typeof ABI, networkId: string, address: string): Promise<{
14
+ abi: ABI;
15
+ startBlock: string;
16
+ name: string;
17
+ } | null>;
13
18
  private fetchTransactionByHash;
14
19
  }
@@ -122,6 +122,45 @@ export class ContractService {
122
122
  }
123
123
  throw new Error(`Failed to fetch contract name for ${address}`);
124
124
  }
125
+ async getFromSourcify(ABICtor, networkId, address) {
126
+ try {
127
+ const network = this.registry.getNetworkById(networkId);
128
+ if (!network)
129
+ throw new Error(`Invalid network ${networkId}`);
130
+ if (!network.caip2Id.startsWith('eip155'))
131
+ throw new Error(`Invalid chainId, Sourcify API only supports EVM chains`);
132
+ const chainId = network.caip2Id.split(':')[1];
133
+ const url = `https://sourcify.dev/server/files/any/${chainId}/${address}`;
134
+ const json = await (await fetch(url).catch(error => {
135
+ throw new Error(`Sourcify API is unreachable: ${error}`);
136
+ })).json();
137
+ if (json) {
138
+ if ('error' in json)
139
+ throw new Error(`Sourcify API error: ${json.error}`);
140
+ let metadata = json.files.find(e => e.name === 'metadata.json')?.content;
141
+ if (!metadata)
142
+ throw new Error('Contract is missing metadata');
143
+ const tx_hash = json.files.find(e => e.name === 'creator-tx-hash.txt')?.content;
144
+ if (!tx_hash)
145
+ throw new Error('Contract is missing tx creation hash');
146
+ const tx = await this.fetchTransactionByHash(networkId, tx_hash);
147
+ if (!tx?.blockNumber)
148
+ throw new Error(`Can't fetch blockNumber from tx: ${JSON.stringify(tx)}`);
149
+ metadata = JSON.parse(metadata);
150
+ const contractName = Object.values(metadata.settings.compilationTarget)[0];
151
+ return {
152
+ abi: new ABICtor(contractName, undefined, immutable.fromJS(metadata.output.abi)),
153
+ startBlock: Number(tx.blockNumber).toString(),
154
+ name: contractName,
155
+ };
156
+ }
157
+ throw new Error(`No result: ${JSON.stringify(json)}`);
158
+ }
159
+ catch (error) {
160
+ logger(`Failed to fetch from Sourcify: ${error}`);
161
+ }
162
+ return null;
163
+ }
125
164
  async fetchTransactionByHash(networkId, txHash) {
126
165
  const urls = this.getRpcUrls(networkId);
127
166
  if (!urls.length) {
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from 'vitest';
2
+ import EthereumABI from '../protocols/ethereum/abi.js';
2
3
  import { ContractService } from './contracts.js';
3
4
  import { loadRegistry } from './registry.js';
4
5
  // An object with some test cases for contract deployment block numbers
@@ -79,16 +80,76 @@ const TEST_CONTRACT_START_BLOCKS = {
79
80
  // clover: {
80
81
  // },
81
82
  };
82
- describe('getStartBlockForContract', { sequential: true }, async () => {
83
+ const TEST_SOURCIFY_CONTRACT_INFO = {
84
+ mainnet: {
85
+ '0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd': {
86
+ name: 'MasterChef',
87
+ startBlock: 10_736_242,
88
+ },
89
+ },
90
+ optimism: {
91
+ '0xc35DADB65012eC5796536bD9864eD8773aBc74C4': {
92
+ name: 'BentoBoxV1',
93
+ startBlock: 7_019_815,
94
+ },
95
+ },
96
+ wax: {
97
+ account: {
98
+ name: null,
99
+ startBlock: null,
100
+ },
101
+ },
102
+ 'non-existing chain': {
103
+ '0x0000000000000000000000000000000000000000': {
104
+ name: null,
105
+ startBlock: null,
106
+ },
107
+ },
108
+ };
109
+ // Retry helper with configurable number of retries
110
+ async function retry(operation, maxRetries = 3, sleepMs = 5000) {
111
+ let lastError;
112
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
113
+ try {
114
+ return await operation();
115
+ }
116
+ catch (error) {
117
+ lastError = error;
118
+ if (attempt < maxRetries - 1) {
119
+ await new Promise(resolve => setTimeout(resolve, sleepMs));
120
+ }
121
+ }
122
+ }
123
+ throw lastError;
124
+ }
125
+ describe('getStartBlockForContract', { concurrent: true }, async () => {
83
126
  const registry = await loadRegistry();
84
127
  const contractService = new ContractService(registry);
85
128
  for (const [network, contracts] of Object.entries(TEST_CONTRACT_START_BLOCKS)) {
86
129
  for (const [contract, startBlockExp] of Object.entries(contracts)) {
87
- test(`Returns the start block ${network} ${contract} ${startBlockExp}`, async () => {
88
- //loop through the TEST_CONTRACT_START_BLOCKS object and test each network
89
- const startBlock = await contractService.getStartBlock(network, contract);
130
+ test(`Returns the start block ${network} ${contract} ${startBlockExp}`, { timeout: 50_000 }, async ({ expect }) => {
131
+ const startBlock = await retry(() => contractService.getStartBlock(network, contract), 10);
90
132
  expect(parseInt(startBlock)).toBe(startBlockExp);
91
- }, { timeout: 10_000 });
133
+ });
134
+ }
135
+ }
136
+ });
137
+ describe('getFromSourcifyForContract', { concurrent: true }, async () => {
138
+ const registry = await loadRegistry();
139
+ const contractService = new ContractService(registry);
140
+ for (const [networkId, contractInfo] of Object.entries(TEST_SOURCIFY_CONTRACT_INFO)) {
141
+ for (const [contract, t] of Object.entries(contractInfo)) {
142
+ test(`Returns contract information ${networkId} ${contract} ${t.name} ${t.startBlock}`, { timeout: 50_000 }, async () => {
143
+ const result = await retry(() => contractService.getFromSourcify(EthereumABI, networkId, contract));
144
+ if (t.name === null && t.startBlock === null) {
145
+ expect(result).toBeNull();
146
+ }
147
+ else {
148
+ // Only check name and startBlock, omit API property from Sourcify results
149
+ const { name, startBlock } = result;
150
+ expect(t).toEqual({ name, startBlock: parseInt(startBlock) });
151
+ }
152
+ });
92
153
  }
93
154
  }
94
155
  });
@@ -1,9 +1,10 @@
1
1
  import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
2
3
  import * as toolbox from 'gluegun';
3
4
  import { afterAll, beforeAll, describe, expect, test } from 'vitest';
4
5
  import yaml from 'yaml';
5
6
  import { initNetworksConfig, updateSubgraphNetwork } from './network.js';
6
- const SUBGRAPH_PATH_BASE = path.join(`${process.platform === 'win32' ? '' : '/'}${/file:\/{2,3}(.+)\/[^/]/.exec(import.meta.url)[1]}`, '..', '..', '..', '..', 'examples', 'example-subgraph');
7
+ const SUBGRAPH_PATH_BASE = path.join(fileURLToPath(import.meta.url), '..', '..', '..', '..', '..', 'examples', 'example-subgraph');
7
8
  describe('initNetworksConfig', () => {
8
9
  beforeAll(async () => {
9
10
  await initNetworksConfig(SUBGRAPH_PATH_BASE, 'address');
@@ -58,10 +58,17 @@ export default class AddCommand extends Command {
58
58
  this.warn('`localhost` network detected, prompting user for inputs');
59
59
  const registry = await loadRegistry();
60
60
  const contractService = new ContractService(registry);
61
+ const sourcifyContractInfo = await contractService.getFromSourcify(EthereumABI, network, address);
61
62
  let startBlock = startBlockFlag ? parseInt(startBlockFlag).toString() : startBlockFlag;
62
63
  let contractName = contractNameFlag || DEFAULT_CONTRACT_NAME;
63
64
  let ethabi = null;
64
- if (abi) {
65
+ if (sourcifyContractInfo) {
66
+ startBlock ??= sourcifyContractInfo.startBlock;
67
+ contractName =
68
+ contractName == DEFAULT_CONTRACT_NAME ? sourcifyContractInfo.name : contractName;
69
+ ethabi ??= sourcifyContractInfo.abi;
70
+ }
71
+ if (!ethabi && abi) {
65
72
  ethabi = EthereumABI.load(contractName, abi);
66
73
  }
67
74
  else {
@@ -18,6 +18,7 @@ import { withSpinner } from '../command-helpers/spinner.js';
18
18
  import { getSubgraphBasename } from '../command-helpers/subgraph.js';
19
19
  import { GRAPH_CLI_SHARED_HEADERS } from '../constants.js';
20
20
  import debugFactory from '../debug.js';
21
+ import EthereumABI from '../protocols/ethereum/abi.js';
21
22
  import Protocol from '../protocols/index.js';
22
23
  import { abiEvents } from '../scaffold/schema.js';
23
24
  import Schema from '../schema.js';
@@ -154,6 +155,7 @@ export default class InitCommand extends Command {
154
155
  if ((fromContract || spkgPath) && protocol && subgraphName && directory && network && node) {
155
156
  const registry = await loadRegistry();
156
157
  const contractService = new ContractService(registry);
158
+ const sourcifyContractInfo = await contractService.getFromSourcify(EthereumABI, network, fromContract);
157
159
  if (!protocolChoices.includes(protocol)) {
158
160
  this.error(`Protocol '${protocol}' is not supported, choose from these options: ${protocolChoices.join(', ')}`, { exit: 1 });
159
161
  }
@@ -170,7 +172,9 @@ export default class InitCommand extends Command {
170
172
  }
171
173
  else {
172
174
  try {
173
- abi = await contractService.getABI(ABI, network, fromContract);
175
+ abi = sourcifyContractInfo
176
+ ? sourcifyContractInfo.abi
177
+ : await contractService.getABI(ABI, network, fromContract);
174
178
  }
175
179
  catch (e) {
176
180
  this.exit(1);
@@ -462,11 +466,21 @@ async function processInitForm({ abi: initAbi, abiPath: initAbiPath, directory:
462
466
  source = address;
463
467
  return address;
464
468
  }
469
+ const sourcifyContractInfo = await contractService.getFromSourcify(EthereumABI, network.id, address);
470
+ if (sourcifyContractInfo) {
471
+ initStartBlock ??= sourcifyContractInfo.startBlock;
472
+ initContractName ??= sourcifyContractInfo.name;
473
+ initAbi ??= sourcifyContractInfo.abi;
474
+ initDebugger.extend('processInitForm')("infoFromSourcify: '%s'/'%s'", initStartBlock, initContractName);
475
+ }
465
476
  // If ABI is not provided, try to fetch it from Etherscan API
466
477
  if (protocolInstance.hasABIs() && !initAbi) {
467
478
  abiFromApi = await retryWithPrompt(() => withSpinner('Fetching ABI from contract API...', 'Failed to fetch ABI', 'Warning fetching ABI', () => contractService.getABI(protocolInstance.getABI(), network.id, address)));
468
479
  initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name);
469
480
  }
481
+ else {
482
+ abiFromApi = initAbi;
483
+ }
470
484
  // If startBlock is not provided, try to fetch it from Etherscan API
471
485
  if (!initStartBlock) {
472
486
  startBlock = await retryWithPrompt(() => withSpinner('Fetching start block from contract API...', 'Failed to fetch start block', 'Warning fetching start block', () => contractService.getStartBlock(network.id, address)));
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import http from 'node:http';
3
3
  import net from 'node:net';
4
4
  import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
  import compose from 'docker-compose';
6
7
  import { filesystem, patching } from 'gluegun';
7
8
  import stripAnsi from 'strip-ansi';
@@ -67,7 +68,7 @@ export default class LocalCommand extends Command {
67
68
  const skipWaitForEthereum = skipWaitForEthereumTypo || skipWaitForEthereumGood;
68
69
  // Obtain the Docker Compose file for services that the tests run against
69
70
  const composeFile = composeFileFlag ||
70
- path.join(`${process.platform === 'win32' ? '' : '/'}${/file:\/{2,3}(.+)\/[^/]/.exec(import.meta.url)[1]}`, '..', '..', 'resources', 'test', standaloneNode ? 'docker-compose-standalone-node.yml' : 'docker-compose.yml');
71
+ path.join(fileURLToPath(import.meta.url), '..', '..', '..', 'resources', 'test', standaloneNode ? 'docker-compose-standalone-node.yml' : 'docker-compose.yml');
71
72
  if (!filesystem.exists(composeFile)) {
72
73
  this.error(`Docker Compose file "${composeFile}" not found`, { exit: 1 });
73
74
  }
package/dist/subgraph.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
2
3
  import fs from 'fs-extra';
3
4
  import * as graphql from 'graphql/language/index.js';
4
5
  import immutable from 'immutable';
@@ -31,7 +32,7 @@ export default class Subgraph {
31
32
  ]);
32
33
  }
33
34
  // Parse the default subgraph schema
34
- const schema = graphql.parse(await fs.readFile(path.join(`${process.platform === 'win32' ? '' : '/'}${/file:\/{2,3}(.+)\/[^/]/.exec(import.meta.url)[1]}`, 'protocols',
35
+ const schema = graphql.parse(await fs.readFile(path.join(fileURLToPath(import.meta.url), '..', 'protocols',
35
36
  // TODO: substreams/triggers is a special case, should be handled better
36
37
  protocol.name === 'substreams/triggers' ? 'substreams' : protocol.name, `manifest.graphql`), 'utf-8'));
37
38
  // Obtain the root `SubgraphManifest` type from the schema
package/dist/version.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
3
4
  const packageJson = JSON.parse(fs
4
5
  .readFileSync(
5
6
  // works even when bundled/built because the path to package.json is the same
6
- path.join(`${process.platform === 'win32' ? '' : '/'}${/file:\/{2,3}(.+)\/[^/]/.exec(import.meta.url)[1]}`, '..', 'package.json'))
7
+ path.join(fileURLToPath(import.meta.url), '..', '..', 'package.json'))
7
8
  .toString());
8
9
  export const version = packageJson.version;
9
10
  export const nodeVersion = (packageJson.engines?.node ?? '');
@@ -1011,5 +1011,5 @@
1011
1011
  ]
1012
1012
  }
1013
1013
  },
1014
- "version": "0.94.0"
1014
+ "version": "0.95.0-alpha-20250122003255-e449cf69b90cf3813ea73e4b363230c38dc9257f"
1015
1015
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphprotocol/graph-cli",
3
- "version": "0.94.0",
3
+ "version": "0.95.0-alpha-20250122003255-e449cf69b90cf3813ea73e4b363230c38dc9257f",
4
4
  "type": "module",
5
5
  "description": "CLI for building for and deploying to The Graph",
6
6
  "license": "(Apache-2.0 OR MIT)",
@@ -45,7 +45,7 @@
45
45
  "prettier": "3.4.2",
46
46
  "semver": "7.6.3",
47
47
  "tmp-promise": "3.0.3",
48
- "undici": "7.1.1",
48
+ "undici": "7.2.3",
49
49
  "web3-eth-abi": "4.4.1",
50
50
  "yaml": "2.6.1"
51
51
  },