diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index f50927eb..3fbfff5b 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -23,7 +23,11 @@ import EthereumABI from '../protocols/ethereum/abi.js'; import Protocol, { ProtocolName } from '../protocols/index.js'; import { abiEvents } from '../scaffold/schema.js'; import Schema from '../schema.js'; -import { createIpfsClient, loadSubgraphSchemaFromIPFS } from '../utils.js'; +import { + createIpfsClient, + loadSubgraphSchemaFromIPFS, + validateSubgraphNetworkMatch, +} from '../utils.js'; import { validateContract } from '../validation/index.js'; import AddCommand from './add.js'; @@ -508,7 +512,7 @@ async function processInitForm( value: 'contract', }, { message: 'Substreams', name: 'substreams', value: 'substreams' }, - // { message: 'Subgraph', name: 'subgraph', value: 'subgraph' }, + { message: 'Subgraph', name: 'subgraph', value: 'subgraph' }, ].filter(({ name }) => name), }); @@ -589,9 +593,17 @@ async function processInitForm( isSubstreams || (!protocolInstance.hasContract() && !isComposedSubgraph), initial: initContract, - validate: async (value: string) => { + validate: async (value: string): Promise => { if (isComposedSubgraph) { - return value.startsWith('Qm') ? true : 'Subgraph deployment ID must start with Qm'; + if (!ipfsNode) { + return true; // Skip validation if no IPFS node is available + } + const ipfs = createIpfsClient(ipfsNode); + const { valid, error } = await validateSubgraphNetworkMatch(ipfs, value, network.id); + if (!valid) { + return error || 'Invalid subgraph network match'; + } + return true; } if (initFromExample !== undefined || !protocolInstance.hasContract()) { return true; @@ -706,7 +718,7 @@ async function processInitForm( isSubstreams || !!initAbiPath || isComposedSubgraph, - validate: async (value: string) => { + validate: async (value: string): Promise => { if ( initFromExample || abiFromApi || @@ -797,6 +809,22 @@ async function processInitForm( await promptManager.executeInteractive(); + // If loading from IPFS, validate network matches + if (ipfsNode && subgraphName.startsWith('Qm')) { + const ipfs = createIpfsClient(ipfsNode); + try { + const { valid, error } = await validateSubgraphNetworkMatch(ipfs, subgraphName, network.id); + if (!valid) { + throw new Error(error || 'Invalid subgraph network match'); + } + } catch (e) { + if (e instanceof Error) { + print.error(`Failed to validate subgraph network: ${e.message}`); + } + throw e; + } + } + return { abi: (abiFromApi || abiFromFile)!, protocolInstance, @@ -1163,8 +1191,9 @@ async function initSubgraphFromContract( } if ( - !protocolInstance.isComposedSubgraph() && + !isComposedSubgraph && protocolInstance.hasABIs() && + abi && // Add check for abi existence (abiEvents(abi).size === 0 || // @ts-expect-error TODO: the abiEvents result is expected to be a List, how's it an array? abiEvents(abi).length === 0) @@ -1179,6 +1208,12 @@ async function initSubgraphFromContract( `Failed to create subgraph scaffold`, `Warnings while creating subgraph scaffold`, async spinner => { + initDebugger('Generating scaffold with ABI:', abi); + initDebugger('ABI data:', abi?.data); + if (abi) { + initDebugger('ABI events:', abiEvents(abi)); + } + const scaffold = await generateScaffold( { protocolInstance, diff --git a/packages/cli/src/scaffold/index.ts b/packages/cli/src/scaffold/index.ts index 66a14cee..d80c780c 100644 --- a/packages/cli/src/scaffold/index.ts +++ b/packages/cli/src/scaffold/index.ts @@ -1,3 +1,4 @@ +import debugFactory from 'debug'; import fs from 'fs-extra'; import { strings } from 'gluegun'; import prettier from 'prettier'; @@ -11,6 +12,8 @@ import { generateEventIndexingHandlers } from './mapping.js'; import { abiEvents, generateEventType, generateExampleEntityType } from './schema.js'; import { generateTestsFiles } from './tests.js'; +const scaffoldDebugger = debugFactory('graph-cli:scaffold'); + const GRAPH_CLI_VERSION = process.env.GRAPH_CLI_TESTS ? // JSON.stringify should remove this key, we will install the local // graph-cli for the tests using `npm link` instead of fetching from npm. @@ -47,18 +50,34 @@ export default class Scaffold { spkgPath?: string; entities?: string[]; - constructor(options: ScaffoldOptions) { - this.protocol = options.protocol; - this.abi = options.abi; - this.indexEvents = options.indexEvents; - this.contract = options.contract; - this.network = options.network; - this.contractName = options.contractName; - this.subgraphName = options.subgraphName; - this.startBlock = options.startBlock; - this.node = options.node; - this.spkgPath = options.spkgPath; - this.entities = options.entities; + constructor({ + protocol, + abi, + contract, + network, + contractName, + startBlock, + subgraphName, + node, + spkgPath, + indexEvents, + entities, + }: ScaffoldOptions) { + this.protocol = protocol; + this.abi = abi; + this.contract = contract; + this.network = network; + this.contractName = contractName; + this.startBlock = startBlock; + this.subgraphName = subgraphName; + this.node = node; + this.spkgPath = spkgPath; + this.indexEvents = indexEvents; + this.entities = entities; + + scaffoldDebugger('Scaffold constructor called with ABI:', abi); + scaffoldDebugger('ABI data:', abi?.data); + scaffoldDebugger('ABI file:', abi?.file); } async generatePackageJson() { @@ -203,9 +222,24 @@ dataSources: } async generateABIs() { + scaffoldDebugger('Generating ABIs...'); + scaffoldDebugger('Protocol has ABIs:', this.protocol.hasABIs()); + scaffoldDebugger('ABI data:', this.abi?.data); + scaffoldDebugger('ABI file:', this.abi?.file); + + if (!this.protocol.hasABIs()) { + scaffoldDebugger('Protocol does not have ABIs, skipping ABI generation'); + return; + } + + if (!this.abi?.data) { + scaffoldDebugger('ABI data is undefined, skipping ABI generation'); + return; + } + return this.protocol.hasABIs() ? { - [`${this.contractName}.json`]: await prettier.format(JSON.stringify(this.abi?.data), { + [`${this.contractName}.json`]: await prettier.format(JSON.stringify(this.abi.data), { parser: 'json', }), } diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 5b8223d2..4dcbb9d4 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -32,3 +32,69 @@ export async function loadSubgraphSchemaFromIPFS(ipfsClient: any, manifest: stri throw Error(`Failed to load schema from IPFS ${manifest}`); } } + +export async function loadManifestFromIPFS(ipfsClient: any, manifest: string) { + try { + const manifestBuffer = ipfsClient.cat(manifest); + let manifestFile = ''; + for await (const chunk of manifestBuffer) { + manifestFile += Buffer.from(chunk).toString('utf8'); + } + return manifestFile; + } catch (e) { + utilsDebug.extend('loadManifestFromIPFS')(`Failed to load manifest from IPFS ${manifest}`); + utilsDebug.extend('loadManifestFromIPFS')(e); + throw Error(`Failed to load manifest from IPFS ${manifest}`); + } +} + +/** + * Validates that the network of a source subgraph matches the target network + * @param ipfsClient IPFS client instance + * @param sourceSubgraphId IPFS hash of the source subgraph + * @param targetNetwork Network of the target subgraph being created + * @returns Object containing validation result and error message if any + */ +export async function validateSubgraphNetworkMatch( + ipfsClient: any, + sourceSubgraphId: string, + targetNetwork: string, +): Promise<{ valid: boolean; error?: string }> { + try { + const manifestFile = await loadManifestFromIPFS(ipfsClient, sourceSubgraphId); + const manifestYaml = yaml.load(manifestFile) as any; + + // Extract network from data sources + const dataSources = manifestYaml.dataSources || []; + const templates = manifestYaml.templates || []; + const allSources = [...dataSources, ...templates]; + + if (allSources.length === 0) { + return { valid: true }; // No data sources to validate + } + + // Get network from first data source + const sourceNetwork = allSources[0].network; + if (!sourceNetwork) { + return { valid: true }; // Network not specified in source, skip validation + } + + const normalizedSourceNetwork = sourceNetwork.toLowerCase(); + const normalizedTargetNetwork = targetNetwork.toLowerCase(); + + if (normalizedSourceNetwork !== normalizedTargetNetwork) { + return { + valid: false, + 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.`, + }; + } + + return { valid: true }; + } catch (e) { + utilsDebug.extend('validateSubgraphNetworkMatch')(`Failed to validate network match: ${e}`); + return { + valid: false, + error: e instanceof Error ? e.message : 'Failed to validate subgraph network', + }; + } +}