Skip to content

Commit 6b9f04b

Browse files
committed
Fix graph init for composed subgraphs
1 parent fddfea6 commit 6b9f04b

File tree

3 files changed

+154
-19
lines changed

3 files changed

+154
-19
lines changed

packages/cli/src/commands/init.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ 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+
loadSubgraphSchemaFromIPFS,
29+
validateSubgraphNetworkMatch,
30+
} from '../utils.js';
2731
import { validateContract } from '../validation/index.js';
2832
import AddCommand from './add.js';
2933

@@ -515,7 +519,7 @@ async function processInitForm(
515519
value: 'contract',
516520
},
517521
{ message: 'Substreams', name: 'substreams', value: 'substreams' },
518-
// { message: 'Subgraph', name: 'subgraph', value: 'subgraph' },
522+
{ message: 'Subgraph', name: 'subgraph', value: 'subgraph' },
519523
].filter(({ name }) => name),
520524
});
521525

@@ -596,9 +600,17 @@ async function processInitForm(
596600
isSubstreams ||
597601
(!protocolInstance.hasContract() && !isComposedSubgraph),
598602
initial: initContract,
599-
validate: async (value: string) => {
603+
validate: async (value: string): Promise<string | boolean> => {
600604
if (isComposedSubgraph) {
601-
return value.startsWith('Qm') ? true : 'Subgraph deployment ID must start with Qm';
605+
if (!ipfsNode) {
606+
return true; // Skip validation if no IPFS node is available
607+
}
608+
const ipfs = createIpfsClient(ipfsNode);
609+
const { valid, error } = await validateSubgraphNetworkMatch(ipfs, value, network.id);
610+
if (!valid) {
611+
return error || 'Invalid subgraph network match';
612+
}
613+
return true;
602614
}
603615
if (initFromExample !== undefined || !protocolInstance.hasContract()) {
604616
return true;
@@ -731,7 +743,7 @@ async function processInitForm(
731743
isSubstreams ||
732744
!!initAbiPath ||
733745
isComposedSubgraph,
734-
validate: async (value: string) => {
746+
validate: async (value: string): Promise<string | boolean> => {
735747
if (
736748
initFromExample ||
737749
abiFromApi ||
@@ -822,6 +834,22 @@ async function processInitForm(
822834

823835
await promptManager.executeInteractive();
824836

837+
// If loading from IPFS, validate network matches
838+
if (ipfsNode && subgraphName.startsWith('Qm')) {
839+
const ipfs = createIpfsClient(ipfsNode);
840+
try {
841+
const { valid, error } = await validateSubgraphNetworkMatch(ipfs, subgraphName, network.id);
842+
if (!valid) {
843+
throw new Error(error || 'Invalid subgraph network match');
844+
}
845+
} catch (e) {
846+
if (e instanceof Error) {
847+
print.error(`Failed to validate subgraph network: ${e.message}`);
848+
}
849+
throw e;
850+
}
851+
}
852+
825853
return {
826854
abi: (abiFromApi || abiFromFile)!,
827855
protocolInstance,
@@ -1188,8 +1216,9 @@ async function initSubgraphFromContract(
11881216
}
11891217

11901218
if (
1191-
!protocolInstance.isComposedSubgraph() &&
1219+
!isComposedSubgraph &&
11921220
protocolInstance.hasABIs() &&
1221+
abi && // Add check for abi existence
11931222
(abiEvents(abi).size === 0 ||
11941223
// @ts-expect-error TODO: the abiEvents result is expected to be a List, how's it an array?
11951224
abiEvents(abi).length === 0)
@@ -1204,6 +1233,12 @@ async function initSubgraphFromContract(
12041233
`Failed to create subgraph scaffold`,
12051234
`Warnings while creating subgraph scaffold`,
12061235
async spinner => {
1236+
initDebugger('Generating scaffold with ABI:', abi);
1237+
initDebugger('ABI data:', abi?.data);
1238+
if (abi) {
1239+
initDebugger('ABI events:', abiEvents(abi));
1240+
}
1241+
12071242
const scaffold = await generateScaffold(
12081243
{
12091244
protocolInstance,

packages/cli/src/scaffold/index.ts

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import debugFactory from 'debug';
12
import fs from 'fs-extra';
23
import { strings } from 'gluegun';
34
import prettier from 'prettier';
@@ -11,6 +12,8 @@ import { generateEventIndexingHandlers } from './mapping.js';
1112
import { abiEvents, generateEventType, generateExampleEntityType } from './schema.js';
1213
import { generateTestsFiles } from './tests.js';
1314

15+
const scaffoldDebugger = debugFactory('graph-cli:scaffold');
16+
1417
const GRAPH_CLI_VERSION = process.env.GRAPH_CLI_TESTS
1518
? // JSON.stringify should remove this key, we will install the local
1619
// graph-cli for the tests using `npm link` instead of fetching from npm.
@@ -47,18 +50,34 @@ export default class Scaffold {
4750
spkgPath?: string;
4851
entities?: string[];
4952

50-
constructor(options: ScaffoldOptions) {
51-
this.protocol = options.protocol;
52-
this.abi = options.abi;
53-
this.indexEvents = options.indexEvents;
54-
this.contract = options.contract;
55-
this.network = options.network;
56-
this.contractName = options.contractName;
57-
this.subgraphName = options.subgraphName;
58-
this.startBlock = options.startBlock;
59-
this.node = options.node;
60-
this.spkgPath = options.spkgPath;
61-
this.entities = options.entities;
53+
constructor({
54+
protocol,
55+
abi,
56+
contract,
57+
network,
58+
contractName,
59+
startBlock,
60+
subgraphName,
61+
node,
62+
spkgPath,
63+
indexEvents,
64+
entities,
65+
}: ScaffoldOptions) {
66+
this.protocol = protocol;
67+
this.abi = abi;
68+
this.contract = contract;
69+
this.network = network;
70+
this.contractName = contractName;
71+
this.startBlock = startBlock;
72+
this.subgraphName = subgraphName;
73+
this.node = node;
74+
this.spkgPath = spkgPath;
75+
this.indexEvents = indexEvents;
76+
this.entities = entities;
77+
78+
scaffoldDebugger('Scaffold constructor called with ABI:', abi);
79+
scaffoldDebugger('ABI data:', abi?.data);
80+
scaffoldDebugger('ABI file:', abi?.file);
6281
}
6382

6483
async generatePackageJson() {
@@ -203,9 +222,24 @@ dataSources:
203222
}
204223

205224
async generateABIs() {
225+
scaffoldDebugger('Generating ABIs...');
226+
scaffoldDebugger('Protocol has ABIs:', this.protocol.hasABIs());
227+
scaffoldDebugger('ABI data:', this.abi?.data);
228+
scaffoldDebugger('ABI file:', this.abi?.file);
229+
230+
if (!this.protocol.hasABIs()) {
231+
scaffoldDebugger('Protocol does not have ABIs, skipping ABI generation');
232+
return;
233+
}
234+
235+
if (!this.abi?.data) {
236+
scaffoldDebugger('ABI data is undefined, skipping ABI generation');
237+
return;
238+
}
239+
206240
return this.protocol.hasABIs()
207241
? {
208-
[`${this.contractName}.json`]: await prettier.format(JSON.stringify(this.abi?.data), {
242+
[`${this.contractName}.json`]: await prettier.format(JSON.stringify(this.abi.data), {
209243
parser: 'json',
210244
}),
211245
}

packages/cli/src/utils.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,69 @@ export async function loadSubgraphSchemaFromIPFS(ipfsClient: any, manifest: stri
3232
throw Error(`Failed to load schema from IPFS ${manifest}`);
3333
}
3434
}
35+
36+
export async function loadManifestFromIPFS(ipfsClient: any, manifest: string) {
37+
try {
38+
const manifestBuffer = ipfsClient.cat(manifest);
39+
let manifestFile = '';
40+
for await (const chunk of manifestBuffer) {
41+
manifestFile += Buffer.from(chunk).toString('utf8');
42+
}
43+
return manifestFile;
44+
} catch (e) {
45+
utilsDebug.extend('loadManifestFromIPFS')(`Failed to load manifest from IPFS ${manifest}`);
46+
utilsDebug.extend('loadManifestFromIPFS')(e);
47+
throw Error(`Failed to load manifest from IPFS ${manifest}`);
48+
}
49+
}
50+
51+
/**
52+
* Validates that the network of a source subgraph matches the target network
53+
* @param ipfsClient IPFS client instance
54+
* @param sourceSubgraphId IPFS hash of the source subgraph
55+
* @param targetNetwork Network of the target subgraph being created
56+
* @returns Object containing validation result and error message if any
57+
*/
58+
export async function validateSubgraphNetworkMatch(
59+
ipfsClient: any,
60+
sourceSubgraphId: string,
61+
targetNetwork: string,
62+
): Promise<{ valid: boolean; error?: string }> {
63+
try {
64+
const manifestFile = await loadManifestFromIPFS(ipfsClient, sourceSubgraphId);
65+
const manifestYaml = yaml.load(manifestFile) as any;
66+
67+
// Extract network from data sources
68+
const dataSources = manifestYaml.dataSources || [];
69+
const templates = manifestYaml.templates || [];
70+
const allSources = [...dataSources, ...templates];
71+
72+
if (allSources.length === 0) {
73+
return { valid: true }; // No data sources to validate
74+
}
75+
76+
// Get network from first data source
77+
const sourceNetwork = allSources[0].network;
78+
if (!sourceNetwork) {
79+
return { valid: true }; // Network not specified in source, skip validation
80+
}
81+
82+
const normalizedSourceNetwork = sourceNetwork.toLowerCase();
83+
const normalizedTargetNetwork = targetNetwork.toLowerCase();
84+
85+
if (normalizedSourceNetwork !== normalizedTargetNetwork) {
86+
return {
87+
valid: false,
88+
error: `Network mismatch: The source subgraph is indexing the '${sourceNetwork}' network, but you're creating a subgraph for '${targetNetwork}' network. When composing subgraphs, they must index the same network.`,
89+
};
90+
}
91+
92+
return { valid: true };
93+
} catch (e) {
94+
utilsDebug.extend('validateSubgraphNetworkMatch')(`Failed to validate network match: ${e}`);
95+
return {
96+
valid: false,
97+
error: e instanceof Error ? e.message : 'Failed to validate subgraph network',
98+
};
99+
}
100+
}

0 commit comments

Comments
 (0)