-
Notifications
You must be signed in to change notification settings - Fork 216
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
base: main
Are you sure you want to change the base?
Changes from all commits
fddfea6
6b9f04b
b065646
735b565
600280d
3a75a7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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` |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
||
|
@@ -515,7 +519,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), | ||
}); | ||
|
||
|
@@ -584,6 +588,19 @@ 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: 'source', | ||
|
@@ -596,9 +613,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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't allow empty |
||
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; | ||
|
@@ -679,19 +704,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', | ||
|
@@ -731,7 +743,7 @@ async function processInitForm( | |
isSubstreams || | ||
!!initAbiPath || | ||
isComposedSubgraph, | ||
validate: async (value: string) => { | ||
validate: async (value: string): Promise<string | boolean> => { | ||
if ( | ||
initFromExample || | ||
abiFromApi || | ||
|
@@ -822,6 +834,22 @@ async function processInitForm( | |
|
||
await promptManager.executeInteractive(); | ||
|
||
// Validate network matches if loading from IPFS | ||
if (ipfsNode && source?.startsWith('Qm')) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not check for I think we can move this validation into |
||
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, | ||
|
@@ -1188,8 +1216,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) | ||
|
@@ -1204,6 +1233,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, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -63,6 +63,7 @@ type ContractABI { | |
type EntityHandler { | ||
handler: String! | ||
entity: String! | ||
calls: JSON | ||
} | ||
|
||
type Graft { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible? |
||
} | ||
|
||
const normalizedSourceNetwork = sourceNetwork.toLowerCase(); | ||
const normalizedTargetNetwork = targetNetwork.toLowerCase(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to normalize here. If somehow our |
||
|
||
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', | ||
}; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's check that this is at least a valid URL