Skip to content

Commit b6d7f1c

Browse files
incrypto32github-actions[bot]YaroShkvorets
authored
graph init: add subgraph composition (#1920)
* Fix subgraphs without abi field failing to build * Fix graph init for composed subgraphs * Add changeset * Fix validation not working * Support declared calls in manifest * Lint fix * Address review comments * Dont allow adding new contracts when subgraph is a composed subgraph * Allow init of subgraph datasource subgraphs without the interactive mode * Reduce code duplication between subgraph datasource and normal data source * prevent using --from-contract and --from-source-subgraph flags together * cli: validate protocol and source subgraph relationship * chore(dependencies): updated changesets for modified dependencies * change flag name for source subgraph * Refactor manifest validation util functions * get start block from source manifest * set fromSubgraph to be default value for graph init in interactive mode * fix protocol flag validation * Add init test for subgraphs * Fix error message * chore(dependencies): updated changesets for modified dependencies --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: YaroShkvorets <[email protected]>
1 parent 7f22631 commit b6d7f1c

File tree

10 files changed

+330
-68
lines changed

10 files changed

+330
-68
lines changed

.changeset/curly-buses-hang.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphprotocol/graph-cli': minor
3+
---
4+
5+
Add support for subgraph datasource in `graph init`

packages/cli/src/commands/codegen.ts

-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ export default class CodegenCommand extends Command {
4343
summary: 'IPFS node to use for fetching subgraph data.',
4444
char: 'i',
4545
default: DEFAULT_IPFS_URL,
46-
hidden: true,
4746
}),
4847
'uncrashable-config': Flags.file({
4948
summary: 'Directory for uncrashable config.',

packages/cli/src/commands/init.ts

+110-35
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ import EthereumABI from '../protocols/ethereum/abi.js';
2323
import Protocol, { ProtocolName } from '../protocols/index.js';
2424
import { abiEvents } from '../scaffold/schema.js';
2525
import Schema from '../schema.js';
26-
import { createIpfsClient, loadSubgraphSchemaFromIPFS } from '../utils.js';
26+
import {
27+
createIpfsClient,
28+
getMinStartBlock,
29+
loadManifestYaml,
30+
loadSubgraphSchemaFromIPFS,
31+
validateSubgraphNetworkMatch,
32+
} from '../utils.js';
2733
import { validateContract } from '../validation/index.js';
2834
import AddCommand from './add.js';
2935

@@ -54,6 +60,10 @@ export default class InitCommand extends Command {
5460
summary: 'Graph node for which to initialize.',
5561
char: 'g',
5662
}),
63+
'from-subgraph': Flags.string({
64+
description: 'Creates a scaffold based on an existing subgraph.',
65+
exclusive: ['from-example', 'from-contract'],
66+
}),
5767
'from-contract': Flags.string({
5868
description: 'Creates a scaffold based on an existing contract.',
5969
exclusive: ['from-example'],
@@ -88,7 +98,6 @@ export default class InitCommand extends Command {
8898
description: 'Block number to start indexing from.',
8999
// TODO: using a default sets the value and therefore requires --from-contract
90100
// default: '0',
91-
dependsOn: ['from-contract'],
92101
}),
93102

94103
abi: Flags.string({
@@ -110,7 +119,6 @@ export default class InitCommand extends Command {
110119
summary: 'IPFS node to use for fetching subgraph data.',
111120
char: 'i',
112121
default: DEFAULT_IPFS_URL,
113-
hidden: true,
114122
}),
115123
};
116124

@@ -127,6 +135,7 @@ export default class InitCommand extends Command {
127135
protocol,
128136
node: nodeFlag,
129137
'from-contract': fromContract,
138+
'from-subgraph': fromSubgraph,
130139
'contract-name': contractName,
131140
'from-example': fromExample,
132141
'index-events': indexEvents,
@@ -141,11 +150,20 @@ export default class InitCommand extends Command {
141150

142151
initDebugger('Flags: %O', flags);
143152

153+
if (startBlock && !(fromContract || fromSubgraph)) {
154+
this.error('--start-block can only be used with --from-contract or --from-subgraph');
155+
}
156+
157+
if (fromContract && fromSubgraph) {
158+
this.error('Cannot use both --from-contract and --from-subgraph at the same time');
159+
}
160+
144161
if (skipGit) {
145162
this.warn(
146163
'The --skip-git flag will be removed in the next major version. By default we will stop initializing a Git repository.',
147164
);
148165
}
166+
149167
if ((fromContract || spkgPath) && !network && !fromExample) {
150168
this.error('--network is required when using --from-contract or --spkg');
151169
}
@@ -199,16 +217,15 @@ export default class InitCommand extends Command {
199217
let abi!: EthereumABI;
200218

201219
// If all parameters are provided from the command-line,
202-
// go straight to creating the subgraph from an existing contract
203-
if ((fromContract || spkgPath) && protocol && subgraphName && directory && network && node) {
204-
const registry = await loadRegistry();
205-
const contractService = new ContractService(registry);
206-
const sourcifyContractInfo = await contractService.getFromSourcify(
207-
EthereumABI,
208-
network,
209-
fromContract!,
210-
);
211-
220+
// go straight to creating the subgraph from an existing contract or source subgraph
221+
if (
222+
(fromContract || spkgPath || fromSubgraph) &&
223+
protocol &&
224+
subgraphName &&
225+
directory &&
226+
network &&
227+
node
228+
) {
212229
if (!protocolChoices.includes(protocol as ProtocolName)) {
213230
this.error(
214231
`Protocol '${protocol}' is not supported, choose from these options: ${protocolChoices.join(
@@ -220,7 +237,31 @@ export default class InitCommand extends Command {
220237

221238
const protocolInstance = new Protocol(protocol as ProtocolName);
222239

223-
if (protocolInstance.hasABIs()) {
240+
if (fromSubgraph && !protocolInstance.isComposedSubgraph()) {
241+
this.error('--protocol can only be subgraph when using --from-subgraph');
242+
}
243+
244+
if (
245+
fromContract &&
246+
(protocolInstance.isComposedSubgraph() || protocolInstance.isSubstreams())
247+
) {
248+
this.error('--protocol cannot be subgraph or substreams when using --from-contract');
249+
}
250+
251+
if (spkgPath && !protocolInstance.isSubstreams()) {
252+
this.error('--protocol can only be substreams when using --spkg');
253+
}
254+
255+
// Only fetch contract info and ABI for non-source-subgraph cases
256+
if (!fromSubgraph && protocolInstance.hasABIs()) {
257+
const registry = await loadRegistry();
258+
const contractService = new ContractService(registry);
259+
const sourcifyContractInfo = await contractService.getFromSourcify(
260+
EthereumABI,
261+
network,
262+
fromContract!,
263+
);
264+
224265
const ABI = protocolInstance.getABI();
225266
if (abiPath) {
226267
try {
@@ -244,7 +285,7 @@ export default class InitCommand extends Command {
244285
protocolInstance,
245286
abi,
246287
directory,
247-
source: fromContract!,
288+
source: fromSubgraph || fromContract!,
248289
indexEvents,
249290
network,
250291
subgraphName,
@@ -288,7 +329,7 @@ export default class InitCommand extends Command {
288329
abi,
289330
abiPath,
290331
directory,
291-
source: fromContract,
332+
source: fromContract || fromSubgraph,
292333
indexEvents,
293334
fromExample,
294335
subgraphName,
@@ -534,7 +575,7 @@ async function processInitForm(
534575
value: 'contract',
535576
},
536577
{ message: 'Substreams', name: 'substreams', value: 'substreams' },
537-
// { message: 'Subgraph', name: 'subgraph', value: 'subgraph' },
578+
{ message: 'Subgraph', name: 'subgraph', value: 'subgraph' },
538579
].filter(({ name }) => name),
539580
});
540581

@@ -604,6 +645,30 @@ async function processInitForm(
604645
},
605646
});
606647

648+
promptManager.addStep({
649+
type: 'input',
650+
name: 'ipfs',
651+
message: `IPFS node to use for fetching subgraph manifest`,
652+
initial: ipfsUrl,
653+
skip: () => !isComposedSubgraph,
654+
validate: value => {
655+
if (!value) {
656+
return 'IPFS node URL cannot be empty';
657+
}
658+
try {
659+
new URL(value);
660+
return true;
661+
} catch {
662+
return 'Please enter a valid URL';
663+
}
664+
},
665+
result: value => {
666+
ipfsNode = value;
667+
initDebugger.extend('processInitForm')('ipfs: %O', value);
668+
return value;
669+
},
670+
});
671+
607672
promptManager.addStep({
608673
type: 'input',
609674
name: 'source',
@@ -616,9 +681,16 @@ async function processInitForm(
616681
isSubstreams ||
617682
(!protocolInstance.hasContract() && !isComposedSubgraph),
618683
initial: initContract,
619-
validate: async (value: string) => {
684+
validate: async (value: string): Promise<string | boolean> => {
620685
if (isComposedSubgraph) {
621-
return value.startsWith('Qm') ? true : 'Subgraph deployment ID must start with Qm';
686+
const ipfs = createIpfsClient(ipfsNode);
687+
const manifestYaml = await loadManifestYaml(ipfs, value);
688+
const { valid, error } = validateSubgraphNetworkMatch(manifestYaml, network.id);
689+
if (!valid) {
690+
return error || 'Invalid subgraph network match';
691+
}
692+
startBlock ||= getMinStartBlock(manifestYaml)?.toString();
693+
return true;
622694
}
623695
if (initFromExample !== undefined || !protocolInstance.hasContract()) {
624696
return true;
@@ -668,6 +740,7 @@ async function processInitForm(
668740
} else {
669741
abiFromApi = initAbi;
670742
}
743+
671744
// If startBlock is not provided, try to fetch it from Etherscan API
672745
if (!initStartBlock) {
673746
startBlock = await retryWithPrompt(() =>
@@ -699,19 +772,6 @@ async function processInitForm(
699772
},
700773
});
701774

702-
promptManager.addStep({
703-
type: 'input',
704-
name: 'ipfs',
705-
message: `IPFS node to use for fetching subgraph manifest`,
706-
initial: ipfsUrl,
707-
skip: () => !isComposedSubgraph,
708-
result: value => {
709-
ipfsNode = value;
710-
initDebugger.extend('processInitForm')('ipfs: %O', value);
711-
return value;
712-
},
713-
});
714-
715775
promptManager.addStep({
716776
type: 'input',
717777
name: 'spkg',
@@ -751,7 +811,7 @@ async function processInitForm(
751811
isSubstreams ||
752812
!!initAbiPath ||
753813
isComposedSubgraph,
754-
validate: async (value: string) => {
814+
validate: async (value: string): Promise<string | boolean> => {
755815
if (
756816
initFromExample ||
757817
abiFromApi ||
@@ -1199,6 +1259,14 @@ async function initSubgraphFromContract(
11991259
},
12001260
});
12011261

1262+
// Validate network match first
1263+
const manifestYaml = await loadManifestYaml(ipfsClient, source);
1264+
const { valid, error } = validateSubgraphNetworkMatch(manifestYaml, network);
1265+
if (!valid) {
1266+
throw new Error(error || 'Invalid subgraph network match');
1267+
}
1268+
1269+
startBlock ||= getMinStartBlock(manifestYaml)?.toString();
12021270
const schemaString = await loadSubgraphSchemaFromIPFS(ipfsClient, source);
12031271
const schema = await Schema.loadFromString(schemaString);
12041272
entities = schema.getEntityNames();
@@ -1208,8 +1276,9 @@ async function initSubgraphFromContract(
12081276
}
12091277

12101278
if (
1211-
!protocolInstance.isComposedSubgraph() &&
1279+
!isComposedSubgraph &&
12121280
protocolInstance.hasABIs() &&
1281+
abi && // Add check for abi existence
12131282
(abiEvents(abi).size === 0 ||
12141283
// @ts-expect-error TODO: the abiEvents result is expected to be a List, how's it an array?
12151284
abiEvents(abi).length === 0)
@@ -1224,6 +1293,12 @@ async function initSubgraphFromContract(
12241293
`Failed to create subgraph scaffold`,
12251294
`Warnings while creating subgraph scaffold`,
12261295
async spinner => {
1296+
initDebugger('Generating scaffold with ABI:', abi);
1297+
initDebugger('ABI data:', abi?.data);
1298+
if (abi) {
1299+
initDebugger('ABI events:', abiEvents(abi));
1300+
}
1301+
12271302
const scaffold = await generateScaffold(
12281303
{
12291304
protocolInstance,
@@ -1280,7 +1355,7 @@ async function initSubgraphFromContract(
12801355
this.exit(1);
12811356
}
12821357

1283-
while (addContract) {
1358+
while (addContract && !isComposedSubgraph) {
12841359
addContract = await addAnotherContract
12851360
.bind(this)({
12861361
protocolInstance,

packages/cli/src/compiler/index.ts

+39-19
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,9 @@ export default class Compiler {
508508
`Failed to write compiled subgraph to ${displayDir}`,
509509
`Warnings while writing compiled subgraph to ${displayDir}`,
510510
async spinner => {
511+
// Add debug log for initial subgraph state
512+
compilerDebug('Initial subgraph state:', subgraph.toJS());
513+
511514
// Copy schema and update its path
512515
subgraph = subgraph.updateIn(['schema', 'file'], schemaFile => {
513516
const schemaFilePath = path.resolve(this.sourceDir, schemaFile as string);
@@ -518,32 +521,49 @@ export default class Compiler {
518521
return path.relative(this.options.outputDir, targetFile);
519522
});
520523

524+
// Add debug log before processing data sources
525+
compilerDebug('Processing dataSources:', subgraph.get('dataSources').toJS());
526+
521527
// Copy data source files and update their paths
522528
subgraph = subgraph.update('dataSources', (dataSources: any[]) =>
523529
dataSources.map(dataSource => {
530+
// Add debug log for each data source
531+
compilerDebug('Processing dataSource:', dataSource.toJS());
532+
524533
let updatedDataSource = dataSource;
525534

526535
if (this.protocol.hasABIs()) {
527-
updatedDataSource = updatedDataSource
528-
// Write data source ABIs to the output directory
529-
.updateIn(['mapping', 'abis'], (abis: any[]) =>
530-
abis.map((abi: any) =>
531-
abi.update('file', (abiFile: string) => {
532-
abiFile = path.resolve(this.sourceDir, abiFile);
533-
const abiData = this.ABI.load(abi.get('name'), abiFile);
534-
return path.relative(
535-
this.options.outputDir,
536-
this._writeSubgraphFile(
537-
abiFile,
538-
JSON.stringify(abiData.data.toJS(), null, 2),
539-
this.sourceDir,
540-
this.subgraphDir(this.options.outputDir, dataSource),
541-
spinner,
542-
),
543-
);
544-
}),
545-
),
536+
// Add debug log for ABIs
537+
compilerDebug(
538+
'Processing ABIs for dataSource:',
539+
dataSource.getIn(['mapping', 'abis'])?.toJS() || 'undefined',
540+
);
541+
542+
updatedDataSource = updatedDataSource.updateIn(['mapping', 'abis'], (abis: any[]) => {
543+
compilerDebug('ABIs value:', Array.isArray(abis) ? abis : 'undefined');
544+
545+
if (!abis) {
546+
compilerDebug('No ABIs found for dataSource');
547+
return immutable.List();
548+
}
549+
550+
return abis.map((abi: any) =>
551+
abi.update('file', (abiFile: string) => {
552+
abiFile = path.resolve(this.sourceDir, abiFile);
553+
const abiData = this.ABI.load(abi.get('name'), abiFile);
554+
return path.relative(
555+
this.options.outputDir,
556+
this._writeSubgraphFile(
557+
abiFile,
558+
JSON.stringify(abiData.data.toJS(), null, 2),
559+
this.sourceDir,
560+
this.subgraphDir(this.options.outputDir, dataSource),
561+
spinner,
562+
),
563+
);
564+
}),
546565
);
566+
});
547567
}
548568

549569
if (protocol.name == 'substreams' || protocol.name == 'substreams/triggers') {

packages/cli/src/protocols/subgraph/manifest.graphql

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type ContractABI {
6363
type EntityHandler {
6464
handler: String!
6565
entity: String!
66+
calls: JSON
6667
}
6768

6869
type Graft {

0 commit comments

Comments
 (0)