mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-01-06 19:38:05 +00:00
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:
parent
92b7967895
commit
482be8cfaa
@ -13,27 +13,34 @@
|
||||
* Run the following command to generate a watcher from a contract file:
|
||||
|
||||
```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).
|
||||
* `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`.
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
21
packages/codegen/src/events.ts
Normal file
21
packages/codegen/src/events.ts
Normal 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);
|
||||
}
|
@ -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<void> => {
|
||||
const argv = await yargs(hideBin(process.argv))
|
||||
@ -45,8 +45,8 @@ const main = async (): Promise<void> => {
|
||||
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 => {
|
||||
|
@ -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<any>;
|
||||
_events: Array<any>;
|
||||
_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<Param>, returnType: string): void {
|
||||
addQuery (mode: string, name: string, params: Array<Param>, 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<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.
|
||||
* @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);
|
||||
}
|
||||
|
@ -217,6 +217,16 @@ export class Schema {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._composer.Query.addFields({
|
||||
eventsInRange: {
|
||||
type: [this._composer.getOTC('ResultEvent').NonNull],
|
||||
args: {
|
||||
fromBlockNumber: 'Int!',
|
||||
toBlockNumber: 'Int!'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,6 @@
|
||||
[server]
|
||||
host = "127.0.0.1"
|
||||
port = 3008
|
||||
mode = "eth_call"
|
||||
|
||||
[database]
|
||||
type = "postgres"
|
||||
|
@ -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<string, string>; }
|
||||
|
||||
constructor (config: ConnectionOptions) {
|
||||
assert(config);
|
||||
@ -26,10 +31,12 @@ export class Database {
|
||||
};
|
||||
|
||||
this._baseDatabase = new BaseDatabase(this._config);
|
||||
this._propColMaps = {};
|
||||
}
|
||||
|
||||
async init (): Promise<void> {
|
||||
this._conn = await this._baseDatabase.init();
|
||||
this._setPropColMaps();
|
||||
}
|
||||
|
||||
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~}}: {{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<Contract | undefined> {
|
||||
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}}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
113
packages/codegen/src/templates/events-template.handlebars
Normal file
113
packages/codegen/src/templates/events-template.handlebars
Normal 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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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<ValueResult> {
|
||||
|
||||
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<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}}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,12 +35,20 @@ export const createResolvers = async (indexer: Indexer): Promise<any> => {
|
||||
{{~#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));
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -40,7 +40,7 @@ export const main = async (): Promise<any> => {
|
||||
|
||||
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<any> => {
|
||||
// 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);
|
||||
|
||||
|
7
packages/codegen/src/utils/constants.ts
Normal file
7
packages/codegen/src/utils/constants.ts
Normal 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';
|
@ -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)}`;
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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'])
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -12,7 +12,7 @@ import { ValueTransformer } from 'typeorm';
|
||||
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 = {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user