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

Support different input types to block handlers, validate manifest and mapping functions #516

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions manifest-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type EthereumContractAbi {

type EthereumBlockHandler {
handler: String!
blockFormat: String
filter: EthereumBlockFilter
}

Expand Down
94 changes: 94 additions & 0 deletions src/subgraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we now have a separation of naming convention between blockFormat values in the manifest and their corresponding struct names, I've included a mapping between the two here. This gets used in validateBlockFunctions() to ensure the data structure used in the mapping matches what was defined in the manifest.

'block-only': 'Block',
'block-with-transactions': 'BlockWithTransactions',
'block-with-receipts': 'BlockWithReceipts',
})

const buildCombinedWarning = (filename, warnings) =>
warnings.size > 0
? warnings.reduce(
Expand Down Expand Up @@ -317,6 +325,91 @@ ${abiFunctions
}, immutable.List())
}

static validateBlockFunctions(manifest, { resolveFile }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot how hard it is to review JS code, thanks for the reminder.

Does dataSource really have a .getIn method? Sure! One must assume.

Also, it's interesting to see what looks like a mature and fully-fledged persistent immutable data structure library in JS.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, immutable.js is rarely seen in the wild but it is mature. Unfortunately, it's rather unidiomatic, hence a lot of graph-cli is a little... odd.

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'
Expand Down Expand Up @@ -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),
)
Expand Down
9 changes: 9 additions & 0 deletions tests/cli/validation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
)
})
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "event",
"name": "ExampleEvent",
"inputs": [{ "type": "string" }]
}
]
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type ExampleBlockEntity @entity {
id: ID!
number: BigInt!
hash: Bytes!
}
Original file line number Diff line number Diff line change
@@ -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