@graphprotocol/graph-cli 0.94.0-alpha-20250109043757-c53ef95047eaf4833353c30dc2d1f6cbb9fc13e4 → 0.94.0

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,22 +1,35 @@
1
1
  # @graphprotocol/graph-cli
2
2
 
3
- ## 0.94.0-alpha-20250109043757-c53ef95047eaf4833353c30dc2d1f6cbb9fc13e4
3
+ ## 0.94.0
4
4
 
5
5
  ### Minor Changes
6
6
 
7
- - [#1888](https://github.com/graphprotocol/graph-tooling/pull/1888)
8
- [`99d296f`](https://github.com/graphprotocol/graph-tooling/commit/99d296f67c1c49a6b894e737478f34920465948c)
9
- Thanks [@YaroShkvorets](https://github.com/YaroShkvorets)! - Add EIP-1967/OpenZeppelin proxy
10
- contracts support. When proxy contract is detected, user is given an option to use ABI of the
11
- implementation contract.
7
+ - [#1892](https://github.com/graphprotocol/graph-tooling/pull/1892)
8
+ [`b4564a7`](https://github.com/graphprotocol/graph-tooling/commit/b4564a71134c310dee983f6eff613b953bf481be)
9
+ Thanks [@YaroShkvorets](https://github.com/YaroShkvorets)! - handle tuple[] and address[] for
10
+ event parameters - #949
12
11
 
13
12
  ### Patch Changes
14
13
 
14
+ - [#1891](https://github.com/graphprotocol/graph-tooling/pull/1891)
15
+ [`d1fa5e0`](https://github.com/graphprotocol/graph-tooling/commit/d1fa5e0e34a92f24db29d3def26e133e87e7bdde)
16
+ Thanks [@YaroShkvorets](https://github.com/YaroShkvorets)! - fix bug with duplicate IDs - #1604
17
+
18
+ - [#1889](https://github.com/graphprotocol/graph-tooling/pull/1889)
19
+ [`e085e39`](https://github.com/graphprotocol/graph-tooling/commit/e085e39e48cd0241277ede246aa2b7833e6a91f3)
20
+ Thanks [@YaroShkvorets](https://github.com/YaroShkvorets)! - fix confusing "no error" message when
21
+ pressing Escape on prompts
22
+
15
23
  - [#1879](https://github.com/graphprotocol/graph-tooling/pull/1879)
16
24
  [`f6079a6`](https://github.com/graphprotocol/graph-tooling/commit/f6079a68c016e8d9864f2fc94526a60d4b21eb69)
17
25
  Thanks [@McAronDev](https://github.com/McAronDev)! - fix bug with missing Graft type in SpS
18
26
  manifest definition
19
27
 
28
+ - [#1884](https://github.com/graphprotocol/graph-tooling/pull/1884)
29
+ [`93d87a8`](https://github.com/graphprotocol/graph-tooling/commit/93d87a8c6912b87ed8dbdce0e1b4d2765bc1df39)
30
+ Thanks [@YaroShkvorets](https://github.com/YaroShkvorets)! - `graph init`: fix tests generation
31
+ for events with [][] params #1878
32
+
20
33
  ## 0.93.3
21
34
 
22
35
  ### Patch Changes
@@ -11,4 +11,7 @@ export declare const unrollTuple: ({ path, value, }: {
11
11
  path: string[];
12
12
  index: number;
13
13
  value: any;
14
- }) => any;
14
+ }) => {
15
+ path: string[];
16
+ type: string;
17
+ }[];
@@ -10,6 +10,5 @@ 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
- getProxyImplementation(networkId: string, address: string): Promise<string>;
14
13
  private fetchTransactionByHash;
15
14
  }
@@ -122,48 +122,6 @@ export class ContractService {
122
122
  }
123
123
  throw new Error(`Failed to fetch contract name for ${address}`);
124
124
  }
125
- async getProxyImplementation(networkId, address) {
126
- const urls = this.getRpcUrls(networkId);
127
- if (!urls.length) {
128
- throw new Error(`No JSON-RPC available for ${networkId} in the registry`);
129
- }
130
- const EIP_1967_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc';
131
- const OPEN_ZEPPELIN_SLOT = '0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3';
132
- const getStorageAt = async (url, slot) => {
133
- const response = await fetch(url, {
134
- method: 'POST',
135
- headers: { 'Content-Type': 'application/json' },
136
- body: JSON.stringify({
137
- jsonrpc: '2.0',
138
- method: 'eth_getStorageAt',
139
- params: [address, slot, 'latest'],
140
- id: 1,
141
- }),
142
- });
143
- const json = await response.json();
144
- if (json?.result) {
145
- const impl = '0x' + json.result.slice(-40);
146
- if (impl !== '0x0000000000000000000000000000000000000000') {
147
- return impl;
148
- }
149
- }
150
- return null;
151
- };
152
- for (const url of urls) {
153
- for (const slot of [EIP_1967_SLOT, OPEN_ZEPPELIN_SLOT]) {
154
- try {
155
- const impl = await getStorageAt(url, slot);
156
- if (impl) {
157
- return impl;
158
- }
159
- }
160
- catch (error) {
161
- logger(`Failed to fetch proxy implementation from ${url}: ${error}`);
162
- }
163
- }
164
- }
165
- throw new Error(`No implementation address found`);
166
- }
167
125
  async fetchTransactionByHash(networkId, txHash) {
168
126
  const urls = this.getRpcUrls(networkId);
169
127
  if (!urls.length) {
@@ -191,6 +149,6 @@ export class ContractService {
191
149
  logger(`Failed to fetch tx ${txHash} from ${url}: ${error}`);
192
150
  }
193
151
  }
194
- throw new Error(`Failed to fetch tx ${txHash}`);
152
+ throw new Error(`JSON-RPC is unreachable`);
195
153
  }
196
154
  }
@@ -4,7 +4,6 @@ import { Args, Command, Errors, Flags } from '@oclif/core';
4
4
  import { ContractService } from '../command-helpers/contracts.js';
5
5
  import * as DataSourcesExtractor from '../command-helpers/data-sources.js';
6
6
  import { updateNetworksFile } from '../command-helpers/network.js';
7
- import { checkForProxy } from '../command-helpers/proxy.js';
8
7
  import { loadRegistry } from '../command-helpers/registry.js';
9
8
  import { retryWithPrompt } from '../command-helpers/retry.js';
10
9
  import { generateDataSource, writeABI, writeMapping, writeSchema, writeTestsFiles, } from '../command-helpers/scaffold.js';
@@ -62,7 +61,6 @@ export default class AddCommand extends Command {
62
61
  let startBlock = startBlockFlag ? parseInt(startBlockFlag).toString() : startBlockFlag;
63
62
  let contractName = contractNameFlag || DEFAULT_CONTRACT_NAME;
64
63
  let ethabi = null;
65
- let implAddress = null;
66
64
  if (abi) {
67
65
  ethabi = EthereumABI.load(contractName, abi);
68
66
  }
@@ -73,18 +71,12 @@ export default class AddCommand extends Command {
73
71
  ethabi = await retryWithPrompt(() => withSpinner('Fetching ABI from contract API...', 'Failed to fetch ABI', 'Warning fetching ABI', () => contractService.getABI(EthereumABI, network, address)));
74
72
  if (!ethabi)
75
73
  throw Error;
76
- const { implementationAbi, implementationAddress } = await checkForProxy(contractService, network, address, ethabi);
77
- if (implementationAddress) {
78
- implAddress = implementationAddress;
79
- ethabi = implementationAbi;
80
- }
81
- if (!ethabi)
82
- throw Error;
83
74
  }
84
75
  catch (error) {
85
76
  // we cannot ask user to do prompt in test environment
86
77
  if (process.env.NODE_ENV !== 'test') {
87
- const { abi: abiFile } = await prompt.ask([
78
+ const { abi: abiFile } = await prompt
79
+ .ask([
88
80
  {
89
81
  type: 'input',
90
82
  name: 'abi',
@@ -99,24 +91,23 @@ export default class AddCommand extends Command {
99
91
  }
100
92
  },
101
93
  },
102
- ]);
94
+ ])
95
+ .catch(() => this.exit(1)); // properly handle ESC
103
96
  ethabi = EthereumABI.load(contractName, abiFile);
104
97
  }
105
98
  }
106
99
  }
107
- if (!ethabi) {
108
- this.error('Failed to load ABI', { exit: 1 });
109
- }
110
100
  try {
111
101
  if (isLocalHost)
112
102
  throw Error; // Triggers user prompting without waiting for Etherscan lookup to fail
113
- startBlock ||= Number(await contractService.getStartBlock(network, implAddress ?? address)).toString();
103
+ startBlock ||= Number(await contractService.getStartBlock(network, address)).toString();
114
104
  }
115
105
  catch (error) {
116
106
  // we cannot ask user to do prompt in test environment
117
107
  if (process.env.NODE_ENV !== 'test') {
118
108
  // If we can't get the start block, we'll just leave it out of the manifest
119
- const { startBlock: userInputStartBlock } = await prompt.ask([
109
+ const { startBlock: userInputStartBlock } = await prompt
110
+ .ask([
120
111
  {
121
112
  type: 'input',
122
113
  name: 'startBlock',
@@ -127,7 +118,8 @@ export default class AddCommand extends Command {
127
118
  return value;
128
119
  },
129
120
  },
130
- ]);
121
+ ])
122
+ .catch(() => this.exit(1));
131
123
  startBlock = userInputStartBlock;
132
124
  }
133
125
  }
@@ -136,14 +128,14 @@ export default class AddCommand extends Command {
136
128
  throw Error; // Triggers user prompting without waiting for Etherscan lookup to fail
137
129
  if (contractName === DEFAULT_CONTRACT_NAME) {
138
130
  contractName =
139
- (await contractService.getContractName(network, implAddress ?? address)) ??
140
- DEFAULT_CONTRACT_NAME;
131
+ (await contractService.getContractName(network, address)) ?? DEFAULT_CONTRACT_NAME;
141
132
  }
142
133
  }
143
134
  catch (error) {
144
135
  // not asking user to do prompt in test environment
145
136
  if (process.env.NODE_ENV !== 'test') {
146
- const { contractName: userInputContractName } = await prompt.ask([
137
+ const { contractName: userInputContractName } = await prompt
138
+ .ask([
147
139
  {
148
140
  type: 'input',
149
141
  name: 'contractName',
@@ -154,7 +146,8 @@ export default class AddCommand extends Command {
154
146
  return value;
155
147
  },
156
148
  },
157
- ]);
149
+ ])
150
+ .catch(() => this.exit(1));
158
151
  contractName = userInputContractName;
159
152
  }
160
153
  }
@@ -196,6 +189,7 @@ export default class AddCommand extends Command {
196
189
  });
197
190
  }
198
191
  await withSpinner('Running codegen', 'Failed to run codegen', 'Warning during codegen', async () => await system.run(yarn ? 'yarn codegen' : 'npm run codegen'));
192
+ this.exit(0);
199
193
  }
200
194
  }
201
195
  const getEntities = (manifest) => {
@@ -20,7 +20,8 @@ export default class AuthCommand extends Command {
20
20
  async run() {
21
21
  const { args: { 'deploy-key': initialDeployKey }, } = await this.parse(AuthCommand);
22
22
  const { node } = chooseNodeUrl({});
23
- const { deployKey } = await prompt.ask([
23
+ const { deployKey } = await prompt
24
+ .ask([
24
25
  {
25
26
  type: 'input',
26
27
  name: 'deployKey',
@@ -30,7 +31,8 @@ export default class AuthCommand extends Command {
30
31
  skip: this.validateStudioDeployKey(initialDeployKey),
31
32
  validate: value => this.validateStudioDeployKey(value) || `Invalid Subgraph Studio deploy key: ${value}`,
32
33
  },
33
- ]);
34
+ ])
35
+ .catch(() => this.exit(1));
34
36
  try {
35
37
  await saveDeployKey(node, deployKey);
36
38
  print.success(`Deploy key set for ${node}`);
@@ -87,7 +87,8 @@ export default class DeployCommand extends Command {
87
87
  };
88
88
  async run() {
89
89
  const { args: { 'subgraph-name': subgraphNameArg, 'subgraph-manifest': manifest }, flags: { 'deploy-key': deployKeyFlag, 'access-token': accessToken, 'version-label': versionLabelFlag, ipfs, headers, node: nodeFlag, 'output-dir': outputDir, 'skip-migrations': skipMigrations, watch, 'debug-fork': debugFork, network, 'network-file': networkFile, 'ipfs-hash': ipfsHash, }, } = await this.parse(DeployCommand);
90
- const { subgraphName } = await prompt.ask([
90
+ const { subgraphName } = await prompt
91
+ .ask([
91
92
  {
92
93
  type: 'input',
93
94
  name: 'subgraphName',
@@ -96,7 +97,8 @@ export default class DeployCommand extends Command {
96
97
  initial: subgraphNameArg,
97
98
  required: true,
98
99
  },
99
- ]);
100
+ ])
101
+ .catch(() => this.exit(1));
100
102
  const { node } = chooseNodeUrl({
101
103
  node: nodeFlag,
102
104
  });
@@ -124,7 +126,8 @@ export default class DeployCommand extends Command {
124
126
  };
125
127
  }
126
128
  // Ask for label if not on hosted service
127
- const { versionLabel } = await prompt.ask([
129
+ const { versionLabel } = await prompt
130
+ .ask([
128
131
  {
129
132
  type: 'input',
130
133
  name: 'versionLabel',
@@ -133,7 +136,8 @@ export default class DeployCommand extends Command {
133
136
  skip: () => !!versionLabelFlag,
134
137
  required: true,
135
138
  },
136
- ]);
139
+ ])
140
+ .catch(() => this.exit(1));
137
141
  const deploySubgraph = async (ipfsHash) => {
138
142
  const spinner = print.spin(`Deploying to Graph node ${requestUrl}`);
139
143
  client.request('subgraph_deploy', {
@@ -10,7 +10,6 @@ import { DEFAULT_IPFS_URL } from '../command-helpers/ipfs.js';
10
10
  import { initNetworksConfig } from '../command-helpers/network.js';
11
11
  import { chooseNodeUrl } from '../command-helpers/node.js';
12
12
  import { PromptManager } from '../command-helpers/prompt-manager.js';
13
- import { checkForProxy } from '../command-helpers/proxy.js';
14
13
  import { loadRegistry } from '../command-helpers/registry.js';
15
14
  import { retryWithPrompt } from '../command-helpers/retry.js';
16
15
  import { generateScaffold, writeScaffold } from '../command-helpers/scaffold.js';
@@ -464,27 +463,18 @@ async function processInitForm({ abi: initAbi, abiPath: initAbiPath, directory:
464
463
  return address;
465
464
  }
466
465
  // If ABI is not provided, try to fetch it from Etherscan API
467
- let implAddress = undefined;
468
466
  if (protocolInstance.hasABIs() && !initAbi) {
469
467
  abiFromApi = await retryWithPrompt(() => withSpinner('Fetching ABI from contract API...', 'Failed to fetch ABI', 'Warning fetching ABI', () => contractService.getABI(protocolInstance.getABI(), network.id, address)));
470
- initDebugger.extend('processInitForm')("ABI: '%s'", abiFromApi?.name);
471
- if (abiFromApi) {
472
- const { implementationAbi, implementationAddress } = await checkForProxy(contractService, network.id, address, abiFromApi);
473
- if (implementationAddress) {
474
- implAddress = implementationAddress;
475
- abiFromApi = implementationAbi;
476
- initDebugger.extend('processInitForm')("Impl ABI: '%s', Impl Address: '%s'", abiFromApi?.name, implAddress);
477
- }
478
- }
468
+ initDebugger.extend('processInitForm')("abiFromEtherscan len: '%s'", abiFromApi?.name);
479
469
  }
480
470
  // If startBlock is not provided, try to fetch it from Etherscan API
481
471
  if (!initStartBlock) {
482
- startBlock = await retryWithPrompt(() => withSpinner('Fetching start block from contract API...', 'Failed to fetch start block', 'Warning fetching start block', () => contractService.getStartBlock(network.id, implAddress ?? address)));
472
+ startBlock = await retryWithPrompt(() => withSpinner('Fetching start block from contract API...', 'Failed to fetch start block', 'Warning fetching start block', () => contractService.getStartBlock(network.id, address)));
483
473
  initDebugger.extend('processInitForm')("startBlockFromEtherscan: '%s'", startBlock);
484
474
  }
485
475
  // If contract name is not provided, try to fetch it from Etherscan API
486
476
  if (!initContractName) {
487
- contractName = await retryWithPrompt(() => withSpinner('Fetching contract name from contract API...', 'Failed to fetch contract name', 'Warning fetching contract name', () => contractService.getContractName(network.id, implAddress ?? address)));
477
+ contractName = await retryWithPrompt(() => withSpinner('Fetching contract name from contract API...', 'Failed to fetch contract name', 'Warning fetching contract name', () => contractService.getContractName(network.id, address)));
488
478
  initDebugger.extend('processInitForm')("contractNameFromEtherscan: '%s'", contractName);
489
479
  }
490
480
  source = address;
@@ -720,9 +710,10 @@ Subgraph ${subgraphName} created in ${relativeDir}
720
710
  Make sure to visit the documentation on https://thegraph.com/docs/ for further information.`);
721
711
  }
722
712
  async function initSubgraphFromExample({ fromExample, subgraphName, directory, skipInstall, skipGit, }, { commands, }) {
723
- let overwrite = false;
724
713
  if (filesystem.exists(directory)) {
725
- overwrite = await prompt.confirm('Directory already exists, do you want to initialize the subgraph here (files will be overwritten) ?', false);
714
+ const overwrite = await prompt
715
+ .confirm('Directory already exists, do you want to initialize the subgraph here (files will be overwritten) ?', false)
716
+ .catch(() => false);
726
717
  if (!overwrite) {
727
718
  this.exit(1);
728
719
  }
@@ -746,7 +737,7 @@ async function initSubgraphFromExample({ fromExample, subgraphName, directory, s
746
737
  if (!filesystem.exists(exampleSubgraphPath)) {
747
738
  return { result: false, error: `Example not found: ${fromExample}` };
748
739
  }
749
- filesystem.copy(exampleSubgraphPath, directory, { overwrite });
740
+ filesystem.copy(exampleSubgraphPath, directory, { overwrite: true });
750
741
  return true;
751
742
  }
752
743
  finally {
@@ -811,9 +802,13 @@ async function initSubgraphFromExample({ fromExample, subgraphName, directory, s
811
802
  }
812
803
  async function initSubgraphFromContract({ protocolInstance, subgraphName, directory, abi, network, source, indexEvents, contractName, node, startBlock, spkgPath, skipInstall, skipGit, ipfsUrl, }, { commands, addContract, }) {
813
804
  const isComposedSubgraph = protocolInstance.isComposedSubgraph();
814
- if (filesystem.exists(directory) &&
815
- !(await prompt.confirm('Directory already exists, do you want to initialize the subgraph here (files will be overwritten) ?', false))) {
816
- this.exit(1);
805
+ if (filesystem.exists(directory)) {
806
+ const overwrite = await prompt
807
+ .confirm('Directory already exists, do you want to initialize the subgraph here (files will be overwritten) ?', false)
808
+ .catch(() => false);
809
+ if (!overwrite) {
810
+ this.exit(1);
811
+ }
817
812
  }
818
813
  let entities;
819
814
  if (isComposedSubgraph) {
@@ -890,10 +885,12 @@ async function initSubgraphFromContract({ protocolInstance, subgraphName, direct
890
885
  this.exit(1);
891
886
  }
892
887
  while (addContract) {
893
- addContract = await addAnotherContract.bind(this)({
888
+ addContract = await addAnotherContract
889
+ .bind(this)({
894
890
  protocolInstance,
895
891
  directory,
896
- });
892
+ })
893
+ .catch(() => false);
897
894
  }
898
895
  }
899
896
  printNextSteps.bind(this)({ subgraphName, directory }, { commands });
@@ -917,7 +914,7 @@ async function addAnotherContract({ protocolInstance, directory, }) {
917
914
  name: 'contract',
918
915
  initial: ProtocolContract.identifierName(),
919
916
  required: true,
920
- message: () => `Contract ${ProtocolContract.identifierName()}`,
917
+ message: () => `\nContract ${ProtocolContract.identifierName()}`,
921
918
  validate: value => {
922
919
  const { valid, error } = validateContract(value, ProtocolContract);
923
920
  return valid ? true : error;
@@ -51,7 +51,8 @@ export default class PublishCommand extends Command {
51
51
  * Prompt the user to open up the browser to continue publishing the subgraph
52
52
  */
53
53
  async publishWithBrowser({ ipfsHash, webapp, subgraphId, protocolNetwork, apiKey, }) {
54
- const { openBrowser } = await prompt.ask([
54
+ const { openBrowser } = await prompt
55
+ .ask([
55
56
  {
56
57
  type: 'confirm',
57
58
  name: 'openBrowser',
@@ -59,7 +60,8 @@ export default class PublishCommand extends Command {
59
60
  initial: true,
60
61
  required: true,
61
62
  },
62
- ]);
63
+ ])
64
+ .catch(() => this.exit(1));
63
65
  if (!openBrowser) {
64
66
  this.exit(0);
65
67
  }
@@ -220,7 +220,7 @@ export default class AbiCodeGenerator {
220
220
  name = parentType === 'event' ? `param${index}` : `value${index}`;
221
221
  }
222
222
  const tupleIdentifier = parentClass + tsCodegen.namedType(name).capitalize();
223
- const tupleClassName = tupleIdentifier + 'Struct';
223
+ const tupleClassName = tupleIdentifier + (parentField === 'outputValues' ? 'Output' : '') + 'Struct';
224
224
  let tupleClasses = [];
225
225
  const isTupleType = util.isTupleType(type);
226
226
  const returnValue = typesCodegen.ethereumToAsc(parentType === 'tuple'
@@ -27,7 +27,7 @@ export const generatePlaceholderHandlers = ({ abi, events, contractName, }) => `
27
27
  entity.count = entity.count + BigInt.fromI32(1)
28
28
 
29
29
  // Entity fields can be set based on event parameters
30
- ${generateEventFieldAssignments(event, contractName).slice(0, 2).join('\n')}
30
+ ${generateEventFieldAssignments(event, contractName).assignments.slice(0, 2).join('\n')}
31
31
 
32
32
  // Entities can be written to the store with \`.save()\`
33
33
  entity.save()
@@ -51,7 +51,28 @@ const TEST_CALLABLE_FUNCTIONS = [
51
51
  outputs: [{ type: 'tuple', components: [{ type: 'uint256' }] }],
52
52
  },
53
53
  ];
54
- const TEST_ABI = new ABI('Contract', undefined, immutable.fromJS([TEST_EVENT, OVERLOADED_EVENT, TEST_CONTRACT, ...TEST_CALLABLE_FUNCTIONS]));
54
+ const TEST_TUPLE_ARRAY_EVENT = {
55
+ name: 'TupleArrayEvent',
56
+ type: 'event',
57
+ inputs: [
58
+ {
59
+ name: 'tupleArray',
60
+ type: 'tuple[]',
61
+ components: [
62
+ { name: 'field1', type: 'uint256' },
63
+ { name: 'field2', type: 'address' },
64
+ ],
65
+ },
66
+ { name: 'addressArray', type: 'address[]' },
67
+ ],
68
+ };
69
+ const TEST_ABI = new ABI('Contract', undefined, immutable.fromJS([
70
+ TEST_EVENT,
71
+ OVERLOADED_EVENT,
72
+ TEST_TUPLE_ARRAY_EVENT,
73
+ TEST_CONTRACT,
74
+ ...TEST_CALLABLE_FUNCTIONS,
75
+ ]));
55
76
  const protocol = new Protocol('ethereum');
56
77
  const scaffoldOptions = {
57
78
  protocol,
@@ -82,6 +103,9 @@ describe('Ethereum subgraph scaffolding', () => {
82
103
  test('Mapping (for indexing events)', async () => {
83
104
  expect(await scaffoldWithIndexEvents.generateMapping()).toMatchSnapshot();
84
105
  });
106
+ test('Mapping handles tuple array type conversion', async () => {
107
+ expect(await scaffold.generateMapping()).toMatchSnapshot();
108
+ });
85
109
  test('Test Files (default)', async () => {
86
110
  const files = await scaffoldWithIndexEvents.generateTests();
87
111
  const testFile = files?.['contract.test.ts'];
@@ -1,8 +1,21 @@
1
- export declare const generateFieldAssignment: (key: string[], value: string[]) => string;
2
- export declare const generateFieldAssignments: ({ index, input }: {
1
+ /**
2
+ * Map of value types that need to be changetype'd to their corresponding AssemblyScript type
3
+ */
4
+ export declare const VALUE_TYPECAST_MAP: Record<string, string>;
5
+ export declare const generateFieldAssignment: (key: string[], value: string[], type: string) => {
6
+ assignment: string;
7
+ imports: string[];
8
+ };
9
+ export declare const generateFieldAssignments: ({ index, input, }: {
3
10
  index: number;
4
11
  input: any;
5
- }) => any;
12
+ }) => {
13
+ assignments: string[];
14
+ imports: string[];
15
+ };
6
16
  export declare const renameNameIfNeeded: (name: string) => string;
7
- export declare const generateEventFieldAssignments: (event: any, _contractName: string) => any;
17
+ export declare const generateEventFieldAssignments: (event: any, _contractName: string) => {
18
+ assignments: string[];
19
+ imports: string[];
20
+ };
8
21
  export declare const generateEventIndexingHandlers: (events: any[], contractName: string) => string;
@@ -1,10 +1,37 @@
1
1
  import * as util from '../codegen/util.js';
2
- export const generateFieldAssignment = (key, value) => `entity.${key.join('_')} = event.params.${value.join('.')}`;
3
- export const generateFieldAssignments = ({ index, input }) => input.type === 'tuple'
4
- ? util
5
- .unrollTuple({ value: input, index, path: [input.name || `param${index}`] })
6
- .map(({ path }) => generateFieldAssignment(path, path))
7
- : generateFieldAssignment([(input.mappedName ?? input.name) || `param${index}`], [input.name || `param${index}`]);
2
+ /**
3
+ * Map of value types that need to be changetype'd to their corresponding AssemblyScript type
4
+ */
5
+ export const VALUE_TYPECAST_MAP = {
6
+ 'address[]': 'Bytes[]',
7
+ 'tuple[]': 'Bytes[]',
8
+ };
9
+ export const generateFieldAssignment = (key, value, type) => {
10
+ let rightSide = `event.params.${value.join('.')}`;
11
+ const imports = [];
12
+ if (type in VALUE_TYPECAST_MAP) {
13
+ const castTo = VALUE_TYPECAST_MAP[type];
14
+ rightSide = `changetype<${castTo}>(${rightSide})`;
15
+ imports.push(castTo.replace('[]', ''));
16
+ }
17
+ return {
18
+ assignment: `entity.${key.join('_')} = ${rightSide}`,
19
+ imports,
20
+ };
21
+ };
22
+ export const generateFieldAssignments = ({ index, input, }) => {
23
+ const fields = input.type === 'tuple'
24
+ ? util
25
+ .unrollTuple({ value: input, index, path: [input.name || `param${index}`] })
26
+ .map(({ path, type }) => generateFieldAssignment(path, path, type))
27
+ : [
28
+ generateFieldAssignment([(input.mappedName ?? input.name) || `param${index}`], [input.name || `param${index}`], input.type),
29
+ ];
30
+ return {
31
+ assignments: fields.map(a => a.assignment),
32
+ imports: fields.map(a => a.imports).flat(),
33
+ };
34
+ };
8
35
  /**
9
36
  * Map of input names that are reserved so we do not use them as field names to avoid conflicts
10
37
  * see https://github.com/graphprotocol/graph-tooling/issues/710
@@ -19,17 +46,23 @@ export const renameNameIfNeeded = (name) => {
19
46
  };
20
47
  export const generateEventFieldAssignments = (event, _contractName) => event.inputs.reduce((acc, input, index) => {
21
48
  input.mappedName = renameNameIfNeeded(input.name);
22
- return acc.concat(generateFieldAssignments({ input, index }));
23
- }, []);
24
- export const generateEventIndexingHandlers = (events, contractName) => `
25
- import { ${events.map(event => `${event._alias} as ${event._alias}Event`)}} from '../generated/${contractName}/${contractName}'
26
- import { ${events.map(event => event._alias)} } from '../generated/schema'
27
-
28
- ${events
29
- .map(event => `
49
+ const { assignments, imports } = generateFieldAssignments({ input, index });
50
+ return {
51
+ assignments: acc.assignments.concat(assignments),
52
+ imports: acc.imports.concat(imports),
53
+ };
54
+ }, { assignments: [], imports: [] });
55
+ export const generateEventIndexingHandlers = (events, contractName) => {
56
+ const eventFieldAssignments = events.map(event => ({
57
+ event,
58
+ ...generateEventFieldAssignments(event, contractName),
59
+ }));
60
+ const allImports = [...new Set(eventFieldAssignments.map(({ imports }) => imports).flat())];
61
+ const eventHandlers = eventFieldAssignments.map(({ event, assignments }) => {
62
+ return `
30
63
  export function handle${event._alias}(event: ${event._alias}Event): void {
31
64
  let entity = new ${event._alias}(event.transaction.hash.concatI32(event.logIndex.toI32()))
32
- ${generateEventFieldAssignments(event, contractName).join('\n')}
65
+ ${assignments.join('\n')}
33
66
 
34
67
  entity.blockNumber = event.block.number
35
68
  entity.blockTimestamp = event.block.timestamp
@@ -37,6 +70,15 @@ export const generateEventIndexingHandlers = (events, contractName) => `
37
70
 
38
71
  entity.save()
39
72
  }
40
- `)
41
- .join('\n')}
73
+ `;
74
+ });
75
+ return `
76
+ import { ${events.map(event => `${event._alias} as ${event._alias}Event`)}} from '../generated/${contractName}/${contractName}'
77
+ import { ${events.map(event => event._alias)} } from '../generated/schema'
78
+ ${allImports.length > 0
79
+ ? `import { ${allImports.join(', ')} } from '@graphprotocol/graph-ts'`
80
+ : ''}
81
+
82
+ ${eventHandlers.join('\n')}
42
83
  `;
84
+ };
@@ -13,6 +13,6 @@ export declare const generateEventFields: ({ index, input, protocolName, }: {
13
13
  index: number;
14
14
  input: any;
15
15
  protocolName: string;
16
- }) => any;
16
+ }) => string[];
17
17
  export declare const generateEventType: (event: any, protocolName: string, contractName: string | undefined) => string;
18
18
  export declare const generateExampleEntityType: (protocol: Protocol, events: any[]) => string;
@@ -97,7 +97,13 @@ const isNativeType = (type) => {
97
97
  const natives = [/i32/, /string/, /boolean/];
98
98
  return natives.some(rx => rx.test(type));
99
99
  };
100
- const fetchArrayInnerType = (type) => type.match(/Array<(.*?)>/);
100
+ // get inner type: Array<T> -> T, Array<Array<T>> -> T
101
+ const fetchArrayInnerType = (type) => {
102
+ const match = type.match(/Array<(.+)>/);
103
+ if (!match)
104
+ return null;
105
+ return fetchArrayInnerType(match[1]) || match;
106
+ };
101
107
  // Generates the example test.ts file
102
108
  const generateExampleTest = (contract, event, indexEvents, importTypes) => {
103
109
  const entity = indexEvents ? String(event._alias) : 'ExampleEntity';
@@ -1 +1 @@
1
- {"root":["../src/constants.ts","../src/debug.ts","../src/fetch.ts","../src/index.ts","../src/migrations.ts","../src/schema.ts","../src/subgraph.ts","../src/type-generator.ts","../src/utils.ts","../src/version.ts","../src/watcher.ts","../src/codegen/schema.test.ts","../src/codegen/schema.ts","../src/codegen/template.ts","../src/codegen/typescript.ts","../src/codegen/util.test.ts","../src/codegen/util.ts","../src/codegen/types/conversions.ts","../src/codegen/types/index.test.ts","../src/codegen/types/index.ts","../src/command-helpers/auth.ts","../src/command-helpers/compiler.test.ts","../src/command-helpers/compiler.ts","../src/command-helpers/contracts.test.ts","../src/command-helpers/contracts.ts","../src/command-helpers/data-sources.ts","../src/command-helpers/file-resolver.test.ts","../src/command-helpers/file-resolver.ts","../src/command-helpers/fs.ts","../src/command-helpers/ipfs.ts","../src/command-helpers/jsonrpc.ts","../src/command-helpers/network.test.ts","../src/command-helpers/network.ts","../src/command-helpers/node.ts","../src/command-helpers/prompt-manager.ts","../src/command-helpers/proxy.test.ts","../src/command-helpers/proxy.ts","../src/command-helpers/registry.ts","../src/command-helpers/retry.ts","../src/command-helpers/scaffold.ts","../src/command-helpers/sort.test.ts","../src/command-helpers/sort.ts","../src/command-helpers/spinner.ts","../src/command-helpers/subgraph.ts","../src/command-helpers/version.test.ts","../src/command-helpers/version.ts","../src/commands/add.ts","../src/commands/auth.ts","../src/commands/binary-install.d.ts","../src/commands/build.ts","../src/commands/clean.ts","../src/commands/codegen.ts","../src/commands/create.ts","../src/commands/deploy.ts","../src/commands/init.ts","../src/commands/local.ts","../src/commands/publish.ts","../src/commands/remove.ts","../src/commands/test.ts","../src/compiler/asc.ts","../src/compiler/index.ts","../src/migrations/mapping_api_version_0_0_1.ts","../src/migrations/mapping_api_version_0_0_2.ts","../src/migrations/mapping_api_version_0_0_3.ts","../src/migrations/mapping_api_version_0_0_4.ts","../src/migrations/mapping_api_version_0_0_5.ts","../src/migrations/spec_version_0_0_2.ts","../src/migrations/spec_version_0_0_3.ts","../src/migrations/util/load-manifest.ts","../src/migrations/util/versions.ts","../src/protocols/contract.ts","../src/protocols/file_template.ts","../src/protocols/index.ts","../src/protocols/subgraph.ts","../src/protocols/arweave/subgraph.ts","../src/protocols/arweave/scaffold/manifest.ts","../src/protocols/arweave/scaffold/mapping.ts","../src/protocols/cosmos/subgraph.ts","../src/protocols/cosmos/scaffold/manifest.ts","../src/protocols/cosmos/scaffold/mapping.ts","../src/protocols/ethereum/abi.ts","../src/protocols/ethereum/contract.ts","../src/protocols/ethereum/subgraph.ts","../src/protocols/ethereum/type-generator.ts","../src/protocols/ethereum/codegen/abi.test.ts","../src/protocols/ethereum/codegen/abi.ts","../src/protocols/ethereum/codegen/template.ts","../src/protocols/ethereum/scaffold/manifest.ts","../src/protocols/ethereum/scaffold/mapping.ts","../src/protocols/near/contract.ts","../src/protocols/near/subgraph.ts","../src/protocols/near/scaffold/manifest.ts","../src/protocols/near/scaffold/mapping.ts","../src/protocols/subgraph/subgraph.ts","../src/protocols/subgraph/scaffold/manifest.ts","../src/protocols/subgraph/scaffold/mapping.ts","../src/protocols/substreams/subgraph.ts","../src/protocols/substreams/scaffold/manifest.ts","../src/scaffold/cosmos.test.ts","../src/scaffold/ethereum.test.ts","../src/scaffold/get-docker-file.ts","../src/scaffold/get-git-ignore.ts","../src/scaffold/index.ts","../src/scaffold/mapping.ts","../src/scaffold/near.test.ts","../src/scaffold/schema.ts","../src/scaffold/tests.ts","../src/validation/contract.ts","../src/validation/index.ts","../src/validation/manifest.ts","../src/validation/schema.test.ts","../src/validation/schema.ts"],"version":"5.7.2"}
1
+ {"root":["../src/constants.ts","../src/debug.ts","../src/fetch.ts","../src/index.ts","../src/migrations.ts","../src/schema.ts","../src/subgraph.ts","../src/type-generator.ts","../src/utils.ts","../src/version.ts","../src/watcher.ts","../src/codegen/schema.test.ts","../src/codegen/schema.ts","../src/codegen/template.ts","../src/codegen/typescript.ts","../src/codegen/util.test.ts","../src/codegen/util.ts","../src/codegen/types/conversions.ts","../src/codegen/types/index.test.ts","../src/codegen/types/index.ts","../src/command-helpers/auth.ts","../src/command-helpers/compiler.test.ts","../src/command-helpers/compiler.ts","../src/command-helpers/contracts.test.ts","../src/command-helpers/contracts.ts","../src/command-helpers/data-sources.ts","../src/command-helpers/file-resolver.test.ts","../src/command-helpers/file-resolver.ts","../src/command-helpers/fs.ts","../src/command-helpers/ipfs.ts","../src/command-helpers/jsonrpc.ts","../src/command-helpers/network.test.ts","../src/command-helpers/network.ts","../src/command-helpers/node.ts","../src/command-helpers/prompt-manager.ts","../src/command-helpers/registry.ts","../src/command-helpers/retry.ts","../src/command-helpers/scaffold.ts","../src/command-helpers/sort.test.ts","../src/command-helpers/sort.ts","../src/command-helpers/spinner.ts","../src/command-helpers/subgraph.ts","../src/command-helpers/version.test.ts","../src/command-helpers/version.ts","../src/commands/add.ts","../src/commands/auth.ts","../src/commands/binary-install.d.ts","../src/commands/build.ts","../src/commands/clean.ts","../src/commands/codegen.ts","../src/commands/create.ts","../src/commands/deploy.ts","../src/commands/init.ts","../src/commands/local.ts","../src/commands/publish.ts","../src/commands/remove.ts","../src/commands/test.ts","../src/compiler/asc.ts","../src/compiler/index.ts","../src/migrations/mapping_api_version_0_0_1.ts","../src/migrations/mapping_api_version_0_0_2.ts","../src/migrations/mapping_api_version_0_0_3.ts","../src/migrations/mapping_api_version_0_0_4.ts","../src/migrations/mapping_api_version_0_0_5.ts","../src/migrations/spec_version_0_0_2.ts","../src/migrations/spec_version_0_0_3.ts","../src/migrations/util/load-manifest.ts","../src/migrations/util/versions.ts","../src/protocols/contract.ts","../src/protocols/file_template.ts","../src/protocols/index.ts","../src/protocols/subgraph.ts","../src/protocols/arweave/subgraph.ts","../src/protocols/arweave/scaffold/manifest.ts","../src/protocols/arweave/scaffold/mapping.ts","../src/protocols/cosmos/subgraph.ts","../src/protocols/cosmos/scaffold/manifest.ts","../src/protocols/cosmos/scaffold/mapping.ts","../src/protocols/ethereum/abi.ts","../src/protocols/ethereum/contract.ts","../src/protocols/ethereum/subgraph.ts","../src/protocols/ethereum/type-generator.ts","../src/protocols/ethereum/codegen/abi.test.ts","../src/protocols/ethereum/codegen/abi.ts","../src/protocols/ethereum/codegen/template.ts","../src/protocols/ethereum/scaffold/manifest.ts","../src/protocols/ethereum/scaffold/mapping.ts","../src/protocols/near/contract.ts","../src/protocols/near/subgraph.ts","../src/protocols/near/scaffold/manifest.ts","../src/protocols/near/scaffold/mapping.ts","../src/protocols/subgraph/subgraph.ts","../src/protocols/subgraph/scaffold/manifest.ts","../src/protocols/subgraph/scaffold/mapping.ts","../src/protocols/substreams/subgraph.ts","../src/protocols/substreams/scaffold/manifest.ts","../src/scaffold/cosmos.test.ts","../src/scaffold/ethereum.test.ts","../src/scaffold/get-docker-file.ts","../src/scaffold/get-git-ignore.ts","../src/scaffold/index.ts","../src/scaffold/mapping.ts","../src/scaffold/near.test.ts","../src/scaffold/schema.ts","../src/scaffold/tests.ts","../src/validation/contract.ts","../src/validation/index.ts","../src/validation/manifest.ts","../src/validation/schema.test.ts","../src/validation/schema.ts"],"version":"5.7.2"}
@@ -1011,5 +1011,5 @@
1011
1011
  ]
1012
1012
  }
1013
1013
  },
1014
- "version": "0.94.0-alpha-20250109043757-c53ef95047eaf4833353c30dc2d1f6cbb9fc13e4"
1014
+ "version": "0.94.0"
1015
1015
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphprotocol/graph-cli",
3
- "version": "0.94.0-alpha-20250109043757-c53ef95047eaf4833353c30dc2d1f6cbb9fc13e4",
3
+ "version": "0.94.0",
4
4
  "type": "module",
5
5
  "description": "CLI for building for and deploying to The Graph",
6
6
  "license": "(Apache-2.0 OR MIT)",
@@ -1,7 +0,0 @@
1
- import EthereumABI from '../protocols/ethereum/abi.js';
2
- import { ContractService } from './contracts.js';
3
- export interface CheckForProxyResult {
4
- implementationAbi: EthereumABI | null;
5
- implementationAddress: string | null;
6
- }
7
- export declare function checkForProxy(contractService: ContractService, network: string, address: string, abi: EthereumABI): Promise<CheckForProxyResult>;
@@ -1,25 +0,0 @@
1
- import { prompt } from 'gluegun';
2
- import EthereumABI from '../protocols/ethereum/abi.js';
3
- import { retryWithPrompt } from './retry.js';
4
- import { withSpinner } from './spinner.js';
5
- export async function checkForProxy(contractService, network, address, abi) {
6
- let implementationAddress = null;
7
- let implementationAbi = null;
8
- const maybeProxy = abi.callFunctionSignatures()?.includes('upgradeTo(address)');
9
- if (maybeProxy) {
10
- const impl = await retryWithPrompt(() => withSpinner('Fetching proxy implementation address...', 'Failed to fetch proxy implementation address', 'Warning fetching proxy implementation address', () => contractService.getProxyImplementation(network, address)));
11
- if (impl) {
12
- const useImplementation = await prompt
13
- .confirm(`Proxy contract detected. Use implementation contract ABI at ${impl}?`, true)
14
- .catch(() => false);
15
- if (useImplementation) {
16
- implementationAddress = impl;
17
- implementationAbi = await retryWithPrompt(() => withSpinner('Fetching implementation contract ABI...', 'Failed to fetch implementation ABI', 'Warning fetching implementation ABI', () => contractService.getABI(EthereumABI, network, implementationAddress)));
18
- }
19
- }
20
- }
21
- return {
22
- implementationAbi,
23
- implementationAddress,
24
- };
25
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,64 +0,0 @@
1
- import { prompt } from 'gluegun';
2
- import { describe, expect, it, vi } from 'vitest';
3
- import EthereumABI from '../protocols/ethereum/abi.js';
4
- import { ContractService } from './contracts.js';
5
- import { checkForProxy } from './proxy.js';
6
- import { loadRegistry } from './registry.js';
7
- // Mock gluegun's prompt
8
- vi.mock('gluegun', async () => {
9
- const actual = await vi.importActual('gluegun');
10
- return {
11
- ...actual,
12
- prompt: {
13
- confirm: vi.fn().mockResolvedValue(true),
14
- },
15
- };
16
- });
17
- describe('Proxy detection', async () => {
18
- const NETWORK = 'mainnet';
19
- const registry = await loadRegistry();
20
- const contractService = new ContractService(registry);
21
- const testCases = [
22
- {
23
- name: 'USDC',
24
- type: 'EIP-1967 Upgradeable',
25
- address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
26
- implementationAddress: '0x43506849d7c04f9138d1a2050bbf3a0c054402dd',
27
- expectedFunctions: ['mint(address,uint256)', 'configureMinter(address,uint256)'],
28
- },
29
- {
30
- name: 'BUSD',
31
- type: 'OpenZeppelin Unstructured Storage',
32
- address: '0x4Fabb145d64652a948d72533023f6E7A623C7C53',
33
- implementationAddress: '0x2A3F1A37C04F82aA274f5353834B2d002Db91015',
34
- expectedFunctions: ['reclaimBUSD()', 'claimOwnership()'],
35
- },
36
- {
37
- name: 'Gelato',
38
- type: 'EIP-2535 Diamond Pattern (not supported)',
39
- address: '0x3caca7b48d0573d793d3b0279b5f0029180e83b6',
40
- implementationAddress: null,
41
- expectedFunctions: [],
42
- },
43
- ];
44
- for (const testCase of testCases) {
45
- it(`should handle ${testCase.name} ${testCase.type} Proxy`, async () => {
46
- const abi = await contractService.getABI(EthereumABI, NETWORK, testCase.address);
47
- expect(abi).toBeDefined();
48
- const { implementationAddress, implementationAbi } = await checkForProxy(contractService, NETWORK, testCase.address, abi);
49
- expect(implementationAddress === testCase.implementationAddress);
50
- const implFunctions = implementationAbi?.callFunctionSignatures();
51
- for (const expectedFunction of testCase.expectedFunctions) {
52
- expect(implFunctions).toContain(expectedFunction);
53
- }
54
- });
55
- }
56
- it('should handle when user declines to use implementation', async () => {
57
- vi.mocked(prompt.confirm).mockResolvedValueOnce(false);
58
- const abi = await contractService.getABI(EthereumABI, NETWORK, testCases[0].address);
59
- expect(abi).toBeDefined();
60
- const { implementationAddress, implementationAbi } = await checkForProxy(contractService, NETWORK, testCases[0].address, abi);
61
- expect(implementationAddress).toBeNull();
62
- expect(implementationAbi).toBeNull();
63
- });
64
- });