Skip to content

Commit

Permalink
Fix graph init for composed subgraphs
Browse files Browse the repository at this point in the history
  • Loading branch information
incrypto32 committed Jan 23, 2025
1 parent 311861f commit 15b9df3
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 19 deletions.
47 changes: 41 additions & 6 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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),
});

Expand Down Expand Up @@ -589,9 +593,17 @@ async function processInitForm(
isSubstreams ||
(!protocolInstance.hasContract() && !isComposedSubgraph),
initial: initContract,
validate: async (value: string) => {
validate: async (value: string): Promise<string | boolean> => {
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;
Expand Down Expand Up @@ -706,7 +718,7 @@ async function processInitForm(
isSubstreams ||
!!initAbiPath ||
isComposedSubgraph,
validate: async (value: string) => {
validate: async (value: string): Promise<string | boolean> => {
if (
initFromExample ||
abiFromApi ||
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
60 changes: 47 additions & 13 deletions packages/cli/src/scaffold/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import debugFactory from 'debug';
import fs from 'fs-extra';
import { strings } from 'gluegun';
import prettier from 'prettier';
Expand All @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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',
}),
}
Expand Down
66 changes: 66 additions & 0 deletions packages/cli/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
}
}

0 comments on commit 15b9df3

Please sign in to comment.