@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 +15 -0
- package/dist/command-helpers/contracts.d.ts +5 -0
- package/dist/command-helpers/contracts.js +39 -0
- package/dist/command-helpers/contracts.test.js +66 -5
- package/dist/command-helpers/network.test.js +2 -1
- package/dist/commands/add.js +8 -1
- package/dist/commands/init.js +15 -1
- package/dist/commands/local.js +2 -1
- package/dist/subgraph.js +2 -1
- package/dist/version.js +2 -1
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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(
|
|
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');
|
package/dist/commands/add.js
CHANGED
|
@@ -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 (
|
|
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 {
|
package/dist/commands/init.js
CHANGED
|
@@ -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 =
|
|
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)));
|
package/dist/commands/local.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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 ?? '');
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@graphprotocol/graph-cli",
|
|
3
|
-
"version": "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.
|
|
48
|
+
"undici": "7.2.3",
|
|
49
49
|
"web3-eth-abi": "4.4.1",
|
|
50
50
|
"yaml": "2.6.1"
|
|
51
51
|
},
|