Skip to content

Commit 71d9a97

Browse files
committed
subgraph: Divide logic between protocols
1 parent 5d7adb4 commit 71d9a97

File tree

7 files changed

+299
-203
lines changed

7 files changed

+299
-203
lines changed

src/compiler/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ class Compiler {
106106
}
107107

108108
async loadSubgraph({ quiet } = { quiet: false }) {
109+
const subgraphLoadOptions = { protocol: this.protocol, skipValidation: false }
110+
109111
if (quiet) {
110-
return Subgraph.load(this.options.subgraphManifest).result
112+
return Subgraph.load(this.options.subgraphManifest, subgraphLoadOptions).result
111113
} else {
112114
const manifestPath = this.displayPath(this.options.subgraphManifest)
113115

@@ -116,7 +118,7 @@ class Compiler {
116118
`Failed to load subgraph from ${manifestPath}`,
117119
`Warnings loading subgraph from ${manifestPath}`,
118120
async spinner => {
119-
return Subgraph.load(this.options.subgraphManifest)
121+
return Subgraph.load(this.options.subgraphManifest, subgraphLoadOptions)
120122
},
121123
)
122124
}

src/protocols/ethereum/subgraph.js

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
const immutable = require('immutable')
2+
const ABI = require('./abi')
3+
const Subgraph = require('../../subgraph')
4+
5+
module.exports = class EthereumSubgraph {
6+
constructor(options = {}) {
7+
this.manifest = options.manifest
8+
this.resolveFile = options.resolveFile
9+
}
10+
11+
validateManifest() {
12+
return this.validateAbis()
13+
.concat(this.validateContractAddresses())
14+
.concat(this.validateEvents())
15+
.concat(this.validateCallFunctions())
16+
}
17+
18+
validateAbis() {
19+
let dataSources = Subgraph.collectDataSources(this.manifest, 'ethereum/contract')
20+
let dataSourceTemplates = Subgraph.collectDataSourceTemplates(this.manifest, 'ethereum/contract')
21+
22+
return dataSources.concat(dataSourceTemplates).reduce(
23+
(errors, dataSourceOrTemplate) =>
24+
errors.concat(
25+
this.validateDataSourceAbis(
26+
dataSourceOrTemplate.get('dataSource'),
27+
dataSourceOrTemplate.get('path'),
28+
),
29+
),
30+
immutable.List(),
31+
)
32+
}
33+
34+
validateDataSourceAbis(dataSource, path) {
35+
// Validate that the the "source > abi" reference of all data sources
36+
// points to an existing ABI in the data source ABIs
37+
let abiName = dataSource.getIn(['source', 'abi'])
38+
let abiNames = dataSource.getIn(['mapping', 'abis']).map(abi => abi.get('name'))
39+
let nameErrors = abiNames.includes(abiName)
40+
? immutable.List()
41+
: immutable.fromJS([
42+
{
43+
path: [...path, 'source', 'abi'],
44+
message: `\
45+
ABI name '${abiName}' not found in mapping > abis.
46+
Available ABIs:
47+
${abiNames
48+
.sort()
49+
.map(name => `- ${name}`)
50+
.join('\n')}`,
51+
},
52+
])
53+
54+
// Validate that all ABI files are valid
55+
let fileErrors = dataSource
56+
.getIn(['mapping', 'abis'])
57+
.reduce((errors, abi, abiIndex) => {
58+
try {
59+
ABI.load(abi.get('name'), this.resolveFile(abi.get('file')))
60+
return errors
61+
} catch (e) {
62+
return errors.push(
63+
immutable.fromJS({
64+
path: [...path, 'mapping', 'abis', abiIndex, 'file'],
65+
message: e.message,
66+
}),
67+
)
68+
}
69+
}, immutable.List())
70+
71+
return nameErrors.concat(fileErrors)
72+
}
73+
74+
validateContractAddresses() {
75+
const ethereumAddressPattern = /^(0x)?[0-9a-fA-F]{40}$/
76+
77+
return Subgraph.validateContractAddresses(
78+
this.manifest,
79+
'ethereum/contract',
80+
address => ethereumAddressPattern.test(address),
81+
"Must be 40 hexadecimal characters, with an optional '0x' prefix.",
82+
)
83+
}
84+
85+
validateEvents() {
86+
let dataSources = Subgraph.collectDataSources(this.manifest, 'ethereum/contract')
87+
let dataSourceTemplates = Subgraph.collectDataSourceTemplates(this.manifest, 'ethereum/contract')
88+
89+
return dataSources
90+
.concat(dataSourceTemplates)
91+
.reduce((errors, dataSourceOrTemplate) => {
92+
return errors.concat(
93+
this.validateDataSourceEvents(
94+
dataSourceOrTemplate.get('dataSource'),
95+
dataSourceOrTemplate.get('path'),
96+
),
97+
)
98+
}, immutable.List())
99+
}
100+
101+
validateDataSourceEvents(dataSource, path) {
102+
let abi
103+
try {
104+
// Resolve the source ABI name into a real ABI object
105+
let abiName = dataSource.getIn(['source', 'abi'])
106+
let abiEntry = dataSource
107+
.getIn(['mapping', 'abis'])
108+
.find(abi => abi.get('name') === abiName)
109+
abi = ABI.load(abiEntry.get('name'), this.resolveFile(abiEntry.get('file')))
110+
} catch (_) {
111+
// Ignore errors silently; we can't really say anything about
112+
// the events if the ABI can't even be loaded
113+
return immutable.List()
114+
}
115+
116+
// Obtain event signatures from the mapping
117+
let manifestEvents = dataSource
118+
.getIn(['mapping', 'eventHandlers'], immutable.List())
119+
.map(handler => handler.get('event'))
120+
121+
// Obtain event signatures from the ABI
122+
let abiEvents = abi.eventSignatures()
123+
124+
// Add errors for every manifest event signature that is not
125+
// present in the ABI
126+
return manifestEvents.reduce(
127+
(errors, manifestEvent, index) =>
128+
abiEvents.includes(manifestEvent)
129+
? errors
130+
: errors.push(
131+
immutable.fromJS({
132+
path: [...path, 'eventHandlers', index],
133+
message: `\
134+
Event with signature '${manifestEvent}' not present in ABI '${abi.name}'.
135+
Available events:
136+
${abiEvents
137+
.sort()
138+
.map(event => `- ${event}`)
139+
.join('\n')}`,
140+
}),
141+
),
142+
immutable.List(),
143+
)
144+
}
145+
146+
validateCallFunctions() {
147+
return this.manifest
148+
.get('dataSources')
149+
.filter(dataSource => dataSource.get('kind') === 'ethereum/contract')
150+
.reduce((errors, dataSource, dataSourceIndex) => {
151+
let path = ['dataSources', dataSourceIndex, 'callHandlers']
152+
153+
let abi
154+
try {
155+
// Resolve the source ABI name into a real ABI object
156+
let abiName = dataSource.getIn(['source', 'abi'])
157+
let abiEntry = dataSource
158+
.getIn(['mapping', 'abis'])
159+
.find(abi => abi.get('name') === abiName)
160+
abi = ABI.load(abiEntry.get('name'), this.resolveFile(abiEntry.get('file')))
161+
} catch (e) {
162+
// Ignore errors silently; we can't really say anything about
163+
// the call functions if the ABI can't even be loaded
164+
return errors
165+
}
166+
167+
// Obtain event signatures from the mapping
168+
let manifestFunctions = dataSource
169+
.getIn(['mapping', 'callHandlers'], immutable.List())
170+
.map(handler => handler.get('function'))
171+
172+
// Obtain event signatures from the ABI
173+
let abiFunctions = abi.callFunctionSignatures()
174+
175+
// Add errors for every manifest event signature that is not
176+
// present in the ABI
177+
return manifestFunctions.reduce(
178+
(errors, manifestFunction, index) =>
179+
abiFunctions.includes(manifestFunction)
180+
? errors
181+
: errors.push(
182+
immutable.fromJS({
183+
path: [...path, index],
184+
message: `\
185+
Call function with signature '${manifestFunction}' not present in ABI '${abi.name}'.
186+
Available call functions:
187+
${abiFunctions
188+
.sort()
189+
.map(tx => `- ${tx}`)
190+
.join('\n')}`,
191+
}),
192+
),
193+
errors,
194+
)
195+
}, immutable.List())
196+
}
197+
198+
handlerTypes() {
199+
return immutable.List([
200+
'blockHandlers',
201+
'callHandlers',
202+
'eventHandlers',
203+
])
204+
}
205+
}

src/protocols/index.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const EthereumTypeGenerator = require('./ethereum/type-generator')
22
const EthereumTemplateCodeGen = require('./ethereum/codegen/template')
33
const NearTemplateCodeGen = require('./near/codegen/template')
44
const EthereumABI = require('./ethereum/abi')
5+
const EthereumSubgraph = require('./ethereum/subgraph')
6+
const NearSubgraph = require('./near/subgraph')
57

68
module.exports = class Protocol {
79
static fromDataSources(dataSourcesAndTemplates) {
@@ -56,4 +58,14 @@ module.exports = class Protocol {
5658
return null
5759
}
5860
}
61+
62+
getSubgraph(options) {
63+
switch (this.name) {
64+
case 'ethereum':
65+
case 'ethereum/contract':
66+
return new EthereumSubgraph(options)
67+
case 'near':
68+
return new NearSubgraph(options)
69+
}
70+
}
5971
}

src/protocols/near/subgraph.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const immutable = require('immutable')
2+
const Subgraph = require('../../subgraph')
3+
4+
module.exports = class NearSubgraph {
5+
constructor(options = {}) {
6+
this.manifest = options.manifest
7+
this.resolveFile = options.resolveFile
8+
}
9+
10+
validateManifest() {
11+
return this.validateContractAddresses()
12+
}
13+
14+
validateContractAddresses() {
15+
// Reference: https://docs.near.org/docs/concepts/account#account-id-rules
16+
const MINIMUM_ACCOUNT_ID_LENGTH = 2
17+
const MAXIMUM_ACCOUNT_ID_LENGTH = 64
18+
const validateLength = accountId =>
19+
accountId.length >= MINIMUM_ACCOUNT_ID_LENGTH &&
20+
accountId.length <= MAXIMUM_ACCOUNT_ID_LENGTH
21+
const nearAccountIdPattern = /^(([a-z\d]+[\-_])*[a-z\d]+\.)*([a-z\d]+[\-_])*[a-z\d]+$/
22+
23+
return Subgraph.validateContractAddresses(
24+
this.manifest,
25+
'near',
26+
accountId => validateLength(accountId) && nearAccountIdPattern.test(accountId),
27+
`Must be between '${MINIMUM_ACCOUNT_ID_LENGTH}' and '${MAXIMUM_ACCOUNT_ID_LENGTH}' characters
28+
An Account ID consists of Account ID parts separated by '.' (dots)
29+
Each Account ID part consists of lowercase alphanumeric symbols separated by either a '_' (underscore) or '-' (dash)
30+
For further information look for: https://docs.near.org/docs/concepts/account#account-id-rules`,
31+
)
32+
}
33+
34+
handlerTypes() {
35+
return immutable.List([
36+
'blockHandlers',
37+
'receiptHandlers',
38+
'functionCallHandlers',
39+
])
40+
}
41+
}

0 commit comments

Comments
 (0)