@graphprotocol/graph-cli 0.31.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,7 @@ As of today, the command line interface supports the following commands:
16
16
  - `graph auth` — Stores a [Graph Node](https://github.com/graphprotocol/graph-node) access token in the system's keychain.
17
17
  - `graph local` — Runs tests against a [Graph Node](https://github.com/graphprotocol/graph-node) test environment (using Ganache by default).
18
18
  - `graph test` — Downloads and runs the [Matchstick](https://github.com/LimeChain/matchstick) rust binary in order to test a subgraph.
19
+ - `graph add` - Adds a new datasource to the yaml file and writes the necessary changes to other files - schema.graphql, abi and mapping.
19
20
 
20
21
  ## How It Works
21
22
 
@@ -49,9 +50,9 @@ yarn global add @graphprotocol/graph-cli
49
50
 
50
51
  ## Getting Started
51
52
 
52
- The Graph CLI can be used with a local or self-hosted [Graph Node](https://github.com/graphprotocol/graph-node) or with the [Hosted Service](https://thegraph.com/explorer/). To help you get going, there are [quick start guides](https://thegraph.com/docs/quick-start) available for both.
53
+ The Graph CLI can be used with a local or self-hosted [Graph Node](https://github.com/graphprotocol/graph-node) or with the [Hosted Service](https://thegraph.com/explorer/). To help you get going, there are [quick start guides](https://thegraph.com/docs/en/developer/quick-start/) available for both.
53
54
 
54
- If you are ready to dive into the details of building a subgraph from scratch, there is a [detailed walkthrough](https://thegraph.com/docs/define-a-subgraph) for that as well, along with API documentation for the [AssemblyScript API](https://thegraph.com/docs/assemblyscript-api).
55
+ If you are ready to dive into the details of building a subgraph from scratch, there is a [detailed walkthrough](https://thegraph.com/docs/en/developer/create-subgraph-hosted/) for that as well, along with API documentation for the [AssemblyScript API](https://thegraph.com/docs/en/developer/assemblyscript-api/).
55
56
 
56
57
  ## Release process
57
58
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphprotocol/graph-cli",
3
- "version": "0.31.0",
3
+ "version": "0.33.1",
4
4
  "license": "(Apache-2.0 OR MIT)",
5
5
  "description": "CLI for building for and deploying to The Graph",
6
6
  "dependencies": {
@@ -1,11 +1,18 @@
1
1
  const immutable = require('immutable')
2
+ const IpfsFileTemplateCodeGen = require('../protocols/ipfs/codegen/file_template')
2
3
 
3
4
  const tsCodegen = require('./typescript')
4
5
 
5
6
  module.exports = class DataSourceTemplateCodeGenerator {
6
7
  constructor(template, protocol) {
7
8
  this.template = template
8
- this.protocolTemplateCodeGen = protocol.getTemplateCodeGen(template)
9
+ let kind = template.get('kind')
10
+
11
+ if (kind.split('/')[0] == protocol.name) {
12
+ this.protocolTemplateCodeGen = protocol.getTemplateCodeGen(template)
13
+ } else if (kind == "file/ipfs") {
14
+ this.protocolTemplateCodeGen = new IpfsFileTemplateCodeGen(template)
15
+ }
9
16
  }
10
17
 
11
18
  generateModuleImports() {
@@ -143,9 +143,9 @@ const ASSEMBLYSCRIPT_TO_ETHEREUM_VALUE = [
143
143
  /^string\[([0-9]+)?\]$/,
144
144
  code => `ethereum.Value.fromStringArray(${code})`,
145
145
  ],
146
- ['Tuple', 'tuple', code => `ethereum.Value.fromTuple(${code})`],
146
+ ['ethereum.Tuple', 'tuple', code => `ethereum.Value.fromTuple(${code})`],
147
147
  [
148
- 'Array<Tuple>',
148
+ 'Array<ethereum.Tuple>',
149
149
  /^tuple\[([0-9]+)?\]$/,
150
150
  code => `ethereum.Value.fromTupleArray(${code})`,
151
151
  ],
@@ -60,6 +60,9 @@ const getEtherscanLikeAPIUrl = (network) => {
60
60
  case "aurora-testnet": return `https://api-testnet.aurorascan.dev/api`
61
61
  case "optimism-kovan": return `https://api-kovan-optimistic.etherscan.io/api`
62
62
  case "optimism": return `https://api-optimistic.etherscan.io/api`
63
+ case "moonbeam": return `https://api-moonbeam.moonscan.io/api`
64
+ case "moonriver": return `https://api-moonriver.moonscan.io/api`
65
+ case "mbase": return `https://api-moonbase.moonscan.io/api`
63
66
  case "avalanche": return `https://api.snowtrace.io/api`;
64
67
  case "fuji": return `https://api-testnet.snowtrace.io/api`;
65
68
  default: return `https://api-${network}.etherscan.io/api`
@@ -3,12 +3,16 @@ const toolbox = require('gluegun/toolbox')
3
3
 
4
4
  const createJsonRpcClient = url => {
5
5
  let params = {
6
- auth: url.auth,
7
6
  host: url.hostname,
8
7
  port: url.port,
9
8
  path: url.pathname,
10
9
  }
11
10
 
11
+ // username may be empty
12
+ if (url.password) {
13
+ params.auth = `${url.username}:${url.password}`
14
+ }
15
+
12
16
  if (url.protocol === 'https:') {
13
17
  return jayson.Client.https(params)
14
18
  } else if (url.protocol === 'http:') {
@@ -7,10 +7,17 @@ const { step } = require('./spinner')
7
7
  const Scaffold = require('../scaffold')
8
8
  const { generateEventIndexingHandlers } = require('../scaffold/mapping')
9
9
  const { generateEventType, abiEvents } = require('../scaffold/schema')
10
+ const { generateTestsFiles } = require('../scaffold/tests')
10
11
  const { strings } = require('gluegun')
11
12
  const { Map } = require('immutable')
12
13
 
13
- const generateDataSource = async (protocol, contractName, network, contractAddress, abi) => {
14
+ const generateDataSource = async (
15
+ protocol,
16
+ contractName,
17
+ network,
18
+ contractAddress,
19
+ abi,
20
+ ) => {
14
21
  const protocolManifest = protocol.getManifestScaffold()
15
22
 
16
23
  return Map.of(
@@ -89,13 +96,13 @@ const writeABI = async (abi, contractName) => {
89
96
 
90
97
  const writeSchema = async (abi, protocol, schemaPath, entities) => {
91
98
  const events = protocol.hasEvents()
92
- ? abiEvents(abi).filter(event => entities.indexOf(event.get('name')) === -1).toJS()
99
+ ? abiEvents(abi)
100
+ .filter(event => entities.indexOf(event.get('name')) === -1)
101
+ .toJS()
93
102
  : []
94
103
 
95
104
  let data = prettier.format(
96
- events.map(
97
- event => generateEventType(event, protocol.name)
98
- ).join('\n\n'),
105
+ events.map(event => generateEventType(event, protocol.name)).join('\n\n'),
99
106
  {
100
107
  parser: 'graphql',
101
108
  },
@@ -106,18 +113,39 @@ const writeSchema = async (abi, protocol, schemaPath, entities) => {
106
113
 
107
114
  const writeMapping = async (abi, protocol, contractName, entities) => {
108
115
  const events = protocol.hasEvents()
109
- ? abiEvents(abi).filter(event => entities.indexOf(event.get('name')) === -1).toJS()
116
+ ? abiEvents(abi)
117
+ .filter(event => entities.indexOf(event.get('name')) === -1)
118
+ .toJS()
110
119
  : []
111
120
 
112
- let mapping = prettier.format(
113
- generateEventIndexingHandlers(
114
- events,
115
- contractName,
116
- ),
117
- { parser: 'typescript', semi: false },
118
- )
121
+ let mapping = prettier.format(generateEventIndexingHandlers(events, contractName), {
122
+ parser: 'typescript',
123
+ semi: false,
124
+ })
125
+
126
+ await fs.writeFile(`./src/${strings.kebabCase(contractName)}.ts`, mapping, {
127
+ encoding: 'utf-8',
128
+ })
129
+ }
119
130
 
120
- await fs.writeFile(`./src/${strings.kebabCase(contractName)}.ts`, mapping, { encoding: 'utf-8' })
131
+ const writeTestsFiles = async (abi, protocol, contractName) => {
132
+ const hasEvents = protocol.hasEvents()
133
+ const events = hasEvents
134
+ ? abiEvents(abi).toJS()
135
+ : []
136
+
137
+ if(events.length > 0) {
138
+ // If a contract is added to a subgraph that has no tests folder
139
+ await fs.ensureDir('./tests/')
140
+
141
+ const testsFiles = generateTestsFiles(contractName, events, true)
142
+
143
+ for (const [fileName, content] of Object.entries(testsFiles)) {
144
+ await fs.writeFile(`./tests/${fileName}`, content, {
145
+ encoding: 'utf-8',
146
+ })
147
+ }
148
+ }
121
149
  }
122
150
 
123
151
  module.exports = {
@@ -128,4 +156,5 @@ module.exports = {
128
156
  writeABI,
129
157
  writeSchema,
130
158
  writeMapping,
159
+ writeTestsFiles,
131
160
  }
@@ -1,4 +1,4 @@
1
- const allowedStudioNetworks = ['mainnet', 'rinkeby']
1
+ const allowedStudioNetworks = ['mainnet', 'rinkeby', 'goerli']
2
2
 
3
3
  const validateStudioNetwork = ({ studio, product, network }) => {
4
4
  let isStudio = studio || product === 'subgraph-studio'
@@ -5,7 +5,7 @@ const { withSpinner } = require('../command-helpers/spinner')
5
5
  const Subgraph = require('../subgraph')
6
6
  const Protocol = require('../protocols')
7
7
  const DataSourcesExtractor = require('../command-helpers/data-sources')
8
- const { generateDataSource, writeABI, writeSchema, writeMapping } = require('../command-helpers/scaffold')
8
+ const { generateDataSource, writeABI, writeSchema, writeMapping, writeTestsFiles } = require('../command-helpers/scaffold')
9
9
  const { loadAbiFromEtherscan, loadAbiFromBlockScout } = require('../command-helpers/abi')
10
10
  const EthereumABI = require('../protocols/ethereum/abi')
11
11
  const { fixParameters } = require('../command-helpers/gluegun')
@@ -19,12 +19,12 @@ ${chalk.dim('Options:')}
19
19
  --abi <path> Path to the contract ABI (default: download from Etherscan)
20
20
  --contract-name Name of the contract (default: Contract)
21
21
  --merge-entities Whether to merge entities with the same name (default: false)
22
- --network-file <path> Networks file (default: "./networks.json")
22
+ --network-file <path> Networks config file path (default: "./networks.json")
23
23
  -h, --help Show usage information
24
24
  `
25
25
 
26
26
  module.exports = {
27
- description: 'Creates a new subgraph with basic scaffolding',
27
+ description: 'Adds a new datasource to a subgraph',
28
28
  run: async toolbox => {
29
29
  // Obtain tools
30
30
  let { print, system } = toolbox
@@ -102,9 +102,10 @@ module.exports = {
102
102
  await writeABI(ethabi, contractName)
103
103
  await writeSchema(ethabi, protocol, result.getIn(['schema', 'file']), collisionEntities)
104
104
  await writeMapping(ethabi, protocol, contractName, collisionEntities)
105
+ await writeTestsFiles(ethabi, protocol, contractName)
105
106
 
106
107
  let dataSources = result.get('dataSources')
107
- let dataSource = await generateDataSource(protocol,
108
+ let dataSource = await generateDataSource(protocol,
108
109
  contractName, network, address, ethabi)
109
110
 
110
111
  // Handle the collisions edge case by copying another data source yaml data
@@ -181,12 +182,12 @@ const updateEventNamesOnCollision = (ethabi, entities, contractName, mergeEntiti
181
182
  if (dataRow.get('type') === 'event'){
182
183
  if (entities.indexOf(dataRow.get('name')) !== -1) {
183
184
  if (entities.indexOf(`${contractName}${dataRow.get('name')}`) !== -1) {
184
- print.error(`Contract name ('${contractName}')
185
+ print.error(`Contract name ('${contractName}')
185
186
  + event name ('${dataRow.get('name')}') entity already exists.`)
186
187
  process.exitCode = 1
187
188
  return
188
189
  }
189
-
190
+
190
191
  if (mergeEntities) {
191
192
  collisionEntities.push(dataRow.get('name'))
192
193
  abiData = abiData.asImmutable().delete(i) // needs to be immutable when deleting, yes you read that right - https://github.com/immutable-js/immutable-js/issues/1901
@@ -17,8 +17,8 @@ Options:
17
17
  -t, --output-format <format> Output format for mappings (wasm, wast) (default: wasm)
18
18
  --skip-migrations Skip subgraph migrations (default: false)
19
19
  -w, --watch Regenerate types when subgraph files change (default: false)
20
- --network <name> Network to use from networks.json
21
- --network-file <path> Networks file (default: "./networks.json")
20
+ --network <name> Network configuration to use from the networks config file
21
+ --network-file <path> Networks config file path (default: "./networks.json")
22
22
  `
23
23
 
24
24
  module.exports = {
@@ -14,6 +14,7 @@ const { assertManifestApiVersion, assertGraphTsVersion } = require('../command-h
14
14
  const DataSourcesExtractor = require('../command-helpers/data-sources')
15
15
  const { validateStudioNetwork } = require('../command-helpers/studio')
16
16
  const Protocol = require('../protocols')
17
+ const { updateSubgraphNetwork } = require('../command-helpers/network')
17
18
 
18
19
  const HELP = `
19
20
  ${chalk.bold('graph deploy')} [options] ${chalk.bold('<subgraph-name>')} ${chalk.bold(
@@ -35,6 +36,8 @@ Options:
35
36
  -o, --output-dir <path> Output directory for build results (default: build/)
36
37
  --skip-migrations Skip subgraph migrations (default: false)
37
38
  -w, --watch Regenerate types when subgraph files change (default: false)
39
+ --network <name> Network configuration to use from the networks config file
40
+ --network-file <path> Networks config file path (default: "./networks.json")
38
41
  `
39
42
 
40
43
  const processForm = async (
@@ -52,7 +55,7 @@ const processForm = async (
52
55
  name: 'product',
53
56
  message: 'Product for which to deploy',
54
57
  choices: ['subgraph-studio', 'hosted-service'],
55
- skip:
58
+ skip:
56
59
  product === 'subgraph-studio' ||
57
60
  product === 'hosted-service' ||
58
61
  studio !== undefined || node !== undefined,
@@ -101,6 +104,8 @@ module.exports = {
101
104
  w,
102
105
  watch,
103
106
  debugFork,
107
+ network,
108
+ networkFile,
104
109
  } = toolbox.parameters.options
105
110
 
106
111
  // Support both long and short option variants
@@ -141,6 +146,10 @@ module.exports = {
141
146
  manifest !== undefined && manifest !== ''
142
147
  ? manifest
143
148
  : filesystem.resolve('subgraph.yaml')
149
+ networkFile =
150
+ networkFile !== undefined && networkFile !== ''
151
+ ? networkFile
152
+ : filesystem.resolve("networks.json")
144
153
 
145
154
  try {
146
155
  const dataSourcesAndTemplates = await DataSourcesExtractor.fromFilePath(manifest)
@@ -223,6 +232,11 @@ module.exports = {
223
232
  return
224
233
  }
225
234
 
235
+ if (network) {
236
+ let identifierName = protocol.getContract().identifierName()
237
+ await updateSubgraphNetwork(toolbox, manifest, network, networkFile, identifierName)
238
+ }
239
+
226
240
  const isStudio = node.match(/studio/)
227
241
  const isHostedService = node.match(/thegraph.com/) && !isStudio
228
242
 
@@ -827,7 +827,7 @@ const addAnotherContract = async (toolbox, { protocolInstance, directory }) => {
827
827
  if (addContractConfirmation) {
828
828
  let abiFromFile
829
829
  let ProtocolContract = protocolInstance.getContract()
830
-
830
+
831
831
  let questions = [
832
832
  {
833
833
  type: 'input',
@@ -866,16 +866,16 @@ const addAnotherContract = async (toolbox, { protocolInstance, directory }) => {
866
866
 
867
867
  // Get the cwd before process.chdir in order to switch back in the end of command execution
868
868
  const cwd = process.cwd();
869
-
869
+
870
870
  try {
871
871
  let { abi, contract, contractName } = await toolbox.prompt.ask(questions)
872
-
872
+
873
873
  if (fs.existsSync(directory)) {
874
874
  process.chdir(directory)
875
875
  }
876
-
876
+
877
877
  let commandLine = ['add', contract, '--contract-name', contractName]
878
-
878
+
879
879
  if (abiFromFile) {
880
880
  if (abi.includes(directory)) {
881
881
  commandLine.push('--abi', path.normalize(abi.replace(directory, '')))
@@ -895,4 +895,4 @@ const addAnotherContract = async (toolbox, { protocolInstance, directory }) => {
895
895
  }
896
896
 
897
897
  return addContractConfirmation
898
- }
898
+ }
@@ -168,7 +168,7 @@ async function getPlatform(logsOpt) {
168
168
  const type = os.type()
169
169
  const arch = os.arch()
170
170
  const cpuCore = os.cpus()[0]
171
- const isM1 = (arch === 'arm64' && /Apple (M1|processor)/.test(cpuCore.model))
171
+ const isAppleSilicon = (arch === 'arm64' && /Apple (M1|M2|processor)/.test(cpuCore.model))
172
172
  const linuxInfo = type === 'Linux' ? await getLinuxInfo() : {}
173
173
  const linuxDistro = linuxInfo.name
174
174
  const release = linuxInfo.version || os.release()
@@ -178,13 +178,11 @@ async function getPlatform(logsOpt) {
178
178
  print.info(`OS type: ${linuxDistro || type}\nOS arch: ${arch}\nOS release: ${release}\nOS major version: ${majorVersion}\nCPU model: ${cpuCore.model}`)
179
179
  }
180
180
 
181
- if (arch === 'x64' || isM1) {
181
+ if (arch === 'x64' || isAppleSilicon) {
182
182
  if (type === 'Darwin') {
183
- if (majorVersion === 19) {
184
- return 'binary-macos-10.15'
185
- } else if (majorVersion === 18) {
186
- return 'binary-macos-10.14'
187
- } else if (isM1) {
183
+ if (majorVersion === 18 || majorVersion === 19) {
184
+ return 'binary-macos-10.15' // GitHub dropped support for macOS 10.14 in Actions, but it seems 10.15 binary works on 10.14 too
185
+ } else if (isAppleSilicon) {
188
186
  return 'binary-macos-11-m1'
189
187
  }
190
188
  return 'binary-macos-11'
@@ -196,8 +194,6 @@ async function getPlatform(logsOpt) {
196
194
  } else {
197
195
  return 'binary-linux-20'
198
196
  }
199
- } else if (type === 'Windows_NT') {
200
- return 'binary-windows'
201
197
  }
202
198
  }
203
199
 
@@ -19,7 +19,7 @@ module.exports = {
19
19
  return 'graph-ts dependency not installed yet'
20
20
  }
21
21
 
22
- let manifest = loadManifest(manifestFile)
22
+ let manifest = await loadManifest(manifestFile)
23
23
  return (
24
24
  // Only migrate if the graph-ts version is >= 0.22.0...
25
25
  // Coerce needed because we may be dealing with an alpha version
@@ -19,7 +19,7 @@ module.exports = {
19
19
  return 'graph-ts dependency not installed yet'
20
20
  }
21
21
 
22
- let manifest = loadManifest(manifestFile)
22
+ let manifest = await loadManifest(manifestFile)
23
23
  return (
24
24
  // Only migrate if the graph-ts version is >= 0.24.0...
25
25
  // Coerce needed because we may be dealing with an alpha version
@@ -42,9 +42,7 @@ module.exports = class Protocol {
42
42
  arweave: ['arweave-mainnet'],
43
43
  ethereum: [
44
44
  'mainnet',
45
- 'kovan',
46
45
  'rinkeby',
47
- 'ropsten',
48
46
  'goerli',
49
47
  'poa-core',
50
48
  'poa-sokol',
@@ -74,9 +72,11 @@ module.exports = class Protocol {
74
72
  near: ['near-mainnet', 'near-testnet'],
75
73
  cosmos: [
76
74
  'cosmoshub-4',
77
- 'theta-testnet-001',
75
+ 'theta-testnet-001', // CosmosHub testnet
78
76
  'osmosis-1',
79
- 'osmo-test-4'
77
+ 'osmo-test-4', // Osmosis testnet
78
+ 'juno-1',
79
+ 'uni-3' // Juno testnet
80
80
  ],
81
81
  })
82
82
  }
@@ -0,0 +1,40 @@
1
+ const tsCodegen = require('../../../codegen/typescript')
2
+
3
+ module.exports = class IpfsFileTemplateCodeGen {
4
+ constructor(template) {
5
+ this.template = template
6
+ }
7
+
8
+ generateModuleImports() {
9
+ return []
10
+ }
11
+
12
+ generateCreateMethod() {
13
+ const name = this.template.get('name')
14
+
15
+ return tsCodegen.staticMethod(
16
+ 'create',
17
+ [tsCodegen.param('cid', tsCodegen.namedType('string'))],
18
+ tsCodegen.namedType('void'),
19
+ `
20
+ DataSourceTemplate.create('${name}', [cid])
21
+ `,
22
+ )
23
+ }
24
+
25
+ generateCreateWithContextMethod() {
26
+ const name = this.template.get('name')
27
+
28
+ return tsCodegen.staticMethod(
29
+ 'createWithContext',
30
+ [
31
+ tsCodegen.param('cid', tsCodegen.namedType('string')),
32
+ tsCodegen.param('context', tsCodegen.namedType('DataSourceContext')),
33
+ ],
34
+ tsCodegen.namedType('void'),
35
+ `
36
+ DataSourceTemplate.createWithContext('${name}', [cid], context)
37
+ `,
38
+ )
39
+ }
40
+ }
@@ -33,6 +33,12 @@ type DataSource {
33
33
  type ContractSource {
34
34
  account: String
35
35
  startBlock: BigInt
36
+ accounts: PartialAccount
37
+ }
38
+
39
+ type PartialAccount {
40
+ prefixes: [String!]
41
+ suffixes: [String!]
36
42
  }
37
43
 
38
44
  type ContractMapping {
@@ -243,6 +243,265 @@ export function handleExampleEvent1(event: ExampleEvent1Event): void {
243
243
  entity.a = event.params.a
244
244
  entity.save()
245
245
  }
246
+ `)
247
+ })
248
+
249
+ test('Test Files (default)', () => {
250
+ const files = scaffoldWithIndexEvents.generateTests()
251
+ const testFile = files['contract.test.ts']
252
+ const utilsFile = files['contract-utils.ts']
253
+ expect(testFile).toEqual(`\
254
+ import {
255
+ assert,
256
+ describe,
257
+ test,
258
+ clearStore,
259
+ beforeAll,
260
+ afterAll
261
+ } from \"matchstick-as/assembly/index\"
262
+ import { BigInt, Bytes } from \"@graphprotocol/graph-ts\"
263
+ import { ExampleEvent } from \"../generated/schema\"
264
+ import { ExampleEvent as ExampleEventEvent } from \"../generated/Contract/Contract\"
265
+ import { handleExampleEvent } from \"../src/contract\"
266
+ import { createExampleEventEvent } from \"./contract-utils\"
267
+
268
+ // Tests structure (matchstick-as >=0.5.0)
269
+ // https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0
270
+
271
+ describe(\"Describe entity assertions\", () => {
272
+ beforeAll(() => {
273
+ let a = BigInt.fromI32(234)
274
+ let b = [Bytes.fromI32(1234567890)]
275
+ let param2 = \"Example string value\"
276
+ let c = \"ethereum.Tuple Not implemented\"
277
+ let d = \"Example string value\"
278
+ let newExampleEventEvent = createExampleEventEvent(a, b, param2, c, d)
279
+ handleExampleEvent(newExampleEventEvent)
280
+ })
281
+
282
+ afterAll(() => {
283
+ clearStore()
284
+ })
285
+
286
+ // For more test scenarios, see:
287
+ // https://thegraph.com/docs/en/developer/matchstick/#write-a-unit-test
288
+
289
+ test(\"ExampleEvent created and stored\", () => {
290
+ assert.entityCount(\"ExampleEvent\", 1)
291
+
292
+ // 0xa16081f360e3847006db660bae1c6d1b2e17ec2a is the default address used in newMockEvent() function
293
+ assert.fieldEquals(
294
+ \"ExampleEvent\",
295
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
296
+ \"a\",
297
+ \"234\"
298
+ )
299
+ assert.fieldEquals(
300
+ \"ExampleEvent\",
301
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
302
+ \"b\",
303
+ \"[1234567890]\"
304
+ )
305
+ assert.fieldEquals(
306
+ \"ExampleEvent\",
307
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
308
+ \"param2\",
309
+ \"Example string value\"
310
+ )
311
+ assert.fieldEquals(
312
+ \"ExampleEvent\",
313
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
314
+ \"c\",
315
+ \"ethereum.Tuple Not implemented\"
316
+ )
317
+ assert.fieldEquals(
318
+ \"ExampleEvent\",
319
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
320
+ \"d\",
321
+ \"Example string value\"
322
+ )
323
+
324
+ // More assert options:
325
+ // https://thegraph.com/docs/en/developer/matchstick/#asserts
326
+ })
327
+ })
328
+ `)
329
+ expect(utilsFile).toEqual(`\
330
+ import { newMockEvent } from \"matchstick-as\"
331
+ import { ethereum, BigInt, Bytes } from \"@graphprotocol/graph-ts\"
332
+ import { ExampleEvent, ExampleEvent1 } from \"../generated/Contract/Contract\"
333
+
334
+ export function createExampleEventEvent(
335
+ a: BigInt,
336
+ b: Array<Bytes>,
337
+ param2: string,
338
+ c: ethereum.Tuple,
339
+ d: string
340
+ ): ExampleEvent {
341
+ let exampleEventEvent = changetype<ExampleEvent>(newMockEvent())
342
+
343
+ exampleEventEvent.parameters = new Array()
344
+
345
+ exampleEventEvent.parameters.push(
346
+ new ethereum.EventParam(\"a\", ethereum.Value.fromUnsignedBigInt(a))
347
+ )
348
+ exampleEventEvent.parameters.push(
349
+ new ethereum.EventParam(\"b\", ethereum.Value.fromBytesArray(b))
350
+ )
351
+ exampleEventEvent.parameters.push(
352
+ new ethereum.EventParam(\"param2\", ethereum.Value.fromString(param2))
353
+ )
354
+ exampleEventEvent.parameters.push(
355
+ new ethereum.EventParam(\"c\", ethereum.Value.fromTuple(c))
356
+ )
357
+ exampleEventEvent.parameters.push(
358
+ new ethereum.EventParam(\"d\", ethereum.Value.fromString(d))
359
+ )
360
+
361
+ return exampleEventEvent
362
+ }
363
+
364
+ export function createExampleEvent1Event(a: Bytes): ExampleEvent1 {
365
+ let exampleEvent1Event = changetype<ExampleEvent1>(newMockEvent())
366
+
367
+ exampleEvent1Event.parameters = new Array()
368
+
369
+ exampleEvent1Event.parameters.push(
370
+ new ethereum.EventParam(\"a\", ethereum.Value.fromFixedBytes(a))
371
+ )
372
+
373
+ return exampleEvent1Event
374
+ }
375
+ `)
376
+ })
377
+
378
+ test('Test Files (for indexing events)', () => {
379
+ const files = scaffoldWithIndexEvents.generateTests()
380
+ const testFile = files['contract.test.ts']
381
+ const utilsFile = files['contract-utils.ts']
382
+
383
+ expect(testFile).toEqual(`\
384
+ import {
385
+ assert,
386
+ describe,
387
+ test,
388
+ clearStore,
389
+ beforeAll,
390
+ afterAll
391
+ } from \"matchstick-as/assembly/index\"
392
+ import { BigInt, Bytes } from \"@graphprotocol/graph-ts\"
393
+ import { ExampleEvent } from \"../generated/schema\"
394
+ import { ExampleEvent as ExampleEventEvent } from \"../generated/Contract/Contract\"
395
+ import { handleExampleEvent } from \"../src/contract\"
396
+ import { createExampleEventEvent } from \"./contract-utils\"
397
+
398
+ // Tests structure (matchstick-as >=0.5.0)
399
+ // https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0
400
+
401
+ describe(\"Describe entity assertions\", () => {
402
+ beforeAll(() => {
403
+ let a = BigInt.fromI32(234)
404
+ let b = [Bytes.fromI32(1234567890)]
405
+ let param2 = \"Example string value\"
406
+ let c = \"ethereum.Tuple Not implemented\"
407
+ let d = \"Example string value\"
408
+ let newExampleEventEvent = createExampleEventEvent(a, b, param2, c, d)
409
+ handleExampleEvent(newExampleEventEvent)
410
+ })
411
+
412
+ afterAll(() => {
413
+ clearStore()
414
+ })
415
+
416
+ // For more test scenarios, see:
417
+ // https://thegraph.com/docs/en/developer/matchstick/#write-a-unit-test
418
+
419
+ test(\"ExampleEvent created and stored\", () => {
420
+ assert.entityCount(\"ExampleEvent\", 1)
421
+
422
+ // 0xa16081f360e3847006db660bae1c6d1b2e17ec2a is the default address used in newMockEvent() function
423
+ assert.fieldEquals(
424
+ \"ExampleEvent\",
425
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
426
+ \"a\",
427
+ \"234\"
428
+ )
429
+ assert.fieldEquals(
430
+ \"ExampleEvent\",
431
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
432
+ \"b\",
433
+ \"[1234567890]\"
434
+ )
435
+ assert.fieldEquals(
436
+ \"ExampleEvent\",
437
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
438
+ \"param2\",
439
+ \"Example string value\"
440
+ )
441
+ assert.fieldEquals(
442
+ \"ExampleEvent\",
443
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
444
+ \"c\",
445
+ \"ethereum.Tuple Not implemented\"
446
+ )
447
+ assert.fieldEquals(
448
+ \"ExampleEvent\",
449
+ \"0xa16081f360e3847006db660bae1c6d1b2e17ec2a-1\",
450
+ \"d\",
451
+ \"Example string value\"
452
+ )
453
+
454
+ // More assert options:
455
+ // https://thegraph.com/docs/en/developer/matchstick/#asserts
456
+ })
457
+ })
458
+ `)
459
+ expect(utilsFile).toEqual(`\
460
+ import { newMockEvent } from \"matchstick-as\"
461
+ import { ethereum, BigInt, Bytes } from \"@graphprotocol/graph-ts\"
462
+ import { ExampleEvent, ExampleEvent1 } from \"../generated/Contract/Contract\"
463
+
464
+ export function createExampleEventEvent(
465
+ a: BigInt,
466
+ b: Array<Bytes>,
467
+ param2: string,
468
+ c: ethereum.Tuple,
469
+ d: string
470
+ ): ExampleEvent {
471
+ let exampleEventEvent = changetype<ExampleEvent>(newMockEvent())
472
+
473
+ exampleEventEvent.parameters = new Array()
474
+
475
+ exampleEventEvent.parameters.push(
476
+ new ethereum.EventParam(\"a\", ethereum.Value.fromUnsignedBigInt(a))
477
+ )
478
+ exampleEventEvent.parameters.push(
479
+ new ethereum.EventParam(\"b\", ethereum.Value.fromBytesArray(b))
480
+ )
481
+ exampleEventEvent.parameters.push(
482
+ new ethereum.EventParam(\"param2\", ethereum.Value.fromString(param2))
483
+ )
484
+ exampleEventEvent.parameters.push(
485
+ new ethereum.EventParam(\"c\", ethereum.Value.fromTuple(c))
486
+ )
487
+ exampleEventEvent.parameters.push(
488
+ new ethereum.EventParam(\"d\", ethereum.Value.fromString(d))
489
+ )
490
+
491
+ return exampleEventEvent
492
+ }
493
+
494
+ export function createExampleEvent1Event(a: Bytes): ExampleEvent1 {
495
+ let exampleEvent1Event = changetype<ExampleEvent1>(newMockEvent())
496
+
497
+ exampleEvent1Event.parameters = new Array()
498
+
499
+ exampleEvent1Event.parameters.push(
500
+ new ethereum.EventParam(\"a\", ethereum.Value.fromFixedBytes(a))
501
+ )
502
+
503
+ return exampleEvent1Event
504
+ }
246
505
  `)
247
506
  })
248
507
  })
@@ -15,6 +15,7 @@ const {
15
15
  generateExampleEntityType,
16
16
  } = require('./schema')
17
17
  const { generateEventIndexingHandlers } = require('./mapping')
18
+ const { generateTestsFiles } = require('./tests')
18
19
  const { getSubgraphBasename } = require('../command-helpers/subgraph')
19
20
 
20
21
  module.exports = class Scaffold {
@@ -48,11 +49,13 @@ module.exports = class Scaffold {
48
49
  `--node http://localhost:8020/ ` +
49
50
  `--ipfs http://localhost:5001 ` +
50
51
  this.subgraphName,
52
+ 'test': 'graph test',
51
53
  },
52
54
  dependencies: {
53
55
  '@graphprotocol/graph-cli': GRAPH_CLI_VERSION,
54
56
  '@graphprotocol/graph-ts': `0.27.0`,
55
57
  },
58
+ devDependencies: this.protocol.hasEvents() ? { 'matchstick-as': `0.5.0`} : undefined,
56
59
  }),
57
60
  { parser: 'json' },
58
61
  )
@@ -137,6 +140,17 @@ dataSources:
137
140
  : undefined
138
141
  }
139
142
 
143
+ generateTests() {
144
+ const hasEvents = this.protocol.hasEvents()
145
+ const events = hasEvents
146
+ ? abiEvents(this.abi).toJS()
147
+ : []
148
+
149
+ return events.length > 0
150
+ ? generateTestsFiles(this.contractName, events, this.indexEvents)
151
+ : undefined
152
+ }
153
+
140
154
  generate() {
141
155
  return {
142
156
  'package.json': this.generatePackageJson(),
@@ -145,6 +159,7 @@ dataSources:
145
159
  'tsconfig.json': this.generateTsConfig(),
146
160
  src: { [`${strings.kebabCase(this.contractName)}.ts`]: this.generateMapping() },
147
161
  abis: this.generateABIs(),
162
+ tests: this.generateTests(),
148
163
  }
149
164
  }
150
165
  }
@@ -0,0 +1,196 @@
1
+ const prettier = require('prettier')
2
+ const { strings } = require('gluegun')
3
+ const { ascTypeForEthereum, ethereumFromAsc } = require("../codegen/types")
4
+
5
+ const VARIABLES_VALUES = {
6
+ "i32": 123,
7
+ "BigInt": 234,
8
+ "Bytes": 1234567890,
9
+ "Address": "0x0000000000000000000000000000000000000001",
10
+ "string": "Example string value",
11
+ "bool": true,
12
+ }
13
+
14
+ const generateTestsFiles = (contract, events, indexEvents) => {
15
+ const eventsTypes = events
16
+ .flatMap(event =>
17
+ event
18
+ .inputs
19
+ .map(input => {
20
+ // If the asc type is Array<T> we need to check if T is a native type or a custom graph-ts type
21
+ // If we don't do that we may miss a type that should be imported from graph-ts
22
+ const ascType = ascTypeForEthereum(input.type)
23
+ const inner = fetchArrayInnerType(ascType)
24
+ return inner ? inner[1] : ascType
25
+ })
26
+ ).filter(type => !type.startsWith("ethereum.") && !isNativeType(type))
27
+ const importTypes = [...new Set(eventsTypes)].join(', ')
28
+
29
+ return {
30
+ [`${strings.kebabCase(contract)}.test.ts`]: prettier.format(generateExampleTest(contract, events[0], indexEvents, importTypes), { parser: 'typescript', semi: false }),
31
+ [`${strings.kebabCase(contract)}-utils.ts`]: prettier.format(generateTestHelper(contract, events, importTypes), { parser: 'typescript', semi: false }),
32
+ }
33
+ }
34
+
35
+ /*
36
+ Generates the arguments that will be passed to the mock event function from the event inputs. Example:
37
+ let id = BigInt.fromI32(234)
38
+ let owner = Address.fromString("0x0000000000000000000000000000000000000001")
39
+ let displayName = "Example string value"
40
+ let imageUrl = "Example string value"
41
+ */
42
+ const generateArguments = (eventInputs) => {
43
+ return eventInputs.map((input, index) => {
44
+ let ascType = ascTypeForEthereum(input.type)
45
+ return `let ${input.name || `param${index}`} = ${assignValue(ascType, input.name)}`
46
+ }).join('\n')
47
+ }
48
+
49
+ // Generates the value that will be assigned to a variable in generateArguments()
50
+ const assignValue = (type) => {
51
+ switch (type) {
52
+ case "string":
53
+ return `"${VARIABLES_VALUES[type]}"`
54
+ case "BigInt":
55
+ return `BigInt.fromI32(${VARIABLES_VALUES[type]})`
56
+ case "Address":
57
+ return `Address.fromString("${VARIABLES_VALUES[type]}")`
58
+ case "Bytes":
59
+ return `Bytes.fromI32(${VARIABLES_VALUES[type]})`
60
+ case fetchArrayInnerType(type)?.input:
61
+ innerType = fetchArrayInnerType(type)[1]
62
+ return `[${assignValue(innerType)}]`
63
+ default:
64
+ let value = VARIABLES_VALUES[type]
65
+ return value ? value : `"${type} Not implemented"`
66
+ }
67
+ }
68
+
69
+ /*
70
+ Generates the assert.fieldEquals() for a given entity and event inputs. Example:
71
+ assert.fieldEquals(
72
+ "ExampleEntity",
73
+ "0xa16081f360e3847006db660bae1c6d1b2e17ec2a",
74
+ "owner",
75
+ "0x0000000000000000000000000000000000000001"
76
+ )
77
+ */
78
+ const generateFieldsAssertions = (entity, eventInputs, indexEvents) => eventInputs.filter(input => input.name != "id").map((input, index) =>
79
+ `assert.fieldEquals(
80
+ "${entity}",
81
+ "0xa16081f360e3847006db660bae1c6d1b2e17ec2a${indexEvents ? "-1" : ""}",
82
+ "${input.name || `param${index}`}",
83
+ "${expectedValue(ascTypeForEthereum(input.type))}"
84
+ )`
85
+ ).join('\n')
86
+
87
+ // Returns the expected value for a given type in generateFieldsAssertions()
88
+ const expectedValue = type => {
89
+ switch (type) {
90
+ case fetchArrayInnerType(type)?.input:
91
+ innerType = fetchArrayInnerType(type)[1]
92
+ return `[${expectedValue(innerType)}]`
93
+ default:
94
+ let value = VARIABLES_VALUES[type]
95
+ return value ? value : `${type} Not implemented`
96
+ }
97
+ }
98
+
99
+ // Checks if the type is a native AS type or should be imported from graph-ts
100
+ const isNativeType = type => {
101
+ let natives = [
102
+ /i32/,
103
+ /string/,
104
+ /boolean/
105
+ ]
106
+
107
+ return natives.some(rx => rx.test(type));
108
+ }
109
+
110
+ const fetchArrayInnerType = type => type.match(/Array<(.*?)>/)
111
+
112
+ // Generates the example test.ts file
113
+ const generateExampleTest = (contract, event, indexEvents, importTypes) => {
114
+ const entity = indexEvents ? `${event._alias}` : 'ExampleEntity'
115
+ const eventInputs = event.inputs
116
+ const eventName = event._alias
117
+
118
+ return `
119
+ import { assert, describe, test, clearStore, beforeAll, afterAll } from "matchstick-as/assembly/index"
120
+ import { ${importTypes} } from "@graphprotocol/graph-ts"
121
+ import { ${entity} } from "../generated/schema"
122
+ import { ${indexEvents ? `${eventName} as ${eventName}Event` : eventName} } from "../generated/${contract}/${contract}"
123
+ import { handle${eventName} } from "../src/${strings.kebabCase(contract)}"
124
+ import { create${eventName}Event } from "./${strings.kebabCase(contract)}-utils"
125
+
126
+
127
+ // Tests structure (matchstick-as >=0.5.0)
128
+ // https://thegraph.com/docs/en/developer/matchstick/#tests-structure-0-5-0
129
+
130
+ describe("Describe entity assertions", () => {
131
+ beforeAll(() => {
132
+ ${generateArguments(eventInputs)}
133
+ let new${eventName}Event = create${eventName}Event(${eventInputs.map((input, index) => input.name || `param${index}`).join(', ')});
134
+ handle${eventName}(new${eventName}Event)
135
+ })
136
+
137
+ afterAll(() => {
138
+ clearStore()
139
+ })
140
+
141
+ // For more test scenarios, see:
142
+ // https://thegraph.com/docs/en/developer/matchstick/#write-a-unit-test
143
+
144
+ test("${entity} created and stored", () => {
145
+ assert.entityCount('${entity}', 1)
146
+
147
+ // 0xa16081f360e3847006db660bae1c6d1b2e17ec2a is the default address used in newMockEvent() function
148
+ ${generateFieldsAssertions(entity, eventInputs, indexEvents)}
149
+
150
+ // More assert options:
151
+ // https://thegraph.com/docs/en/developer/matchstick/#asserts
152
+ })
153
+ })
154
+ `
155
+ }
156
+
157
+ // Generates the utils helper file
158
+ const generateTestHelper = (contract, events, importTypes) => {
159
+ const eventsNames = events.map(event => event._alias)
160
+
161
+ return `
162
+ import { newMockEvent } from 'matchstick-as';
163
+ import { ethereum, ${importTypes} } from '@graphprotocol/graph-ts';
164
+ import { ${eventsNames.join(", ")} } from '../generated/${contract}/${contract}';
165
+
166
+ ${generateMockedEvents(events).join("\n")}`
167
+ }
168
+
169
+ const generateMockedEvents = events =>
170
+ events.reduce(
171
+ (acc, event) => acc.concat(generateMockedEvent(event)),
172
+ [],
173
+ )
174
+
175
+ const generateMockedEvent = event => {
176
+ const varName = `${strings.camelCase(event._alias)}Event`
177
+ const fnArgs = event.inputs.map((input, index) => `${input.name || `param${index}`}: ${ascTypeForEthereum(input.type)}`);
178
+ const ascToEth = event.inputs.map((input, index) => `${varName}.parameters.push(new ethereum.EventParam("${input.name || `param${index}`}", ${ethereumFromAsc(input.name || `param${index}`, input.type)}))`);
179
+
180
+ return `
181
+ export function create${event._alias}Event(${fnArgs.join(', ')}): ${event._alias} {
182
+ let ${varName} = changetype<${event._alias}>(newMockEvent());
183
+
184
+ ${varName}.parameters = new Array();
185
+
186
+ ${ascToEth.join('\n')}
187
+
188
+ return ${varName};
189
+ }
190
+ `
191
+
192
+ }
193
+
194
+ module.exports = {
195
+ generateTestsFiles,
196
+ }
package/src/subgraph.js CHANGED
@@ -216,20 +216,26 @@ More than one template named '${name}', template names must be unique.`,
216
216
  static async load(filename, { protocol, skipValidation } = { skipValidation: false }) {
217
217
  // Load and validate the manifest
218
218
  let data = null
219
+ let has_file_data_sources = false
219
220
 
220
221
  if (filename.match(/.js$/)) {
221
222
  data = require(path.resolve(filename))
222
223
  } else {
223
- data = yaml.parse(await fs.readFile(filename, 'utf-8'))
224
+ let raw_data = await fs.readFile(filename, 'utf-8')
225
+ has_file_data_sources = raw_data.includes('kind: file')
226
+ data = yaml.parse(raw_data)
224
227
  }
225
228
 
226
229
  // Helper to resolve files relative to the subgraph manifest
227
230
  let resolveFile = maybeRelativeFile =>
228
231
  path.resolve(path.dirname(filename), maybeRelativeFile)
229
232
 
230
- let manifestErrors = await Subgraph.validate(data, protocol, { resolveFile })
231
- if (manifestErrors.size > 0) {
232
- throwCombinedError(filename, manifestErrors)
233
+ // TODO: Validation for file data sources
234
+ if (!has_file_data_sources) {
235
+ let manifestErrors = await Subgraph.validate(data, protocol, { resolveFile })
236
+ if (manifestErrors.size > 0) {
237
+ throwCombinedError(filename, manifestErrors)
238
+ }
233
239
  }
234
240
 
235
241
  let manifest = immutable.fromJS(data)
@@ -10,6 +10,14 @@ dataSources:
10
10
  source:
11
11
  account: wnear.flux-dev
12
12
  startBlock: 1
13
+ accounts:
14
+ prefixes:
15
+ - some-prefix
16
+ - a-prefix.with-dot
17
+ suffixes:
18
+ - suffix.near
19
+ - near
20
+ - another-suffix.testnet
13
21
  mapping:
14
22
  apiVersion: 0.0.5
15
23
  language: wasm/assemblyscript