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 <nabarun@deepstacksoft.com>
Co-authored-by: prathamesh <prathamesh.musale0@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-09-27 10:13:50 +05:30 committed by GitHub
parent 92b7967895
commit 482be8cfaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1147 additions and 118 deletions

View File

@ -13,27 +13,34 @@
* Run the following command to generate a watcher from a contract file: * Run the following command to generate a watcher from a contract file:
```bash ```bash
yarn codegen --input-file <input-file-path> --contract-name <contract-name> --output-folder [output-folder] --mode [eth_call | storage] --flatten [true | false] yarn codegen --input-file <input-file-path> --contract-name <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). * `input-file`(alias: `i`): Input contract file path or an URL (required).
* `contract-name`(alias: `c`): Main contract name (required). * `contract-name`(alias: `c`): Main contract name (required).
* `output-folder`(alias: `o`): Output folder path. (logs output using `stdout` if not provided). * `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`). * `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. **Note**: When passed an *URL* as `input-file`, it is assumed that it points to an already flattened contract file.
Examples: Examples:
Generate code in both eth_call and storage mode.
```bash ```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 ```bash
yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../my-erc721-watcher --mode eth_call 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 ## Demo
* Install required packages: * Install required packages:

View File

@ -11,7 +11,6 @@ import _ from 'lodash';
import { getTsForSol } from './utils/type-mappings'; import { getTsForSol } from './utils/type-mappings';
import { Param } from './utils/types'; import { Param } from './utils/types';
import { capitalizeHelper } from './utils/handlebar-helpers';
const TEMPLATE_FILE = './templates/database-template.handlebars'; const TEMPLATE_FILE = './templates/database-template.handlebars';
@ -22,8 +21,6 @@ export class Database {
constructor () { constructor () {
this._queries = []; this._queries = [];
this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString();
Handlebars.registerHelper('capitalize', capitalizeHelper);
} }
/** /**

View File

@ -37,18 +37,56 @@ export class Entity {
const entityObject: any = { const entityObject: any = {
// Capitalize the first letter of name. // Capitalize the first letter of name.
className: `${name.charAt(0).toUpperCase()}${name.slice(1)}`, className: `${name.charAt(0).toUpperCase()}${name.slice(1)}`,
indexOn: {}, indexOn: [],
columns: [{}], columns: [],
returnType: returnType imports: []
}; };
entityObject.indexOn.columns = params.map((param) => { entityObject.imports.push(
return param.name; {
}); toImport: ['Entity', 'PrimaryGeneratedColumn', 'Column', 'Index'],
entityObject.indexOn.unique = true; from: 'typeorm'
}
);
entityObject.columns = params.map((param) => { const indexObject = {
const length = param.type === 'address' ? 42 : null; 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.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 name = param.name;
const tsType = getTsForSol(param.type); const tsType = getTsForSol(param.type);
@ -57,13 +95,36 @@ export class Entity {
const pgType = getPgForTs(tsType); const pgType = getPgForTs(tsType);
assert(pgType); 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 { return {
name, name,
pgType, pgType,
tsType, tsType,
length columnType: 'Column',
columnOptions
}; };
}); })
);
const tsReturnType = getTsForSol(returnType); const tsReturnType = getTsForSol(returnType);
assert(tsReturnType); assert(tsReturnType);
@ -74,7 +135,21 @@ export class Entity {
entityObject.columns.push({ entityObject.columns.push({
name: 'value', name: 'value',
pgType: pgReturnType, 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); this._entities.push(entityObject);
@ -85,6 +160,11 @@ export class Entity {
* @param entityDir Directory to write the entities to. * @param entityDir Directory to write the entities to.
*/ */
exportEntities (entityDir: string): void { exportEntities (entityDir: string): void {
this._addEventEntity();
this._addSyncStatusEntity();
this._addContractEntity();
this._addBlockProgressEntity();
const template = Handlebars.compile(this._templateString); const template = Handlebars.compile(this._templateString);
this._entities.forEach(entityObj => { this._entities.forEach(entityObj => {
const entity = template(entityObj); const entity = template(entityObj);
@ -94,4 +174,364 @@ export class Entity {
outStream.write(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);
}
} }

View File

@ -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);
}

View File

@ -11,6 +11,7 @@ import { flatten } from '@poanet/solidity-flattener';
import { parse, visit } from '@solidity-parser/parser'; import { parse, visit } from '@solidity-parser/parser';
import { MODE_ETH_CALL, MODE_STORAGE, MODE_ALL } from './utils/constants';
import { Visitor } from './visitor'; import { Visitor } from './visitor';
import { exportServer } from './server'; import { exportServer } from './server';
import { exportConfig } from './config'; import { exportConfig } from './config';
@ -18,9 +19,8 @@ import { exportArtifacts } from './artifacts';
import { exportPackage } from './package'; import { exportPackage } from './package';
import { exportTSConfig } from './tsconfig'; import { exportTSConfig } from './tsconfig';
import { exportReadme } from './readme'; import { exportReadme } from './readme';
import { exportEvents } from './events';
const MODE_ETH_CALL = 'eth_call'; import { registerHandlebarHelpers } from './utils/handlebar-helpers';
const MODE_STORAGE = 'storage';
const main = async (): Promise<void> => { const main = async (): Promise<void> => {
const argv = await yargs(hideBin(process.argv)) const argv = await yargs(hideBin(process.argv))
@ -45,8 +45,8 @@ const main = async (): Promise<void> => {
alias: 'm', alias: 'm',
describe: 'Code generation mode.', describe: 'Code generation mode.',
type: 'string', type: 'string',
default: MODE_STORAGE, default: MODE_ALL,
choices: [MODE_ETH_CALL, MODE_STORAGE] choices: [MODE_ETH_CALL, MODE_STORAGE, MODE_ALL]
}) })
.option('flatten', { .option('flatten', {
alias: 'f', alias: 'f',
@ -81,12 +81,14 @@ function parseAndVisit (data: string, visitor: Visitor, mode: string) {
// Filter out library nodes. // Filter out library nodes.
ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library')); 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, { visit(ast, {
FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor), FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor),
EventDefinition: visitor.eventDefinitionVisitor.bind(visitor) EventDefinition: visitor.eventDefinitionVisitor.bind(visitor)
}); });
} else { }
if ([MODE_ALL, MODE_STORAGE].some(value => value === mode)) {
visit(ast, { visit(ast, {
StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor), StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor),
EventDefinition: visitor.eventDefinitionVisitor.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'); const inputFileName = path.basename(argv['input-file'], '.sol');
registerHandlebarHelpers();
let outStream = outputDir let outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/schema.gql')) ? fs.createWriteStream(path.join(outputDir, 'src/schema.gql'))
: process.stdout; : process.stdout;
@ -172,6 +176,11 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
? fs.createWriteStream(path.join(outputDir, 'README.md')) ? fs.createWriteStream(path.join(outputDir, 'README.md'))
: process.stdout; : process.stdout;
exportReadme(path.basename(outputDir), argv['contract-name'], outStream); 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 => { main().catch(err => {

View File

@ -11,38 +11,39 @@ import _ from 'lodash';
import { getTsForSol } from './utils/type-mappings'; import { getTsForSol } from './utils/type-mappings';
import { Param } from './utils/types'; 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'; const TEMPLATE_FILE = './templates/indexer-template.handlebars';
export class Indexer { export class Indexer {
_queries: Array<any>; _queries: Array<any>;
_events: Array<any>;
_templateString: string; _templateString: string;
constructor () { constructor () {
this._queries = []; this._queries = [];
this._events = [];
this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); 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. * Stores the query to be passed to the template.
* @param mode Code generation mode.
* @param name Name of the query. * @param name Name of the query.
* @param params Parameters to the query. * @param params Parameters to the query.
* @param returnType Return type for the query. * @param returnType Return type for the query.
*/ */
addQuery (name: string, params: Array<Param>, returnType: string): void { addQuery (mode: string, name: string, params: Array<Param>, returnType: string): void {
// Check if the query is already added. // Check if the query is already added.
if (this._queries.some(query => query.name === name)) { if (this._queries.some(query => query.name === name)) {
return; return;
} }
const queryObject = { const queryObject = {
name: name, name,
params: _.cloneDeep(params), params: _.cloneDeep(params),
returnType: returnType returnType,
mode
}; };
queryObject.params = queryObject.params.map((param) => { queryObject.params = queryObject.params.map((param) => {
@ -59,6 +60,18 @@ export class Indexer {
this._queries.push(queryObject); this._queries.push(queryObject);
} }
addEvent (name: string, params: Array<Param>): 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. * Writes the indexer file generated from a template to a stream.
* @param outStream A writable output stream to write the indexer file to. * @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 { exportIndexer (outStream: Writable, inputFileName: string): void {
const template = Handlebars.compile(this._templateString); const template = Handlebars.compile(this._templateString);
const obj = { const obj = {
inputFileName, inputFileName,
queries: this._queries queries: this._queries,
constants: {
MODE_ETH_CALL,
MODE_STORAGE
},
events: this._events
}; };
const indexer = template(obj); const indexer = template(obj);
outStream.write(indexer); outStream.write(indexer);
} }

View File

@ -217,6 +217,16 @@ export class Schema {
} }
} }
}); });
this._composer.Query.addFields({
eventsInRange: {
type: [this._composer.getOTC('ResultEvent').NonNull],
args: {
fromBlockNumber: 'Int!',
toBlockNumber: 'Int!'
}
}
});
} }
/** /**

View File

@ -1,7 +1,6 @@
[server] [server]
host = "127.0.0.1" host = "127.0.0.1"
port = 3008 port = 3008
mode = "eth_call"
[database] [database]
type = "postgres" type = "postgres"

View File

@ -3,19 +3,24 @@
// //
import assert from 'assert'; import assert from 'assert';
import { Connection, ConnectionOptions, DeepPartial } from 'typeorm'; import { Connection, ConnectionOptions, DeepPartial, FindConditions, QueryRunner } from 'typeorm';
import path from 'path'; import path from 'path';
import { Database as BaseDatabase } from '@vulcanize/util'; 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 |}} {{#each queries as | query |}}
import { {{capitalize query.name tillIndex=1}} } from './entity/{{capitalize query.name tillIndex=1}}'; import { {{capitalize query.name tillIndex=1}} } from './entity/{{capitalize query.name tillIndex=1}}';
{{/each}} {{/each}}
export class Database { export class Database {
_config: ConnectionOptions _config: ConnectionOptions;
_conn!: Connection _conn!: Connection;
_baseDatabase: BaseDatabase; _baseDatabase: BaseDatabase;
_propColMaps: { [key: string]: Map<string, string>; }
constructor (config: ConnectionOptions) { constructor (config: ConnectionOptions) {
assert(config); assert(config);
@ -26,10 +31,12 @@ export class Database {
}; };
this._baseDatabase = new BaseDatabase(this._config); this._baseDatabase = new BaseDatabase(this._config);
this._propColMaps = {};
} }
async init (): Promise<void> { async init (): Promise<void> {
this._conn = await this._baseDatabase.init(); this._conn = await this._baseDatabase.init();
this._setPropColMaps();
} }
async close (): Promise<void> { async close (): Promise<void> {
@ -41,17 +48,13 @@ export class Database {
{{~#each query.params}}, {{this.name~}} {{/each}} }: { blockHash: string, contractAddress: string {{~#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> { {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}} }): Promise<{{capitalize query.name tillIndex=1}} | undefined> {
return this._conn.getRepository({{capitalize query.name tillIndex=1}}) return this._conn.getRepository({{capitalize query.name tillIndex=1}})
.createQueryBuilder('{{query.name}}') .findOne({
.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}}`, {
blockHash, blockHash,
contractAddress contractAddress{{#if query.params.length}},{{/if}}
{{~#each query.params}}, {{#each query.params}}
{{this.name}} {{this.name}}{{#unless @last}},{{/unless}}
{{~/each}} {{/each}}
}) })
.getOne();
} }
{{/each}} {{/each}}
@ -66,7 +69,124 @@ export class Database {
} }
{{/each}} {{/each}}
_getColumn (entityName: string, propertyName: string) { async getContract (address: string): Promise<Contract | undefined> {
return this._conn.getMetadata(entityName).findColumnWithPropertyName(propertyName)?.databaseName const repo = this._conn.getRepository(Contract);
return this._baseDatabase.getContract(repo, address);
}
async createTransactionRunner (): Promise<QueryRunner> {
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<Array<Event>> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEventsInRange(repo, fromBlockNumber, toBlockNumber);
}
async saveEventEntity (queryRunner: QueryRunner, entity: Event): Promise<Event> {
const repo = queryRunner.manager.getRepository(Event);
return this._baseDatabase.saveEventEntity(repo, entity);
}
async getBlockEvents (blockHash: string, where: FindConditions<Event>): Promise<Event[]> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getBlockEvents(repo, blockHash, where);
}
async saveEvents (queryRunner: QueryRunner, block: DeepPartial<BlockProgress>, events: DeepPartial<Event>[]): Promise<void> {
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<void> {
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<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusIndexedBlock(repo, blockHash, blockNumber);
}
async updateSyncStatusCanonicalBlock (queryRunner: QueryRunner, blockHash: string, blockNumber: number): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusCanonicalBlock(repo, blockHash, blockNumber);
}
async updateSyncStatusChainHead (queryRunner: QueryRunner, blockHash: string, blockNumber: number): Promise<SyncStatus> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.updateSyncStatusChainHead(repo, blockHash, blockNumber);
}
async getSyncStatus (queryRunner: QueryRunner): Promise<SyncStatus | undefined> {
const repo = queryRunner.manager.getRepository(SyncStatus);
return this._baseDatabase.getSyncStatus(repo);
}
async getEvent (id: string): Promise<Event | undefined> {
const repo = this._conn.getRepository(Event);
return this._baseDatabase.getEvent(repo, id);
}
async getBlocksAtHeight (height: number, isPruned: boolean): Promise<BlockProgress[]> {
const repo = this._conn.getRepository(BlockProgress);
return this._baseDatabase.getBlocksAtHeight(repo, height, isPruned);
}
async markBlocksAsPruned (queryRunner: QueryRunner, blocks: BlockProgress[]): Promise<void> {
const repo = queryRunner.manager.getRepository(BlockProgress);
return this._baseDatabase.markBlocksAsPruned(repo, blocks);
}
async getBlockProgress (blockHash: string): Promise<BlockProgress | undefined> {
const repo = this._conn.getRepository(BlockProgress);
return this._baseDatabase.getBlockProgress(repo, blockHash);
}
async updateBlockProgress (queryRunner: QueryRunner, blockHash: string, lastProcessedEventIndex: number): Promise<void> {
const repo = queryRunner.manager.getRepository(BlockProgress);
return this._baseDatabase.updateBlockProgress(repo, blockHash, lastProcessedEventIndex);
}
async removeEntities<Entity> (queryRunner: QueryRunner, entity: new () => Entity, findConditions?: FindConditions<Entity>): Promise<void> {
return this._baseDatabase.removeEntities(queryRunner, entity, findConditions);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseDatabase.getAncestorAtDepth(blockHash, depth);
}
_getPropertyColumnMapForEntity (entityName: string): Map<string, string> {
return this._conn.getMetadata(entityName).ownColumns.reduce((acc, curr) => {
return acc.set(curr.propertyName, curr.databaseName);
}, new Map<string, string>());
}
_setPropColMaps () {
{{#each queries as | query |}}
this._propColMaps['{{capitalize query.name tillIndex=1}}'] = this._getPropertyColumnMapForEntity('{{capitalize query.name tillIndex=1}}');
{{/each}}
} }
} }

View File

@ -2,29 +2,41 @@
// Copyright 2021 Vulcanize, Inc. // 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() @Entity()
{{#if indexOn.columns}} {{#each indexOn as | index |}}
@Index(['blockHash', 'contractAddress' {{#if index.columns}}
{{~#each indexOn.columns}}, '{{this}}' @Index([
{{~/each}}], { unique: {{indexOn.unique}} }) {{~#each index.columns}}'{{this}}'
{{~#unless @last}}, {{/unless}}
{{~/each}}]
{{~#if index.unique}}, { unique: true }{{/if}})
{{/if}} {{/if}}
export class {{className}} { {{/each}}
export class {{className}} {{~#if implements}} implements {{implements}} {{~/if}} {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id!: number; id!: number;
@Column('varchar', { length: 66 })
blockHash!: string;
@Column('varchar', { length: 42 })
contractAddress!: string;
{{#each columns as | column |}} {{#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}}; {{column.name}}!: {{column.tsType}};
{{~#unless @last}}
{{/unless}}
{{/each}} {{/each}}
@Column('text', { nullable: true })
proof!: string;
} }

View File

@ -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<any> {
return this._pubsub.asyncIterator([EVENT]);
}
getBlockProgressEventIterator (): AsyncIterator<any> {
return this._baseEventWatcher.getBlockProgressEventIterator();
}
async start (): Promise<void> {
assert(!this._subscription, 'subscription already started');
await this.watchBlocksAtChainHead();
await this.initBlockProcessingOnCompleteHandler();
await this.initEventProcessingOnCompleteHandler();
}
async stop (): Promise<void> {
this._baseEventWatcher.stop();
}
async watchBlocksAtChainHead (): Promise<void> {
log('Started watching upstream blocks...');
this._subscription = await this._ethClient.watchBlocks(async (value) => {
await this._baseEventWatcher.blocksHandler(value);
});
}
async initBlockProcessingOnCompleteHandler (): Promise<void> {
this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => {
await this._baseEventWatcher.blockProcessingCompleteHandler(job);
});
}
async initEventProcessingOnCompleteHandler (): Promise<void> {
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<void> {
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
}
});
}
}
}

View File

@ -5,37 +5,60 @@
import assert from 'assert'; import assert from 'assert';
import debug from 'debug'; import debug from 'debug';
import { JsonFragment } from '@ethersproject/abi'; import { JsonFragment } from '@ethersproject/abi';
import { DeepPartial } from 'typeorm';
import JSONbig from 'json-bigint'; import JSONbig from 'json-bigint';
import { BigNumber, ethers, Contract } from 'ethers'; import { BigNumber, ethers } from 'ethers';
import { BaseProvider } from '@ethersproject/providers'; import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@vulcanize/ipld-eth-client'; import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper'; import { getStorageValue, GetStorageAt, StorageLayout } from '@vulcanize/solidity-mapper';
import { ValueResult } from '@vulcanize/util'; import { EventInterface, Indexer as BaseIndexer, ValueResult, UNKNOWN_EVENT_NAME } from '@vulcanize/util';
import { Database } from './database'; 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'; import artifacts from './artifacts/{{inputFileName}}.json';
const log = debug('vulcanize:indexer'); 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 { export class Indexer {
_db: Database _db: Database
_ethClient: EthClient _ethClient: EthClient
_getStorageAt: GetStorageAt
_ethProvider: BaseProvider _ethProvider: BaseProvider
_baseIndexer: BaseIndexer
_abi: JsonFragment[] _abi: JsonFragment[]
_storageLayout: StorageLayout _storageLayout: StorageLayout
_contract: ethers.utils.Interface _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(db);
assert(ethClient); assert(ethClient);
this._db = db; this._db = db;
this._ethClient = ethClient; this._ethClient = ethClient;
this._ethProvider = ethProvider; 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; const { abi, storageLayout } = artifacts;
@ -48,9 +71,43 @@ export class Indexer {
this._contract = new ethers.utils.Interface(this._abi); 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 |}} {{#each queries as | query |}}
async {{query.name}} (blockHash: string, contractAddress: string async {{query.name}} (blockHash: string, contractAddress: string
{{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}}): Promise<ValueResult> { {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}}): Promise<ValueResult> {
const entity = await this._db.get{{capitalize query.name tillIndex=1}}({ blockHash, contractAddress const entity = await this._db.get{{capitalize query.name tillIndex=1}}({ blockHash, contractAddress
{{~#each query.params}}, {{this.name~}} {{~/each}} }); {{~#each query.params}}, {{this.name~}} {{~/each}} });
if (entity) { if (entity) {
@ -64,19 +121,13 @@ export class Indexer {
log('{{query.name}}: db miss, fetching from upstream server'); log('{{query.name}}: db miss, fetching from upstream server');
const contract = new Contract(contractAddress, this._abi, this._ethProvider); {{#if (compare query.mode @root.constants.MODE_ETH_CALL)}}
let value = null; const contract = new ethers.Contract(contractAddress, this._abi, this._ethProvider);
{{~#if query.params}}
const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); const { block: { number } } = await this._ethClient.getBlockByHash(blockHash);
const blockNumber = BigNumber.from(number).toNumber(); 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 }); {{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockNumber });
{{else}}
value = await contract.{{query.name}}();
{{/if}}
{{~#if (compare query.returnType 'bigint')}} {{~#if (compare query.returnType 'bigint')}}
@ -85,14 +136,203 @@ export class Indexer {
{{/if}} {{/if}}
const result: ValueResult = { value }; 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 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; return result;
} }
{{#unless @last}}
{{/unless}} {{/each}}
async _getStorageValue (storageLayout: StorageLayout, blockHash: string, contractAddress: string, variable: string, ...mappingKeys: any[]): Promise<ValueResult> {
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}} {{/each}}
} }
return { eventName, eventInfo };
}
async getEventsByFilter (blockHash: string, contract: string, name: string | null): Promise<Array<Event>> {
return this._baseIndexer.getEventsByFilter(blockHash, contract, name);
}
async isWatchedContract (address : string): Promise<Contract | undefined> {
return this._baseIndexer.isWatchedContract(address);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
return this._baseIndexer.getSyncStatus();
}
async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber);
}
async updateSyncStatusChainHead (blockHash: string, blockNumber: number): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber);
}
async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber);
}
async getBlock (blockHash: string): Promise<any> {
return this._baseIndexer.getBlock(blockHash);
}
async getEvent (id: string): Promise<Event | undefined> {
return this._baseIndexer.getEvent(id);
}
async getBlockProgress (blockHash: string): Promise<BlockProgress | undefined> {
return this._baseIndexer.getBlockProgress(blockHash);
}
async getBlocksAtHeight (height: number, isPruned: boolean): Promise<BlockProgress[]> {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async getOrFetchBlockEvents (block: DeepPartial<BlockProgress>): Promise<Array<EventInterface>> {
return this._baseIndexer.getOrFetchBlockEvents(block, this._fetchAndSaveEvents.bind(this));
}
async getBlockEvents (blockHash: string): Promise<Array<Event>> {
return this._baseIndexer.getBlockEvents(blockHash);
}
async markBlocksAsPruned (blocks: BlockProgress[]): Promise<void> {
return this._baseIndexer.markBlocksAsPruned(blocks);
}
async updateBlockProgress (blockHash: string, lastProcessedEventIndex: number): Promise<void> {
return this._baseIndexer.updateBlockProgress(blockHash, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
}
async _fetchAndSaveEvents ({ blockHash }: DeepPartial<BlockProgress>): Promise<void> {
assert(blockHash);
let { block, logs } = await this._ethClient.getLogs({ blockHash });
const dbEvents: Array<DeepPartial<Event>> = [];
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();
}
}
}

View File

@ -35,12 +35,20 @@ export const createResolvers = async (indexer: Indexer): Promise<any> => {
{{~#each this.params}}, {{this.name~}} {{/each}}); {{~#each this.params}}, {{this.name~}} {{/each}});
return indexer.{{this.name}}(blockHash, contractAddress return indexer.{{this.name}}(blockHash, contractAddress
{{~#each this.params}}, {{this.name~}} {{/each}}); {{~#each this.params}}, {{this.name~}} {{/each}});
} },
{{~#unless @last}},
{{/unless}}
{{/each}} {{/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));
}
} }
}; };
}; };

View File

@ -40,7 +40,7 @@ export const main = async (): Promise<any> => {
assert(config.server, 'Missing server config'); assert(config.server, 'Missing server config');
const { host, port, mode } = config.server; const { host, port } = config.server;
const { upstream, database: dbConfig } = config; const { upstream, database: dbConfig } = config;
@ -66,7 +66,7 @@ export const main = async (): Promise<any> => {
// Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. // 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 // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries
const pubsub = new PubSub(); const pubsub = new PubSub();
const indexer = new Indexer(db, ethClient, ethProvider, mode); const indexer = new Indexer(db, ethClient, ethProvider);
const resolvers = await createResolvers(indexer); const resolvers = await createResolvers(indexer);

View File

@ -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';

View File

@ -3,6 +3,12 @@
// //
import assert from 'assert'; 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. * 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. * @param options Handlebars options parameter. `options.hash.operator`: operator to be used for comparison.
* @returns Result of the 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"); assert(lvalue && rvalue, "Handlerbars Helper 'compare' needs at least 2 parameters");
const operator = options.hash.operator || '==='; 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. * @param options Handlebars options parameter. `options.hash.tillIndex`: index till which to capitalize the string.
* @returns The modified 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 tillIndex = options.hash.tillIndex || value.length;
const result = `${value.slice(0, tillIndex).toUpperCase()}${value.slice(tillIndex, value.length)}`; const result = `${value.slice(0, tillIndex).toUpperCase()}${value.slice(tillIndex, value.length)}`;

View File

@ -5,11 +5,12 @@
import { Writable } from 'stream'; import { Writable } from 'stream';
import { Database } from './database'; import { Database } from './database';
import { Param } from './utils/types';
import { MODE_ETH_CALL, MODE_STORAGE } from './utils/constants';
import { Entity } from './entity'; import { Entity } from './entity';
import { Indexer } from './indexer'; import { Indexer } from './indexer';
import { Resolvers } from './resolvers'; import { Resolvers } from './resolvers';
import { Schema } from './schema'; import { Schema } from './schema';
import { Param } from './utils/types';
export class Visitor { export class Visitor {
_schema: Schema; _schema: Schema;
@ -42,7 +43,7 @@ export class Visitor {
this._schema.addQuery(name, params, returnType); this._schema.addQuery(name, params, returnType);
this._resolvers.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._entity.addQuery(name, params, returnType);
this._database.addQuery(name, params, returnType); this._database.addQuery(name, params, returnType);
} }
@ -55,8 +56,7 @@ export class Visitor {
stateVariableDeclarationVisitor (node: any): void { stateVariableDeclarationVisitor (node: any): void {
// TODO Handle multiples variables in a single line. // TODO Handle multiples variables in a single line.
// TODO Handle array types. // TODO Handle array types.
let name: string = node.variables[0].name; const name: string = node.variables[0].name;
name = name.startsWith('_') ? name.substring(1) : name;
const params: Param[] = []; const params: Param[] = [];
@ -75,7 +75,7 @@ export class Visitor {
this._schema.addQuery(name, params, returnType); this._schema.addQuery(name, params, returnType);
this._resolvers.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._entity.addQuery(name, params, returnType);
this._database.addQuery(name, params, returnType); this._database.addQuery(name, params, returnType);
} }
@ -91,6 +91,7 @@ export class Visitor {
}); });
this._schema.addEventType(name, params); this._schema.addEventType(name, params);
this._indexer.addEvent(name, params);
} }
/** /**

View File

@ -5,8 +5,6 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne } from 'typeorm';
import { BlockProgress } from './BlockProgress'; import { BlockProgress } from './BlockProgress';
export const UNKNOWN_EVENT_NAME = '__unknown__';
@Entity() @Entity()
// Index to query all events for a contract efficiently. // Index to query all events for a contract efficiently.
@Index(['block', 'contract']) @Index(['block', 'contract'])

View File

@ -11,11 +11,12 @@ import {
JobQueue, JobQueue,
EventWatcher as BaseEventWatcher, EventWatcher as BaseEventWatcher,
QUEUE_BLOCK_PROCESSING, QUEUE_BLOCK_PROCESSING,
QUEUE_EVENT_PROCESSING QUEUE_EVENT_PROCESSING,
UNKNOWN_EVENT_NAME
} from '@vulcanize/util'; } from '@vulcanize/util';
import { Indexer } from './indexer'; import { Indexer } from './indexer';
import { Event, UNKNOWN_EVENT_NAME } from './entity/Event'; import { Event } from './entity/Event';
const EVENT = 'event'; const EVENT = 'event';

View File

@ -12,10 +12,10 @@ import { BaseProvider } from '@ethersproject/providers';
import { EthClient } from '@vulcanize/ipld-eth-client'; import { EthClient } from '@vulcanize/ipld-eth-client';
import { StorageLayout } from '@vulcanize/solidity-mapper'; 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 { Database } from './database';
import { Event, UNKNOWN_EVENT_NAME } from './entity/Event'; import { Event } from './entity/Event';
import { fetchTokenDecimals, fetchTokenName, fetchTokenSymbol, fetchTokenTotalSupply } from './utils'; import { fetchTokenDecimals, fetchTokenName, fetchTokenSymbol, fetchTokenTotalSupply } from './utils';
import { SyncStatus } from './entity/SyncStatus'; import { SyncStatus } from './entity/SyncStatus';
import artifacts from './artifacts/ERC20.json'; import artifacts from './artifacts/ERC20.json';

View File

@ -12,7 +12,7 @@ import { ValueTransformer } from 'typeorm';
export const wait = async (time: number): Promise<void> => new Promise(resolve => setTimeout(resolve, time)); export const wait = async (time: number): Promise<void> => 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 = { export const decimalTransformer: ValueTransformer = {
to: (value?: Decimal) => { to: (value?: Decimal) => {
@ -30,3 +30,23 @@ export const decimalTransformer: ValueTransformer = {
return value; 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;
}
};