Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subgraph Composition : Fix graph init for composed subgraphs #1920

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fddfea6
Fix subgraphs without abi field failing to build
incrypto32 Jan 5, 2025
6b9f04b
Fix graph init for composed subgraphs
incrypto32 Jan 23, 2025
b065646
Add changeset
incrypto32 Jan 23, 2025
735b565
Fix validation not working
incrypto32 Jan 24, 2025
600280d
Support declared calls in manifest
incrypto32 Jan 24, 2025
3a75a7f
Lint fix
incrypto32 Jan 27, 2025
a074696
Address review comments
incrypto32 Feb 3, 2025
ce24548
Dont allow adding new contracts when subgraph is a composed subgraph
incrypto32 Feb 3, 2025
b7b6bbf
Allow init of subgraph datasource subgraphs without the interactive mode
incrypto32 Feb 4, 2025
2ae4c2e
Reduce code duplication between subgraph datasource and normal data s…
incrypto32 Feb 4, 2025
9ed22cf
prevent using --from-contract and --from-source-subgraph flags together
incrypto32 Feb 4, 2025
7683939
cli: validate protocol and source subgraph relationship
incrypto32 Feb 4, 2025
40dbe07
chore(dependencies): updated changesets for modified dependencies
github-actions[bot] Feb 4, 2025
ff2430c
change flag name for source subgraph
incrypto32 Feb 6, 2025
08cfdd1
Refactor manifest validation util functions
incrypto32 Feb 6, 2025
808d02f
get start block from source manifest
incrypto32 Feb 6, 2025
e5ea2bd
set fromSubgraph to be default value for graph init in interactive mode
incrypto32 Feb 6, 2025
4edbedc
Merge branch 'krishna/comp-fixes' of github.com:graphprotocol/graph-t…
incrypto32 Feb 6, 2025
7e95705
fix protocol flag validation
YaroShkvorets Feb 6, 2025
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
6 changes: 6 additions & 0 deletions .changeset/@graphprotocol_graph-cli-1920-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphprotocol/graph-cli": patch
---
dependencies updates:
- Updated dependency [`@oclif/[email protected]` ↗︎](https://www.npmjs.com/package/@oclif/core/v/4.2.6) (from `4.2.4`, in `dependencies`)
- Updated dependency [`[email protected]` ↗︎](https://www.npmjs.com/package/semver/v/7.7.1) (from `7.6.3`, in `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`
1 change: 0 additions & 1 deletion packages/cli/src/commands/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export default class CodegenCommand extends Command {
summary: 'IPFS node to use for fetching subgraph data.',
char: 'i',
default: DEFAULT_IPFS_URL,
hidden: true,
}),
'uncrashable-config': Flags.file({
summary: 'Directory for uncrashable config.',
Expand Down
148 changes: 113 additions & 35 deletions packages/cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ 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,
getMinStartBlock,
loadManifestYaml,
loadSubgraphSchemaFromIPFS,
validateSubgraphNetworkMatch,
} from '../utils.js';
import { validateContract } from '../validation/index.js';
import AddCommand from './add.js';

Expand Down Expand Up @@ -54,6 +60,10 @@ export default class InitCommand extends Command {
summary: 'Graph node for which to initialize.',
char: 'g',
}),
'from-subgraph': Flags.string({
description: 'Creates a scaffold based on an existing subgraph.',
exclusive: ['from-example', 'from-contract'],
}),
'from-contract': Flags.string({
description: 'Creates a scaffold based on an existing contract.',
exclusive: ['from-example'],
Expand Down Expand Up @@ -88,7 +98,6 @@ export default class InitCommand extends Command {
description: 'Block number to start indexing from.',
// TODO: using a default sets the value and therefore requires --from-contract
// default: '0',
dependsOn: ['from-contract'],
}),

abi: Flags.string({
Expand All @@ -110,7 +119,6 @@ export default class InitCommand extends Command {
summary: 'IPFS node to use for fetching subgraph data.',
char: 'i',
default: DEFAULT_IPFS_URL,
hidden: true,
}),
};

Expand All @@ -124,6 +132,7 @@ export default class InitCommand extends Command {
protocol,
node: nodeFlag,
'from-contract': fromContract,
'from-subgraph': fromSubgraph,
'contract-name': contractName,
'from-example': fromExample,
'index-events': indexEvents,
Expand All @@ -138,11 +147,20 @@ export default class InitCommand extends Command {

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

if (startBlock && !(fromContract || fromSubgraph)) {
this.error('--start-block can only be used with --from-contract or --from-subgraph');
}

if (fromContract && fromSubgraph) {
this.error('Cannot use both --from-contract and --from-subgraph at the same time');
}

if (skipGit) {
this.warn(
'The --skip-git flag will be removed in the next major version. By default we will stop initializing a Git repository.',
);
}

if ((fromContract || spkgPath) && !network && !fromExample) {
this.error('--network is required when using --from-contract or --spkg');
}
Expand Down Expand Up @@ -196,16 +214,15 @@ export default class InitCommand extends Command {
let abi!: EthereumABI;

// If all parameters are provided from the command-line,
// go straight to creating the subgraph from an existing contract
if ((fromContract || spkgPath) && protocol && subgraphName && directory && network && node) {
const registry = await loadRegistry();
const contractService = new ContractService(registry);
const sourcifyContractInfo = await contractService.getFromSourcify(
EthereumABI,
network,
fromContract!,
);

// go straight to creating the subgraph from an existing contract or source subgraph
if (
(fromContract || spkgPath || fromSubgraph) &&
protocol &&
subgraphName &&
directory &&
network &&
node
) {
if (!protocolChoices.includes(protocol as ProtocolName)) {
this.error(
`Protocol '${protocol}' is not supported, choose from these options: ${protocolChoices.join(
Expand All @@ -217,7 +234,31 @@ export default class InitCommand extends Command {

const protocolInstance = new Protocol(protocol as ProtocolName);

if (protocolInstance.hasABIs()) {
if (fromSubgraph && !protocolInstance.isComposedSubgraph()) {
this.error('--protocol can only be subgraph when using --from-subgraph');
}

if (
fromContract &&
(protocolInstance.isComposedSubgraph() || protocolInstance.isSubstreams())
) {
this.error('--protocol cannot be subgraph or substreams when using --from-contract');
}

if (spkgPath && !protocolInstance.isSubstreams()) {
this.error('--protocol can only be substreams when using --spkg');
}

// Only fetch contract info and ABI for non-source-subgraph cases
if (!fromSubgraph && protocolInstance.hasABIs()) {
const registry = await loadRegistry();
const contractService = new ContractService(registry);
const sourcifyContractInfo = await contractService.getFromSourcify(
EthereumABI,
network,
fromContract!,
);

const ABI = protocolInstance.getABI();
if (abiPath) {
try {
Expand All @@ -241,7 +282,7 @@ export default class InitCommand extends Command {
protocolInstance,
abi,
directory,
source: fromContract!,
source: fromSubgraph || fromContract!,
indexEvents,
network,
subgraphName,
Expand Down Expand Up @@ -285,7 +326,7 @@ export default class InitCommand extends Command {
abi,
abiPath,
directory,
source: fromContract,
source: fromContract || fromSubgraph,
indexEvents,
fromExample,
subgraphName,
Expand Down Expand Up @@ -515,7 +556,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 @@ -584,6 +625,30 @@ async function processInitForm(
},
});

promptManager.addStep({
type: 'input',
name: 'ipfs',
message: `IPFS node to use for fetching subgraph manifest`,
initial: ipfsUrl,
skip: () => !isComposedSubgraph,
validate: value => {
if (!value) {
return 'IPFS node URL cannot be empty';
}
try {
new URL(value);
return true;
} catch {
return 'Please enter a valid URL';
}
},
result: value => {
ipfsNode = value;
YaroShkvorets marked this conversation as resolved.
Show resolved Hide resolved
initDebugger.extend('processInitForm')('ipfs: %O', value);
return value;
},
});

promptManager.addStep({
type: 'input',
name: 'source',
Expand All @@ -596,9 +661,16 @@ async function processInitForm(
isSubstreams ||
(!protocolInstance.hasContract() && !isComposedSubgraph),
initial: initContract,
YaroShkvorets marked this conversation as resolved.
Show resolved Hide resolved
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';
const ipfs = createIpfsClient(ipfsNode);
const manifestYaml = await loadManifestYaml(ipfs, value);
const { valid, error } = validateSubgraphNetworkMatch(manifestYaml, network.id);
if (!valid) {
return error || 'Invalid subgraph network match';
}
startBlock ||= getMinStartBlock(manifestYaml)?.toString();
return true;
}
if (initFromExample !== undefined || !protocolInstance.hasContract()) {
return true;
Expand Down Expand Up @@ -648,6 +720,7 @@ async function processInitForm(
} else {
abiFromApi = initAbi;
}

// If startBlock is not provided, try to fetch it from Etherscan API
if (!initStartBlock) {
startBlock = await retryWithPrompt(() =>
Expand Down Expand Up @@ -679,19 +752,6 @@ async function processInitForm(
},
});

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 @@ -731,7 +791,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 @@ -1179,17 +1239,29 @@ async function initSubgraphFromContract(
},
});

// Validate network match first
const manifestYaml = await loadManifestYaml(ipfsClient, source);
const { valid, error } = validateSubgraphNetworkMatch(manifestYaml, network);
if (!valid) {
throw new Error(error || 'Invalid subgraph network match');
}

startBlock ||= getMinStartBlock(manifestYaml)?.toString();
const schemaString = await loadSubgraphSchemaFromIPFS(ipfsClient, source);
const schema = await Schema.loadFromString(schemaString);
entities = schema.getEntityNames();
} catch (e) {
if (e instanceof Error) {
print.error(`Failed to validate subgraph: ${e?.message}`);
}
this.error(`Failed to load and parse subgraph schema: ${e.message}`, { exit: 1 });
}
}

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 @@ -1204,6 +1276,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 Expand Up @@ -1260,7 +1338,7 @@ async function initSubgraphFromContract(
this.exit(1);
}

while (addContract) {
while (addContract && !isComposedSubgraph) {
addContract = await addAnotherContract
.bind(this)({
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
Loading