From 482be8cfaac1454054cd8cd9e4dfe103382b4031 Mon Sep 17 00:00:00 2001 From: Ashwin Phatak Date: Mon, 27 Sep 2021 10:13:50 +0530 Subject: [PATCH] Support storage mode for code generator (#251) * Implement storage mode in code generator. * Fix indexer template for eth_call. * Add support for both eth_call and storage modes. * Add eventsInRange, events file gen and default entites gen. * Use static property to column maps in database. * Process events query. * Avoid adding duplicate events in indexer. * Fix generated watcher query with bigint arguments. Co-authored-by: nabarun Co-authored-by: prathamesh --- packages/codegen/README.md | 21 +- packages/codegen/src/database.ts | 3 - packages/codegen/src/entity.ts | 488 +++++++++++++++++- packages/codegen/src/events.ts | 21 + packages/codegen/src/generate-code.ts | 23 +- packages/codegen/src/indexer.ts | 36 +- packages/codegen/src/schema.ts | 10 + .../src/templates/config-template.handlebars | 1 - .../templates/database-template.handlebars | 148 +++++- .../src/templates/entity-template.handlebars | 42 +- .../src/templates/events-template.handlebars | 113 ++++ .../src/templates/indexer-template.handlebars | 280 +++++++++- .../templates/resolvers-template.handlebars | 14 +- .../src/templates/server-template.handlebars | 4 +- packages/codegen/src/utils/constants.ts | 7 + .../codegen/src/utils/handlebar-helpers.ts | 10 +- packages/codegen/src/visitor.ts | 11 +- packages/erc20-watcher/src/entity/Event.ts | 2 - packages/erc20-watcher/src/events.ts | 5 +- packages/erc20-watcher/src/indexer.ts | 4 +- packages/util/src/index.ts | 22 +- 21 files changed, 1147 insertions(+), 118 deletions(-) create mode 100644 packages/codegen/src/events.ts create mode 100644 packages/codegen/src/templates/events-template.handlebars create mode 100644 packages/codegen/src/utils/constants.ts diff --git a/packages/codegen/README.md b/packages/codegen/README.md index 72f7acc1..17647a35 100644 --- a/packages/codegen/README.md +++ b/packages/codegen/README.md @@ -13,27 +13,34 @@ * Run the following command to generate a watcher from a contract file: ```bash - yarn codegen --input-file --contract-name --output-folder [output-folder] --mode [eth_call | storage] --flatten [true | false] + yarn codegen --input-file --contract-name --output-folder [output-folder] --mode [eth_call | storage | all] --flatten [true | false] ``` * `input-file`(alias: `i`): Input contract file path or an URL (required). * `contract-name`(alias: `c`): Main contract name (required). * `output-folder`(alias: `o`): Output folder path. (logs output using `stdout` if not provided). - * `mode`(alias: `m`): Code generation mode (default: `storage`). + * `mode`(alias: `m`): Code generation mode (default: `all`). * `flatten`(alias: `f`): Flatten the input contract file (default: `true`). **Note**: When passed an *URL* as `input-file`, it is assumed that it points to an already flattened contract file. Examples: - + + Generate code in both eth_call and storage mode. ```bash - yarn codegen --input-file ./test/examples/contracts/ERC20.sol --contract-name ERC20 --output-folder ../my-erc20-watcher --mode eth_call + yarn codegen --input-file ./test/examples/contracts/ERC20.sol --contract-name ERC20 --output-folder ../my-erc20-watcher --mode all ``` + Generate code in eth_call mode using a contract provided by URL. ```bash yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../my-erc721-watcher --mode eth_call ``` + Generate code in storage mode. + ```bash + yarn codegen --input-file ./test/examples/contracts/ERC721.sol --contract-name ERC721 --output-folder ../my-erc721-watcher --mode storage + ``` + ## Demo * Install required packages: @@ -43,7 +50,7 @@ ``` * Generate a watcher from a contract file: - + ```bash yarn codegen --input-file ../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --contract-name ERC20 --output-folder ../demo-erc20-watcher --mode eth_call ``` @@ -51,7 +58,7 @@ This will create a folder called `demo-erc20-watcher` containing the generated code at the specified path. Follow the steps in `demo-erc20-watcher/README.md` to setup and run the generated watcher. * Generate a watcher from a flattened contract file from an URL: - + ```bash yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../demo-erc721-watcher --mode eth_call ``` @@ -65,4 +72,4 @@ ## Known Issues -* Currently, `node-fetch v2.6.2` is being used to fetch from URLs as `v3.0.0` is an [ESM-only module](https://www.npmjs.com/package/node-fetch#loading-and-configuring-the-module) and `ts-node` transpiles to import it using `require`. +* Currently, `node-fetch v2.6.2` is being used to fetch from URLs as `v3.0.0` is an [ESM-only module](https://www.npmjs.com/package/node-fetch#loading-and-configuring-the-module) and `ts-node` transpiles to import it using `require`. diff --git a/packages/codegen/src/database.ts b/packages/codegen/src/database.ts index e7108174..65740deb 100644 --- a/packages/codegen/src/database.ts +++ b/packages/codegen/src/database.ts @@ -11,7 +11,6 @@ import _ from 'lodash'; import { getTsForSol } from './utils/type-mappings'; import { Param } from './utils/types'; -import { capitalizeHelper } from './utils/handlebar-helpers'; const TEMPLATE_FILE = './templates/database-template.handlebars'; @@ -22,8 +21,6 @@ export class Database { constructor () { this._queries = []; this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); - - Handlebars.registerHelper('capitalize', capitalizeHelper); } /** diff --git a/packages/codegen/src/entity.ts b/packages/codegen/src/entity.ts index 7b455521..1f7a0c5e 100644 --- a/packages/codegen/src/entity.ts +++ b/packages/codegen/src/entity.ts @@ -37,34 +37,95 @@ export class Entity { const entityObject: any = { // Capitalize the first letter of name. className: `${name.charAt(0).toUpperCase()}${name.slice(1)}`, - indexOn: {}, - columns: [{}], - returnType: returnType + indexOn: [], + columns: [], + imports: [] }; - entityObject.indexOn.columns = params.map((param) => { - return param.name; + entityObject.imports.push( + { + toImport: ['Entity', 'PrimaryGeneratedColumn', 'Column', 'Index'], + from: 'typeorm' + } + ); + + const indexObject = { + columns: ['blockHash', 'contractAddress'], + unique: true + }; + indexObject.columns = indexObject.columns.concat( + params.map((param) => { + return param.name; + }) + ); + entityObject.indexOn.push(indexObject); + + entityObject.columns.push({ + name: 'blockHash', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 66 + } + ] }); - entityObject.indexOn.unique = true; - - entityObject.columns = params.map((param) => { - const length = param.type === 'address' ? 42 : null; - const name = param.name; - - const tsType = getTsForSol(param.type); - assert(tsType); - - const pgType = getPgForTs(tsType); - assert(pgType); - - return { - name, - pgType, - tsType, - length - }; + entityObject.columns.push({ + name: 'contractAddress', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 42 + } + ] }); + entityObject.columns = entityObject.columns.concat( + params.map((param) => { + const name = param.name; + + const tsType = getTsForSol(param.type); + assert(tsType); + + const pgType = getPgForTs(tsType); + assert(pgType); + + const columnOptions = []; + + if (param.type === 'address') { + columnOptions.push( + { + option: 'length', + value: 42 + } + ); + } + + // Use bigintTransformer for bigint types. + if (tsType === 'bigint') { + columnOptions.push( + { + option: 'transformer', + value: 'bigintTransformer' + } + ); + } + + return { + name, + pgType, + tsType, + columnType: 'Column', + columnOptions + }; + }) + ); + const tsReturnType = getTsForSol(returnType); assert(tsReturnType); @@ -74,7 +135,21 @@ export class Entity { entityObject.columns.push({ name: 'value', pgType: pgReturnType, - tsType: tsReturnType + tsType: tsReturnType, + columnType: 'Column' + }); + + entityObject.columns.push({ + name: 'proof', + pgType: 'text', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'nullable', + value: true + } + ] }); this._entities.push(entityObject); @@ -85,6 +160,11 @@ export class Entity { * @param entityDir Directory to write the entities to. */ exportEntities (entityDir: string): void { + this._addEventEntity(); + this._addSyncStatusEntity(); + this._addContractEntity(); + this._addBlockProgressEntity(); + const template = Handlebars.compile(this._templateString); this._entities.forEach(entityObj => { const entity = template(entityObj); @@ -94,4 +174,364 @@ export class Entity { outStream.write(entity); }); } + + _addEventEntity (): void { + const entity: any = { + className: 'Event', + indexOn: [], + columns: [], + imports: [] + }; + + entity.imports.push( + { + toImport: ['Entity', 'PrimaryGeneratedColumn', 'Column', 'Index', 'ManyToOne'], + from: 'typeorm' + }, + { + toImport: ['BlockProgress'], + from: './BlockProgress' + } + ); + + entity.indexOn.push( + { + columns: ['block', 'contract'] + }, + { + columns: ['block', 'contract', 'eventName'] + } + ); + + entity.columns.push({ + name: 'block', + tsType: 'BlockProgress', + columnType: 'ManyToOne', + lhs: '()', + rhs: 'BlockProgress' + }); + + entity.columns.push({ + name: 'txHash', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 66 + } + ] + }); + + entity.columns.push({ + name: 'index', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'contract', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 42 + } + ] + }); + + entity.columns.push({ + name: 'eventName', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 256 + } + ] + }); + + entity.columns.push({ + name: 'eventInfo', + pgType: 'text', + tsType: 'string', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'extraInfo', + pgType: 'text', + tsType: 'string', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'proof', + pgType: 'text', + tsType: 'string', + columnType: 'Column' + }); + + this._entities.push(entity); + } + + _addSyncStatusEntity (): void { + const entity: any = { + className: 'SyncStatus', + implements: 'SyncStatusInterface', + indexOn: [], + columns: [], + imports: [] + }; + + entity.imports.push({ + toImport: ['Entity', 'PrimaryGeneratedColumn', 'Column'], + from: 'typeorm' + }); + + entity.imports.push({ + toImport: ['SyncStatusInterface'], + from: '@vulcanize/util' + }); + + entity.columns.push({ + name: 'chainHeadBlockHash', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 66 + } + ] + }); + + entity.columns.push({ + name: 'chainHeadBlockNumber', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'latestIndexedBlockHash', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 66 + } + ] + }); + + entity.columns.push({ + name: 'latestIndexedBlockNumber', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'latestCanonicalBlockHash', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 66 + } + ] + }); + + entity.columns.push({ + name: 'latestCanonicalBlockNumber', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + this._entities.push(entity); + } + + _addContractEntity (): void { + const entity: any = { + className: 'Contract', + indexOn: [], + columns: [], + imports: [] + }; + + entity.imports.push({ + toImport: ['Entity', 'PrimaryGeneratedColumn', 'Column', 'Index'], + from: 'typeorm' + }); + + entity.indexOn.push( + { + columns: ['address'], + unique: true + } + ); + + entity.columns.push({ + name: 'address', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 42 + } + ] + }); + + entity.columns.push({ + name: 'kind', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 8 + } + ] + }); + + entity.columns.push({ + name: 'startingBlock', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + this._entities.push(entity); + } + + _addBlockProgressEntity (): void { + const entity: any = { + className: 'BlockProgress', + implements: 'BlockProgressInterface', + indexOn: [], + columns: [], + imports: [] + }; + + entity.imports.push({ + toImport: ['Entity', 'PrimaryGeneratedColumn', 'Column', 'Index'], + from: 'typeorm' + }); + + entity.imports.push({ + toImport: ['BlockProgressInterface'], + from: '@vulcanize/util' + }); + + entity.indexOn.push( + { + columns: ['blockHash'], + unique: true + }, + { + columns: ['blockNumber'] + }, + { + columns: ['parentHash'] + } + ); + + entity.columns.push({ + name: 'blockHash', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 66 + } + ] + }); + + entity.columns.push({ + name: 'parentHash', + pgType: 'varchar', + tsType: 'string', + columnType: 'Column', + columnOptions: [ + { + option: 'length', + value: 66 + } + ] + }); + + entity.columns.push({ + name: 'blockNumber', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'blockTimestamp', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'numEvents', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'numProcessedEvents', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'lastProcessedEventIndex', + pgType: 'integer', + tsType: 'number', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'isComplete', + pgType: 'boolean', + tsType: 'boolean', + columnType: 'Column' + }); + + entity.columns.push({ + name: 'isPruned', + pgType: 'boolean', + tsType: 'boolean', + columnType: 'Column', + columnOptions: [ + { + option: 'default', + value: false + } + ] + }); + + this._entities.push(entity); + } } diff --git a/packages/codegen/src/events.ts b/packages/codegen/src/events.ts new file mode 100644 index 00000000..51720dc0 --- /dev/null +++ b/packages/codegen/src/events.ts @@ -0,0 +1,21 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; + +const TEMPLATE_FILE = './templates/events-template.handlebars'; + +/** + * Writes the events file generated from a template to a stream. + * @param outStream A writable output stream to write the events file to. + */ +export function exportEvents (outStream: Writable): void { + const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + const template = Handlebars.compile(templateString); + const events = template({}); + outStream.write(events); +} diff --git a/packages/codegen/src/generate-code.ts b/packages/codegen/src/generate-code.ts index d15dbaf0..6649d24e 100644 --- a/packages/codegen/src/generate-code.ts +++ b/packages/codegen/src/generate-code.ts @@ -11,6 +11,7 @@ import { flatten } from '@poanet/solidity-flattener'; import { parse, visit } from '@solidity-parser/parser'; +import { MODE_ETH_CALL, MODE_STORAGE, MODE_ALL } from './utils/constants'; import { Visitor } from './visitor'; import { exportServer } from './server'; import { exportConfig } from './config'; @@ -18,9 +19,8 @@ import { exportArtifacts } from './artifacts'; import { exportPackage } from './package'; import { exportTSConfig } from './tsconfig'; import { exportReadme } from './readme'; - -const MODE_ETH_CALL = 'eth_call'; -const MODE_STORAGE = 'storage'; +import { exportEvents } from './events'; +import { registerHandlebarHelpers } from './utils/handlebar-helpers'; const main = async (): Promise => { const argv = await yargs(hideBin(process.argv)) @@ -45,8 +45,8 @@ const main = async (): Promise => { alias: 'm', describe: 'Code generation mode.', type: 'string', - default: MODE_STORAGE, - choices: [MODE_ETH_CALL, MODE_STORAGE] + default: MODE_ALL, + choices: [MODE_ETH_CALL, MODE_STORAGE, MODE_ALL] }) .option('flatten', { alias: 'f', @@ -81,12 +81,14 @@ function parseAndVisit (data: string, visitor: Visitor, mode: string) { // Filter out library nodes. ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library')); - if (mode === MODE_ETH_CALL) { + if ([MODE_ALL, MODE_ETH_CALL].some(value => value === mode)) { visit(ast, { FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor), EventDefinition: visitor.eventDefinitionVisitor.bind(visitor) }); - } else { + } + + if ([MODE_ALL, MODE_STORAGE].some(value => value === mode)) { visit(ast, { StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor), EventDefinition: visitor.eventDefinitionVisitor.bind(visitor) @@ -113,6 +115,8 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) { const inputFileName = path.basename(argv['input-file'], '.sol'); + registerHandlebarHelpers(); + let outStream = outputDir ? fs.createWriteStream(path.join(outputDir, 'src/schema.gql')) : process.stdout; @@ -172,6 +176,11 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) { ? fs.createWriteStream(path.join(outputDir, 'README.md')) : process.stdout; exportReadme(path.basename(outputDir), argv['contract-name'], outStream); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'src/events.ts')) + : process.stdout; + exportEvents(outStream); } main().catch(err => { diff --git a/packages/codegen/src/indexer.ts b/packages/codegen/src/indexer.ts index eca13168..b67a33c3 100644 --- a/packages/codegen/src/indexer.ts +++ b/packages/codegen/src/indexer.ts @@ -11,38 +11,39 @@ import _ from 'lodash'; import { getTsForSol } from './utils/type-mappings'; import { Param } from './utils/types'; -import { compareHelper, capitalizeHelper } from './utils/handlebar-helpers'; +import { MODE_ETH_CALL, MODE_STORAGE } from './utils/constants'; const TEMPLATE_FILE = './templates/indexer-template.handlebars'; export class Indexer { _queries: Array; + _events: Array; _templateString: string; constructor () { this._queries = []; + this._events = []; this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); - - Handlebars.registerHelper('compare', compareHelper); - Handlebars.registerHelper('capitalize', capitalizeHelper); } /** * Stores the query to be passed to the template. + * @param mode Code generation mode. * @param name Name of the query. * @param params Parameters to the query. * @param returnType Return type for the query. */ - addQuery (name: string, params: Array, returnType: string): void { + addQuery (mode: string, name: string, params: Array, returnType: string): void { // Check if the query is already added. if (this._queries.some(query => query.name === name)) { return; } const queryObject = { - name: name, + name, params: _.cloneDeep(params), - returnType: returnType + returnType, + mode }; queryObject.params = queryObject.params.map((param) => { @@ -59,6 +60,18 @@ export class Indexer { this._queries.push(queryObject); } + addEvent (name: string, params: Array): void { + // Check if the event is already added. + if (this._events.some(event => event.name === name)) { + return; + } + + this._events.push({ + name, + params + }); + } + /** * Writes the indexer file generated from a template to a stream. * @param outStream A writable output stream to write the indexer file to. @@ -66,10 +79,17 @@ export class Indexer { */ exportIndexer (outStream: Writable, inputFileName: string): void { const template = Handlebars.compile(this._templateString); + const obj = { inputFileName, - queries: this._queries + queries: this._queries, + constants: { + MODE_ETH_CALL, + MODE_STORAGE + }, + events: this._events }; + const indexer = template(obj); outStream.write(indexer); } diff --git a/packages/codegen/src/schema.ts b/packages/codegen/src/schema.ts index 19eee6f1..1b691ae9 100644 --- a/packages/codegen/src/schema.ts +++ b/packages/codegen/src/schema.ts @@ -217,6 +217,16 @@ export class Schema { } } }); + + this._composer.Query.addFields({ + eventsInRange: { + type: [this._composer.getOTC('ResultEvent').NonNull], + args: { + fromBlockNumber: 'Int!', + toBlockNumber: 'Int!' + } + } + }); } /** diff --git a/packages/codegen/src/templates/config-template.handlebars b/packages/codegen/src/templates/config-template.handlebars index 549ac581..b556d1f9 100644 --- a/packages/codegen/src/templates/config-template.handlebars +++ b/packages/codegen/src/templates/config-template.handlebars @@ -1,7 +1,6 @@ [server] host = "127.0.0.1" port = 3008 - mode = "eth_call" [database] type = "postgres" diff --git a/packages/codegen/src/templates/database-template.handlebars b/packages/codegen/src/templates/database-template.handlebars index 72131ef1..14f512b6 100644 --- a/packages/codegen/src/templates/database-template.handlebars +++ b/packages/codegen/src/templates/database-template.handlebars @@ -3,19 +3,24 @@ // import assert from 'assert'; -import { Connection, ConnectionOptions, DeepPartial } from 'typeorm'; +import { Connection, ConnectionOptions, DeepPartial, FindConditions, QueryRunner } from 'typeorm'; import path from 'path'; import { Database as BaseDatabase } from '@vulcanize/util'; +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { BlockProgress } from './entity/BlockProgress'; {{#each queries as | query |}} import { {{capitalize query.name tillIndex=1}} } from './entity/{{capitalize query.name tillIndex=1}}'; {{/each}} export class Database { - _config: ConnectionOptions - _conn!: Connection + _config: ConnectionOptions; + _conn!: Connection; _baseDatabase: BaseDatabase; + _propColMaps: { [key: string]: Map; } constructor (config: ConnectionOptions) { assert(config); @@ -26,10 +31,12 @@ export class Database { }; this._baseDatabase = new BaseDatabase(this._config); + this._propColMaps = {}; } async init (): Promise { this._conn = await this._baseDatabase.init(); + this._setPropColMaps(); } async close (): Promise { @@ -41,17 +48,13 @@ export class Database { {{~#each query.params}}, {{this.name~}} {{/each}} }: { blockHash: string, contractAddress: string {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}} }): Promise<{{capitalize query.name tillIndex=1}} | undefined> { return this._conn.getRepository({{capitalize query.name tillIndex=1}}) - .createQueryBuilder('{{query.name}}') - .where(`${this._getColumn('{{capitalize query.name tillIndex=1}}', 'blockHash')} = :blockHash AND ${this._getColumn('{{capitalize query.name tillIndex=1}}', 'contractAddress')} = :contractAddress - {{~#each query.params}} AND ${this._getColumn('{{capitalize query.name tillIndex=1}}', '{{this.name}}')} = :{{this.name~}} {{/each}}`, { + .findOne({ blockHash, - contractAddress - {{~#each query.params}}, - {{this.name}} - {{~/each}} - + contractAddress{{#if query.params.length}},{{/if}} + {{#each query.params}} + {{this.name}}{{#unless @last}},{{/unless}} + {{/each}} }) - .getOne(); } {{/each}} @@ -66,7 +69,124 @@ export class Database { } {{/each}} - _getColumn (entityName: string, propertyName: string) { - return this._conn.getMetadata(entityName).findColumnWithPropertyName(propertyName)?.databaseName + async getContract (address: string): Promise { + const repo = this._conn.getRepository(Contract); + + return this._baseDatabase.getContract(repo, address); + } + + async createTransactionRunner (): Promise { + return this._baseDatabase.createTransactionRunner(); + } + + async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getProcessedBlockCountForRange(repo, fromBlockNumber, toBlockNumber); + } + + async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise> { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber); + } + + async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise { + const repo = queryRunner.manager.getRepository(Event); + return this._baseDatabase.saveEventEntity(repo, entity); + } + + async getBlockEvents (blockHash: string, where: FindConditions): Promise { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getBlockEvents(repo, blockHash, where); + } + + async saveEvents (queryRunner: QueryRunner, block: DeepPartial, events: DeepPartial[]): Promise { + const blockRepo = queryRunner.manager.getRepository(BlockProgress); + const eventRepo = queryRunner.manager.getRepository(Event); + + return this._baseDatabase.saveEvents(blockRepo, eventRepo, block, events); + } + + async saveContract (address: string, kind: string, startingBlock: number): Promise { + await this._conn.transaction(async (tx) => { + const repo = tx.getRepository(Contract); + + return this._baseDatabase.saveContract(repo, address, startingBlock, kind); + }); + } + + async updateSyncStatusIndexedBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusIndexedBlock(repo, blockHash, blockNumber); + } + + async updateSyncStatusCanonicalBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusCanonicalBlock(repo, blockHash, blockNumber); + } + + async updateSyncStatusChainHead (queryRunner: QueryRunner, blockHash: string, blockNumber: number): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber); + } + + async getSyncStatus (queryRunner: QueryRunner): Promise { + const repo = queryRunner.manager.getRepository(SyncStatus); + + return this._baseDatabase.getSyncStatus(repo); + } + + async getEvent (id: string): Promise { + const repo = this._conn.getRepository(Event); + + return this._baseDatabase.getEvent(repo, id); + } + + async getBlocksAtHeight (height: number, isPruned: boolean): Promise { + const repo = this._conn.getRepository(BlockProgress); + + return this._baseDatabase.getBlocksAtHeight(repo, height, isPruned); + } + + async markBlocksAsPruned (queryRunner: QueryRunner, blocks: BlockProgress[]): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.markBlocksAsPruned(repo, blocks); + } + + async getBlockProgress (blockHash: string): Promise { + const repo = this._conn.getRepository(BlockProgress); + return this._baseDatabase.getBlockProgress(repo, blockHash); + } + + async updateBlockProgress (queryRunner: QueryRunner, blockHash: string, lastProcessedEventIndex: number): Promise { + const repo = queryRunner.manager.getRepository(BlockProgress); + + return this._baseDatabase.updateBlockProgress(repo, blockHash, lastProcessedEventIndex); + } + + async removeEntities (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindConditions): Promise { + return this._baseDatabase.removeEntities(queryRunner, entity, findConditions); + } + + async getAncestorAtDepth (blockHash: string, depth: number): Promise { + return this._baseDatabase.getAncestorAtDepth(blockHash, depth); + } + + _getPropertyColumnMapForEntity (entityName: string): Map { + return this._conn.getMetadata(entityName).ownColumns.reduce((acc, curr) => { + return acc.set(curr.propertyName, curr.databaseName); + }, new Map()); + } + + _setPropColMaps () { + {{#each queries as | query |}} + this._propColMaps['{{capitalize query.name tillIndex=1}}'] = this._getPropertyColumnMapForEntity('{{capitalize query.name tillIndex=1}}'); + {{/each}} } } diff --git a/packages/codegen/src/templates/entity-template.handlebars b/packages/codegen/src/templates/entity-template.handlebars index f57d0751..438d9510 100644 --- a/packages/codegen/src/templates/entity-template.handlebars +++ b/packages/codegen/src/templates/entity-template.handlebars @@ -2,29 +2,41 @@ // Copyright 2021 Vulcanize, Inc. // -import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; +{{#each imports as | import |}} +import { {{~#each import.toImport}} {{this}} {{~#unless @last}}, {{~/unless}} {{~/each}} } from '{{import.from}}'; +{{/each}} + +import { bigintTransformer } from '@vulcanize/util'; @Entity() -{{#if indexOn.columns}} -@Index(['blockHash', 'contractAddress' -{{~#each indexOn.columns}}, '{{this}}' -{{~/each}}], { unique: {{indexOn.unique}} }) +{{#each indexOn as | index |}} +{{#if index.columns}} +@Index([ +{{~#each index.columns}}'{{this}}' +{{~#unless @last}}, {{/unless}} +{{~/each}}] +{{~#if index.unique}}, { unique: true }{{/if}}) {{/if}} -export class {{className}} { +{{/each}} +export class {{className}} {{~#if implements}} implements {{implements}} {{~/if}} { @PrimaryGeneratedColumn() id!: number; - @Column('varchar', { length: 66 }) - blockHash!: string; - - @Column('varchar', { length: 42 }) - contractAddress!: string; - {{#each columns as | column |}} - @Column('{{column.pgType}}' {{~#if column.length}}, { length: {{column.length}} } {{~/if}}) + {{#if (compare column.columnType 'ManyToOne')}} + @{{column.columnType}}({{column.lhs}} => {{column.rhs}} + {{~else}} + @{{column.columnType}}('{{column.pgType}}' + {{~/if}} + {{~#if column.columnOptions}}, { + {{~#each column.columnOptions}} {{this.option}}: {{this.value}} + {{~#unless @last}}, {{/unless}} + {{~/each}} } + {{~/if}}) {{column.name}}!: {{column.tsType}}; + {{~#unless @last}} + + {{/unless}} {{/each}} - @Column('text', { nullable: true }) - proof!: string; } diff --git a/packages/codegen/src/templates/events-template.handlebars b/packages/codegen/src/templates/events-template.handlebars new file mode 100644 index 00000000..bd7eea99 --- /dev/null +++ b/packages/codegen/src/templates/events-template.handlebars @@ -0,0 +1,113 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import debug from 'debug'; +import { PubSub } from 'apollo-server-express'; + +import { EthClient } from '@vulcanize/ipld-eth-client'; +import { + JobQueue, + EventWatcher as BaseEventWatcher, + QUEUE_BLOCK_PROCESSING, + QUEUE_EVENT_PROCESSING, + UNKNOWN_EVENT_NAME +} from '@vulcanize/util'; + +import { Indexer } from './indexer'; +import { Event } from './entity/Event'; + +const EVENT = 'event'; + +const log = debug('vulcanize:events'); + +export class EventWatcher { + _ethClient: EthClient + _indexer: Indexer + _subscription: ZenObservable.Subscription | undefined + _baseEventWatcher: BaseEventWatcher + _pubsub: PubSub + _jobQueue: JobQueue + + constructor (ethClient: EthClient, indexer: Indexer, pubsub: PubSub, jobQueue: JobQueue) { + assert(ethClient); + assert(indexer); + + this._ethClient = ethClient; + this._indexer = indexer; + this._pubsub = pubsub; + this._jobQueue = jobQueue; + this._baseEventWatcher = new BaseEventWatcher(this._ethClient, this._indexer, this._pubsub, this._jobQueue); + } + + getEventIterator (): AsyncIterator { + return this._pubsub.asyncIterator([EVENT]); + } + + getBlockProgressEventIterator (): AsyncIterator { + return this._baseEventWatcher.getBlockProgressEventIterator(); + } + + async start (): Promise { + assert(!this._subscription, 'subscription already started'); + + await this.watchBlocksAtChainHead(); + await this.initBlockProcessingOnCompleteHandler(); + await this.initEventProcessingOnCompleteHandler(); + } + + async stop (): Promise { + this._baseEventWatcher.stop(); + } + + async watchBlocksAtChainHead (): Promise { + log('Started watching upstream blocks...'); + this._subscription = await this._ethClient.watchBlocks(async (value) => { + await this._baseEventWatcher.blocksHandler(value); + }); + } + + async initBlockProcessingOnCompleteHandler (): Promise { + this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => { + await this._baseEventWatcher.blockProcessingCompleteHandler(job); + }); + } + + async initEventProcessingOnCompleteHandler (): Promise { + await this._jobQueue.onComplete(QUEUE_EVENT_PROCESSING, async (job) => { + const dbEvent = await this._baseEventWatcher.eventProcessingCompleteHandler(job); + + const { data: { request, failed, state, createdOn } } = job; + + const timeElapsedInSeconds = (Date.now() - Date.parse(createdOn)) / 1000; + log(`Job onComplete event ${request.data.id} publish ${!!request.data.publish}`); + if (!failed && state === 'completed' && request.data.publish) { + // Check for max acceptable lag time between request and sending results to live subscribers. + if (timeElapsedInSeconds <= this._jobQueue.maxCompletionLag) { + await this.publishEventToSubscribers(dbEvent, timeElapsedInSeconds); + } else { + log(`event ${request.data.id} is too old (${timeElapsedInSeconds}s), not broadcasting to live subscribers`); + } + } + }); + } + + async publishEventToSubscribers (dbEvent: Event, timeElapsedInSeconds: number): Promise { + if (dbEvent && dbEvent.eventName !== UNKNOWN_EVENT_NAME) { + const { block: { blockHash }, contract: contractAddress } = dbEvent; + const resultEvent = this._indexer.getResultEvent(dbEvent); + + log(`pushing event to GQL subscribers (${timeElapsedInSeconds}s elapsed): ${resultEvent.event.__typename}`); + + // Publishing the event here will result in pushing the payload to GQL subscribers for `onEvent`. + await this._pubsub.publish(EVENT, { + onEvent: { + blockHash, + contractAddress, + event: resultEvent + } + }); + } + } +} diff --git a/packages/codegen/src/templates/indexer-template.handlebars b/packages/codegen/src/templates/indexer-template.handlebars index 70c801e6..8bbd7da4 100644 --- a/packages/codegen/src/templates/indexer-template.handlebars +++ b/packages/codegen/src/templates/indexer-template.handlebars @@ -5,37 +5,60 @@ import assert from 'assert'; import debug from 'debug'; import { JsonFragment } from '@ethersproject/abi'; +import { DeepPartial } from 'typeorm'; import JSONbig from 'json-bigint'; -import { BigNumber, ethers, Contract } from 'ethers'; +import { BigNumber, ethers } from 'ethers'; import { BaseProvider } from '@ethersproject/providers'; import { EthClient } from '@vulcanize/ipld-eth-client'; -import { StorageLayout } from '@vulcanize/solidity-mapper'; -import { ValueResult } from '@vulcanize/util'; +import { getStorageValue, GetStorageAt, StorageLayout } from '@vulcanize/solidity-mapper'; +import { EventInterface, Indexer as BaseIndexer, ValueResult, UNKNOWN_EVENT_NAME } from '@vulcanize/util'; import { Database } from './database'; +import { Contract } from './entity/Contract'; +import { Event } from './entity/Event'; +import { SyncStatus } from './entity/SyncStatus'; +import { BlockProgress } from './entity/BlockProgress'; import artifacts from './artifacts/{{inputFileName}}.json'; const log = debug('vulcanize:indexer'); +{{#each events as | event |}} +const {{capitalize event.name}}_EVENT = '{{event.name}}'; +{{/each}} + +interface ResultEvent { + block: any; + tx: any; + + contract: string; + + eventIndex: number; + event: any; + + proof: string; +} + export class Indexer { _db: Database _ethClient: EthClient + _getStorageAt: GetStorageAt _ethProvider: BaseProvider + _baseIndexer: BaseIndexer _abi: JsonFragment[] _storageLayout: StorageLayout _contract: ethers.utils.Interface - _serverMode: string - constructor (db: Database, ethClient: EthClient, ethProvider: BaseProvider, serverMode: string) { + constructor (db: Database, ethClient: EthClient, ethProvider: BaseProvider) { assert(db); assert(ethClient); this._db = db; this._ethClient = ethClient; this._ethProvider = ethProvider; - this._serverMode = serverMode; + this._getStorageAt = this._ethClient.getStorageAt.bind(this._ethClient); + this._baseIndexer = new BaseIndexer(this._db, this._ethClient); const { abi, storageLayout } = artifacts; @@ -48,9 +71,43 @@ export class Indexer { this._contract = new ethers.utils.Interface(this._abi); } + getResultEvent (event: Event): ResultEvent { + const block = event.block; + const eventFields = JSON.parse(event.eventInfo); + const { tx } = JSON.parse(event.extraInfo); + + return { + block: { + hash: block.blockHash, + number: block.blockNumber, + timestamp: block.blockTimestamp, + parentHash: block.parentHash + }, + + tx: { + hash: event.txHash, + from: tx.src, + to: tx.dst, + index: tx.index + }, + + contract: event.contract, + + eventIndex: event.index, + event: { + __typename: `${event.eventName}Event`, + ...eventFields + }, + + // TODO: Return proof only if requested. + proof: JSON.parse(event.proof) + }; + } + {{#each queries as | query |}} async {{query.name}} (blockHash: string, contractAddress: string {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}}): Promise { + const entity = await this._db.get{{capitalize query.name tillIndex=1}}({ blockHash, contractAddress {{~#each query.params}}, {{this.name~}} {{~/each}} }); if (entity) { @@ -64,19 +121,13 @@ export class Indexer { log('{{query.name}}: db miss, fetching from upstream server'); - const contract = new Contract(contractAddress, this._abi, this._ethProvider); - let value = null; - - {{~#if query.params}} + {{#if (compare query.mode @root.constants.MODE_ETH_CALL)}} + const contract = new ethers.Contract(contractAddress, this._abi, this._ethProvider); const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); const blockNumber = BigNumber.from(number).toNumber(); - value = await contract.{{query.name}}( + let value = await contract.{{query.name}}( {{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockNumber }); - {{else}} - - value = await contract.{{query.name}}(); - {{/if}} {{~#if (compare query.returnType 'bigint')}} @@ -85,14 +136,203 @@ export class Indexer { {{/if}} const result: ValueResult = { value }; - const { proof } = result; + {{/if}} + + {{~#if (compare query.mode @root.constants.MODE_STORAGE)}} + + const result = await this._getStorageValue( + this._storageLayout, + blockHash, + contractAddress, + '{{query.name}}'{{#if query.params.length}},{{/if}} + {{#each query.params}} + {{this.name}}{{#unless @last}},{{/unless}} + {{/each}} + ); + {{/if}} + await this._db.save{{capitalize query.name tillIndex=1}}({ blockHash, contractAddress - {{~#each query.params}}, {{this.name~}} {{/each}}, value, proof: JSONbig.stringify(proof) }); - + {{~#each query.params}}, {{this.name~}} {{/each}}, value: result.value, proof: JSONbig.stringify(result.proof) }); + return result; } - {{#unless @last}} - {{/unless}} {{/each}} + async _getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: any[]): Promise { + return getStorageValue( + storageLayout, + this._getStorageAt, + blockHash, + contractAddress, + variable, + ...mappingKeys + ); + } + + parseEventNameAndArgs (kind: string, logObj: any): any { + let eventName = UNKNOWN_EVENT_NAME; + let eventInfo = {}; + + const { topics, data } = logObj; + const logDescription = this._contract.parseLog({ data, topics }); + + switch (logDescription.name) { + {{#each events as | event |}} + case {{capitalize event.name}}_EVENT: { + eventName = logDescription.name; + const { {{#each event.params~}} {{this.name}} {{~#unless @last}}, {{/unless}} {{~/each}} } = logDescription.args; + eventInfo = { + {{#each event.params}} + {{this.name}} {{~#unless @last}},{{/unless}} + {{/each}} + }; + + break; + } + {{/each}} + } + + return { eventName, eventInfo }; + } + + async getEventsByFilter (blockHash: string, contract: string, name: string | null): Promise> { + return this._baseIndexer.getEventsByFilter(blockHash, contract, name); + } + + async isWatchedContract (address : string): Promise { + return this._baseIndexer.isWatchedContract(address); + } + + async getSyncStatus (): Promise { + return this._baseIndexer.getSyncStatus(); + } + + async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number): Promise { + return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber); + } + + async updateSyncStatusChainHead (blockHash: string, blockNumber: number): Promise { + return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber); + } + + async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number): Promise { + return this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber); + } + + async getBlock (blockHash: string): Promise { + return this._baseIndexer.getBlock(blockHash); + } + + async getEvent (id: string): Promise { + return this._baseIndexer.getEvent(id); + } + + async getBlockProgress (blockHash: string): Promise { + return this._baseIndexer.getBlockProgress(blockHash); + } + + async getBlocksAtHeight (height: number, isPruned: boolean): Promise { + return this._baseIndexer.getBlocksAtHeight(height, isPruned); + } + + async getOrFetchBlockEvents (block: DeepPartial): Promise> { + return this._baseIndexer.getOrFetchBlockEvents(block, this._fetchAndSaveEvents.bind(this)); + } + + async getBlockEvents (blockHash: string): Promise> { + return this._baseIndexer.getBlockEvents(blockHash); + } + + async markBlocksAsPruned (blocks: BlockProgress[]): Promise { + return this._baseIndexer.markBlocksAsPruned(blocks); + } + + async updateBlockProgress (blockHash: string, lastProcessedEventIndex: number): Promise { + return this._baseIndexer.updateBlockProgress(blockHash, lastProcessedEventIndex); + } + + async getAncestorAtDepth (blockHash: string, depth: number): Promise { + return this._baseIndexer.getAncestorAtDepth(blockHash, depth); + } + + async _fetchAndSaveEvents ({ blockHash }: DeepPartial): Promise { + assert(blockHash); + let { block, logs } = await this._ethClient.getLogs({ blockHash }); + + const dbEvents: Array> = []; + + for (let li = 0; li < logs.length; li++) { + const logObj = logs[li]; + const { + topics, + data, + index: logIndex, + cid, + ipldBlock, + account: { + address + }, + transaction: { + hash: txHash + }, + receiptCID, + status + } = logObj; + + if (status) { + let eventName = UNKNOWN_EVENT_NAME; + let eventInfo = {}; + const extraInfo = { topics, data }; + + const contract = ethers.utils.getAddress(address); + const watchedContract = await this.isWatchedContract(contract); + + if (watchedContract) { + const eventDetails = this.parseEventNameAndArgs(watchedContract.kind, logObj); + eventName = eventDetails.eventName; + eventInfo = eventDetails.eventInfo; + } + + dbEvents.push({ + index: logIndex, + txHash, + contract, + eventName, + eventInfo: JSONbig.stringify(eventInfo), + extraInfo: JSONbig.stringify(extraInfo), + proof: JSONbig.stringify({ + data: JSONbig.stringify({ + blockHash, + receiptCID, + log: { + cid, + ipldBlock + } + }) + }) + }); + } else { + log(`Skipping event for receipt ${receiptCID} due to failed transaction.`); + } + } + + const dbTx = await this._db.createTransactionRunner(); + + try { + block = { + blockHash, + blockNumber: block.number, + blockTimestamp: block.timestamp, + parentHash: block.parent.hash + }; + + await this._db.saveEvents(dbTx, block, dbEvents); + await dbTx.commitTransaction(); + } catch (error) { + await dbTx.rollbackTransaction(); + throw error; + } finally { + await dbTx.release(); + } + } } diff --git a/packages/codegen/src/templates/resolvers-template.handlebars b/packages/codegen/src/templates/resolvers-template.handlebars index 2bb65c43..5f7618b8 100644 --- a/packages/codegen/src/templates/resolvers-template.handlebars +++ b/packages/codegen/src/templates/resolvers-template.handlebars @@ -35,12 +35,20 @@ export const createResolvers = async (indexer: Indexer): Promise => { {{~#each this.params}}, {{this.name~}} {{/each}}); return indexer.{{this.name}}(blockHash, contractAddress {{~#each this.params}}, {{this.name~}} {{/each}}); - } - {{~#unless @last}}, + }, - {{/unless}} {{/each}} + events: async (_: any, { blockHash, contractAddress, name }: { blockHash: string, contractAddress: string, name: string }) => { + log('events', blockHash, contractAddress, name || ''); + const block = await indexer.getBlockProgress(blockHash); + if (!block || !block.isComplete) { + throw new Error(`Block hash ${blockHash} number ${block?.blockNumber} not processed yet`); + } + + const events = await indexer.getEventsByFilter(blockHash, contractAddress, name); + return events.map(event => indexer.getResultEvent(event)); + } } }; }; diff --git a/packages/codegen/src/templates/server-template.handlebars b/packages/codegen/src/templates/server-template.handlebars index 04748d42..a51af18a 100644 --- a/packages/codegen/src/templates/server-template.handlebars +++ b/packages/codegen/src/templates/server-template.handlebars @@ -40,7 +40,7 @@ export const main = async (): Promise => { assert(config.server, 'Missing server config'); - const { host, port, mode } = config.server; + const { host, port } = config.server; const { upstream, database: dbConfig } = config; @@ -66,7 +66,7 @@ export const main = async (): Promise => { // Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries const pubsub = new PubSub(); - const indexer = new Indexer(db, ethClient, ethProvider, mode); + const indexer = new Indexer(db, ethClient, ethProvider); const resolvers = await createResolvers(indexer); diff --git a/packages/codegen/src/utils/constants.ts b/packages/codegen/src/utils/constants.ts new file mode 100644 index 00000000..7f2fda28 --- /dev/null +++ b/packages/codegen/src/utils/constants.ts @@ -0,0 +1,7 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +export const MODE_ETH_CALL = 'eth_call'; +export const MODE_STORAGE = 'storage'; +export const MODE_ALL = 'all'; diff --git a/packages/codegen/src/utils/handlebar-helpers.ts b/packages/codegen/src/utils/handlebar-helpers.ts index 3dc08a87..87c8dfe9 100644 --- a/packages/codegen/src/utils/handlebar-helpers.ts +++ b/packages/codegen/src/utils/handlebar-helpers.ts @@ -3,6 +3,12 @@ // import assert from 'assert'; +import Handlebars from 'handlebars'; + +export function registerHandlebarHelpers (): void { + Handlebars.registerHelper('compare', compareHelper); + Handlebars.registerHelper('capitalize', capitalizeHelper); +} /** * Helper function to compare two values using the given operator. @@ -11,7 +17,7 @@ import assert from 'assert'; * @param options Handlebars options parameter. `options.hash.operator`: operator to be used for comparison. * @returns Result of the comparison. */ -export function compareHelper (lvalue: string, rvalue: string, options: any): boolean { +function compareHelper (lvalue: string, rvalue: string, options: any): boolean { assert(lvalue && rvalue, "Handlerbars Helper 'compare' needs at least 2 parameters"); const operator = options.hash.operator || '==='; @@ -38,7 +44,7 @@ export function compareHelper (lvalue: string, rvalue: string, options: any): bo * @param options Handlebars options parameter. `options.hash.tillIndex`: index till which to capitalize the string. * @returns The modified string. */ -export function capitalizeHelper (value: string, options: any): string { +function capitalizeHelper (value: string, options: any): string { const tillIndex = options.hash.tillIndex || value.length; const result = `${value.slice(0, tillIndex).toUpperCase()}${value.slice(tillIndex, value.length)}`; diff --git a/packages/codegen/src/visitor.ts b/packages/codegen/src/visitor.ts index ba089737..ed70c0cf 100644 --- a/packages/codegen/src/visitor.ts +++ b/packages/codegen/src/visitor.ts @@ -5,11 +5,12 @@ import { Writable } from 'stream'; import { Database } from './database'; +import { Param } from './utils/types'; +import { MODE_ETH_CALL, MODE_STORAGE } from './utils/constants'; import { Entity } from './entity'; import { Indexer } from './indexer'; import { Resolvers } from './resolvers'; import { Schema } from './schema'; -import { Param } from './utils/types'; export class Visitor { _schema: Schema; @@ -42,7 +43,7 @@ export class Visitor { this._schema.addQuery(name, params, returnType); this._resolvers.addQuery(name, params, returnType); - this._indexer.addQuery(name, params, returnType); + this._indexer.addQuery(MODE_ETH_CALL, name, params, returnType); this._entity.addQuery(name, params, returnType); this._database.addQuery(name, params, returnType); } @@ -55,8 +56,7 @@ export class Visitor { stateVariableDeclarationVisitor (node: any): void { // TODO Handle multiples variables in a single line. // TODO Handle array types. - let name: string = node.variables[0].name; - name = name.startsWith('_') ? name.substring(1) : name; + const name: string = node.variables[0].name; const params: Param[] = []; @@ -75,7 +75,7 @@ export class Visitor { this._schema.addQuery(name, params, returnType); this._resolvers.addQuery(name, params, returnType); - this._indexer.addQuery(name, params, returnType); + this._indexer.addQuery(MODE_STORAGE, name, params, returnType); this._entity.addQuery(name, params, returnType); this._database.addQuery(name, params, returnType); } @@ -91,6 +91,7 @@ export class Visitor { }); this._schema.addEventType(name, params); + this._indexer.addEvent(name, params); } /** diff --git a/packages/erc20-watcher/src/entity/Event.ts b/packages/erc20-watcher/src/entity/Event.ts index 7971bf56..6ac4e1fd 100644 --- a/packages/erc20-watcher/src/entity/Event.ts +++ b/packages/erc20-watcher/src/entity/Event.ts @@ -5,8 +5,6 @@ import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm'; import { BlockProgress } from './BlockProgress'; -export const UNKNOWN_EVENT_NAME = '__unknown__'; - @Entity() // Index to query all events for a contract efficiently. @Index(['block', 'contract']) diff --git a/packages/erc20-watcher/src/events.ts b/packages/erc20-watcher/src/events.ts index 35e91dd2..08d5ba20 100644 --- a/packages/erc20-watcher/src/events.ts +++ b/packages/erc20-watcher/src/events.ts @@ -11,11 +11,12 @@ import { JobQueue, EventWatcher as BaseEventWatcher, QUEUE_BLOCK_PROCESSING, - QUEUE_EVENT_PROCESSING + QUEUE_EVENT_PROCESSING, + UNKNOWN_EVENT_NAME } from '@vulcanize/util'; import { Indexer } from './indexer'; -import { Event, UNKNOWN_EVENT_NAME } from './entity/Event'; +import { Event } from './entity/Event'; const EVENT = 'event'; diff --git a/packages/erc20-watcher/src/indexer.ts b/packages/erc20-watcher/src/indexer.ts index 026cb4ba..dfbeb56e 100644 --- a/packages/erc20-watcher/src/indexer.ts +++ b/packages/erc20-watcher/src/indexer.ts @@ -12,10 +12,10 @@ import { BaseProvider } from '@ethersproject/providers'; import { EthClient } from '@vulcanize/ipld-eth-client'; import { StorageLayout } from '@vulcanize/solidity-mapper'; -import { EventInterface, Indexer as BaseIndexer, ValueResult } from '@vulcanize/util'; +import { EventInterface, Indexer as BaseIndexer, ValueResult, UNKNOWN_EVENT_NAME } from '@vulcanize/util'; import { Database } from './database'; -import { Event, UNKNOWN_EVENT_NAME } from './entity/Event'; +import { Event } from './entity/Event'; import { fetchTokenDecimals, fetchTokenName, fetchTokenSymbol, fetchTokenTotalSupply } from './utils'; import { SyncStatus } from './entity/SyncStatus'; import artifacts from './artifacts/ERC20.json'; diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index cc6c1722..6fec7c95 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -12,7 +12,7 @@ import { ValueTransformer } from 'typeorm'; export const wait = async (time: number): Promise => new Promise(resolve => setTimeout(resolve, time)); /** - * Transformer used by typeorm entity for Decimal type fields + * Transformer used by typeorm entity for Decimal type fields. */ export const decimalTransformer: ValueTransformer = { to: (value?: Decimal) => { @@ -30,3 +30,23 @@ export const decimalTransformer: ValueTransformer = { return value; } }; + +/** + * Transformer used by typeorm entity for bigint type fields. + */ +export const bigintTransformer: ValueTransformer = { + to: (value?: bigint) => { + if (value) { + return value.toString(); + } + + return value; + }, + from: (value?: string) => { + if (value) { + return BigInt(value); + } + + return value; + } +};