diff --git a/manifest-schema.graphql b/manifest-schema.graphql index b01ead799..837994f71 100644 --- a/manifest-schema.graphql +++ b/manifest-schema.graphql @@ -42,6 +42,7 @@ type EthereumContractAbi { type EthereumBlockHandler { handler: String! + blockFormat: String filter: EthereumBlockFilter } diff --git a/src/subgraph.js b/src/subgraph.js index 53e402ab6..32ab3cbef 100644 --- a/src/subgraph.js +++ b/src/subgraph.js @@ -6,6 +6,7 @@ let { strOptions } = require('yaml/types') let graphql = require('graphql/language') let validation = require('./validation') let ABI = require('./abi') +let asc = require('assemblyscript') const throwCombinedError = (filename, errors) => { throw new Error( @@ -23,6 +24,13 @@ const throwCombinedError = (filename, errors) => { ) } +// Define conversions from a 'blockFormat' value in the manifest to its corresponding struct name +const blockFormatToStructName = immutable.Map({ + 'block-only': 'Block', + 'block-with-transactions': 'BlockWithTransactions', + 'block-with-receipts': 'BlockWithReceipts', +}) + const buildCombinedWarning = (filename, warnings) => warnings.size > 0 ? warnings.reduce( @@ -317,6 +325,91 @@ ${abiFunctions }, immutable.List()) } + static validateBlockFunctions(manifest, { resolveFile }) { + return manifest + .get('dataSources') + .filter( + dataSource => + dataSource.get('kind') === 'ethereum/contract' && + dataSource.getIn(['mapping', 'blockHandlers'], immutable.List()).count() > 0, + ) + .reduce((errors, dataSource, dataSourceIndex) => { + let path = ['dataSources', dataSourceIndex, 'blockHandlers'] + // Use the Assemblyscript parser to generate an AST from the mapping file + let mappingFile = dataSource.getIn(['mapping', 'file']) + let mappingParser = new asc.Parser() + mappingParser.parseFile( + fs.readFileSync(resolveFile(mappingFile), 'utf-8'), + '', + false, + ) + + let blockHandlers = dataSource.getIn( + ['mapping', 'blockHandlers'], + immutable.List(), + ) + + // Ensure each blockHandler has a corresponding mapping handler + // with a compatible function signature and uses a supported `blockFormat` value. + return errors.concat( + blockHandlers.reduce( + (errors, handler, index) => + mappingParser.program.sources + .filter(source => source.kind == asc.SourceKind.DEFAULT) + .some(source => + source.statements + .filter( + statement => statement.kind === asc.NodeKind.FUNCTIONDECLARATION, + ) + .some( + functionDeclaration => + functionDeclaration.name.text === handler.get('handler') && + functionDeclaration.signature.parameters.length === 1 && + functionDeclaration.signature.parameters[0].type.name.identifier + .text === 'ethereum' && + functionDeclaration.signature.parameters[0].type.name.next + .identifier.text === + blockFormatToStructName.get( + handler.get('blockFormat', 'block-only'), + ) && + functionDeclaration.signature.parameters[0].type.name.next + .next === null && + functionDeclaration.signature.returnType.name.identifier.text === + 'void', + ), + ) + ? errors + : blockFormatToStructName.get(handler.get('blockFormat', 'block-only')) + ? errors.push( + immutable.fromJS({ + path: [...path, index], + message: `\ +Matching mapping handler not found in '${mappingFile}' for blockHandler: '${handler.get( + 'handler', + )}'. +Signature: + ${handler.get('handler')}(block: ethereum.${blockFormatToStructName.get( + handler.get('blockFormat', 'block-only'), + )}): void`, + }), + ) + : errors.push( + immutable.fromJS({ + path: [...path, index], + message: `Unsupported blockFormat, '${handler.get( + 'blockFormat', + )}', specified for the '${handler.get('handler')}' blockHandler. +Please use one of the supported blockFormats: ${JSON.stringify( + blockFormatToStructName.keySeq(), + )}`, + }), + ), + immutable.List(), + ), + ) + }, immutable.List()) + } + static validateRepository(manifest, { resolveFile }) { return manifest.get('repository') !== 'https://github.com/graphprotocol/example-subgraph' @@ -446,6 +539,7 @@ More than one template named '${name}', template names must be unique.`, ...Subgraph.validateEthereumContractHandlers(manifest), ...Subgraph.validateEvents(manifest, { resolveFile }), ...Subgraph.validateCallFunctions(manifest, { resolveFile }), + ...Subgraph.validateBlockFunctions(manifest, { resolveFile }), ...Subgraph.validateUniqueDataSourceNames(manifest), ...Subgraph.validateUniqueTemplateNames(manifest), ) diff --git a/tests/cli/validation.test.js b/tests/cli/validation.test.js index f1947cd92..02c5d9e95 100644 --- a/tests/cli/validation.test.js +++ b/tests/cli/validation.test.js @@ -197,4 +197,13 @@ describe('Validation', () => { exitCode: 1, }, ) + + cliTest( + 'Block handler with incompatible mapping function', + ['codegen', '--skip-migrations'], + 'validation/block-handler-with-incompatible-mapping-function', + { + exitCode: 1, + }, + ) }) diff --git a/tests/cli/validation/block-handler-with-incompatible-mapping-function.stderr b/tests/cli/validation/block-handler-with-incompatible-mapping-function.stderr new file mode 100644 index 000000000..34cafba50 --- /dev/null +++ b/tests/cli/validation/block-handler-with-incompatible-mapping-function.stderr @@ -0,0 +1,7 @@ +- Load subgraph from subgraph.yaml +✖ Failed to load subgraph from subgraph.yaml: Error in subgraph.yaml: + + Path: dataSources > 0 > blockHandlers > 0 + Matching mapping handler not found in './mapping.ts' for blockHandler: 'handleBlock'. + Signature: + handleBlock(block: ethereum.BlockWithTransactions): void diff --git a/tests/cli/validation/block-handler-with-incompatible-mapping-function/Abi.json b/tests/cli/validation/block-handler-with-incompatible-mapping-function/Abi.json new file mode 100644 index 000000000..4d05f5839 --- /dev/null +++ b/tests/cli/validation/block-handler-with-incompatible-mapping-function/Abi.json @@ -0,0 +1,7 @@ +[ + { + "type": "event", + "name": "ExampleEvent", + "inputs": [{ "type": "string" }] + } +] diff --git a/tests/cli/validation/block-handler-with-incompatible-mapping-function/mapping.ts b/tests/cli/validation/block-handler-with-incompatible-mapping-function/mapping.ts new file mode 100644 index 000000000..55376e559 --- /dev/null +++ b/tests/cli/validation/block-handler-with-incompatible-mapping-function/mapping.ts @@ -0,0 +1,9 @@ +import { ethereum } from '@graphprotocol/graph-ts' +import { ExampleBlockEntity } from './generated/schema' + +export function handleBlock(block: ethereum.BlockWithReceipts): void { + let entity = new ExampleBlockEntity(block.hash.toHexString()) + entity.number = block.number + entity.hash = block.hash + entity.save() +} \ No newline at end of file diff --git a/tests/cli/validation/block-handler-with-incompatible-mapping-function/schema.graphql b/tests/cli/validation/block-handler-with-incompatible-mapping-function/schema.graphql new file mode 100644 index 000000000..96ba50b79 --- /dev/null +++ b/tests/cli/validation/block-handler-with-incompatible-mapping-function/schema.graphql @@ -0,0 +1,5 @@ +type ExampleBlockEntity @entity { + id: ID! + number: BigInt! + hash: Bytes! +} diff --git a/tests/cli/validation/block-handler-with-incompatible-mapping-function/subgraph.yaml b/tests/cli/validation/block-handler-with-incompatible-mapping-function/subgraph.yaml new file mode 100644 index 000000000..66d422fe0 --- /dev/null +++ b/tests/cli/validation/block-handler-with-incompatible-mapping-function/subgraph.yaml @@ -0,0 +1,22 @@ +specVersion: 0.0.1 +schema: + file: ./schema.graphql +dataSources: +- kind: ethereum/contract + name: ExampleSubgraph + network: mainnet + source: + abi: ExampleContract + mapping: + kind: ethereum/events + apiVersion: 0.0.1 + language: wasm/assemblyscript + file: ./mapping.ts + entities: + - ExampleBlockEntity + abis: + - name: ExampleContract + file: ./Abi.json + blockHandlers: + - handler: handleBlock + blockFormat: block-with-transactions \ No newline at end of file