Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/@graphprotocol_graph-cli-1893-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphprotocol/graph-cli": patch
---
dependencies updates:
- Removed dependency [`binary-install@^1.1.0` ↗︎](https://www.npmjs.com/package/binary-install/v/1.1.0) (from `dependencies`)
5 changes: 5 additions & 0 deletions .changeset/curly-buses-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphprotocol/graph-cli': minor
---

Add support for subgraph datasource in `graph init`
73 changes: 54 additions & 19 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@
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 @@
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 @@ -577,6 +581,19 @@
},
});

promptManager.addStep({
type: 'input',
name: 'ipfs',
message: `IPFS node to use for fetching subgraph manifest`,
initial: ipfsUrl,
skip: () => !isComposedSubgraph,
result: value => {
ipfsNode = value;
initDebugger.extend('processInitForm')('ipfs: %O', value);
return value;
},
});

promptManager.addStep({
type: 'input',
name: 'source',
Expand All @@ -589,9 +606,17 @@
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;
}
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 @@ -654,19 +679,6 @@
},
});

promptManager.addStep({
type: 'input',
name: 'ipfs',
message: `IPFS node to use for fetching subgraph manifest`,
initial: ipfsUrl,
skip: () => !isComposedSubgraph,
result: value => {
ipfsNode = value;
initDebugger.extend('processInitForm')('ipfs: %O', value);
return value;
},
});

promptManager.addStep({
type: 'input',
name: 'spkg',
Expand Down Expand Up @@ -706,7 +718,7 @@
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 @@

await promptManager.executeInteractive();

// Validate network matches if loading from IPFS
if (ipfsNode && source && source.startsWith('Qm')) {
const ipfs = createIpfsClient(ipfsNode);
try {
const { valid, error } = await validateSubgraphNetworkMatch(ipfs, source!, 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 All @@ -807,7 +835,7 @@
network: network.id,
contractName: contractName!,
source: source!,
indexEvents,

Check failure on line 838 in packages/cli/src/commands/init.ts

View workflow job for this annotation

GitHub Actions / Lint

Prefer using an optional chain expression instead, as it's more concise and easier to read
ipfs: ipfsNode,
spkgPath,
cleanup: spkgCleanup,
Expand Down Expand Up @@ -1163,8 +1191,9 @@
}

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 @@
`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
58 changes: 39 additions & 19 deletions packages/cli/src/compiler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,9 @@ export default class Compiler {
`Failed to write compiled subgraph to ${displayDir}`,
`Warnings while writing compiled subgraph to ${displayDir}`,
async spinner => {
// Add debug log for initial subgraph state
compilerDebug('Initial subgraph state:', subgraph.toJS());

// Copy schema and update its path
subgraph = subgraph.updateIn(['schema', 'file'], schemaFile => {
const schemaFilePath = path.resolve(this.sourceDir, schemaFile as string);
Expand All @@ -518,32 +521,49 @@ export default class Compiler {
return path.relative(this.options.outputDir, targetFile);
});

// Add debug log before processing data sources
compilerDebug('Processing dataSources:', subgraph.get('dataSources').toJS());

// Copy data source files and update their paths
subgraph = subgraph.update('dataSources', (dataSources: any[]) =>
dataSources.map(dataSource => {
// Add debug log for each data source
compilerDebug('Processing dataSource:', dataSource.toJS());

let updatedDataSource = dataSource;

if (this.protocol.hasABIs()) {
updatedDataSource = updatedDataSource
// Write data source ABIs to the output directory
.updateIn(['mapping', 'abis'], (abis: any[]) =>
abis.map((abi: any) =>
abi.update('file', (abiFile: string) => {
abiFile = path.resolve(this.sourceDir, abiFile);
const abiData = this.ABI.load(abi.get('name'), abiFile);
return path.relative(
this.options.outputDir,
this._writeSubgraphFile(
abiFile,
JSON.stringify(abiData.data.toJS(), null, 2),
this.sourceDir,
this.subgraphDir(this.options.outputDir, dataSource),
spinner,
),
);
}),
),
// Add debug log for ABIs
compilerDebug(
'Processing ABIs for dataSource:',
dataSource.getIn(['mapping', 'abis'])?.toJS() || 'undefined',
);

updatedDataSource = updatedDataSource.updateIn(['mapping', 'abis'], (abis: any[]) => {
compilerDebug('ABIs value:', Array.isArray(abis) ? abis : 'undefined');

if (!abis) {
compilerDebug('No ABIs found for dataSource');
return immutable.List();
}

return abis.map((abi: any) =>
abi.update('file', (abiFile: string) => {
abiFile = path.resolve(this.sourceDir, abiFile);
const abiData = this.ABI.load(abi.get('name'), abiFile);
return path.relative(
this.options.outputDir,
this._writeSubgraphFile(
abiFile,
JSON.stringify(abiData.data.toJS(), null, 2),
this.sourceDir,
this.subgraphDir(this.options.outputDir, dataSource),
spinner,
),
);
}),
);
});
}

if (protocol.name == 'substreams' || protocol.name == 'substreams/triggers') {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/protocols/subgraph/manifest.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ type ContractABI {
type EntityHandler {
handler: String!
entity: String!
calls: JSON
}

type Graft {
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',
};
}
}
Loading