mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-01-07 20:08:06 +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:
|
* 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:
|
||||||
@ -43,7 +50,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
* Generate a watcher from a contract file:
|
* Generate a watcher from a contract file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn codegen --input-file ../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --contract-name ERC20 --output-folder ../demo-erc20-watcher --mode eth_call
|
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.
|
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:
|
* Generate a watcher from a flattened contract file from an URL:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../demo-erc721-watcher --mode eth_call
|
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
|
## 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 { 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,34 +37,95 @@ 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'],
|
||||||
|
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.push({
|
||||||
|
name: 'contractAddress',
|
||||||
entityObject.columns = params.map((param) => {
|
pgType: 'varchar',
|
||||||
const length = param.type === 'address' ? 42 : null;
|
tsType: 'string',
|
||||||
const name = param.name;
|
columnType: 'Column',
|
||||||
|
columnOptions: [
|
||||||
const tsType = getTsForSol(param.type);
|
{
|
||||||
assert(tsType);
|
option: 'length',
|
||||||
|
value: 42
|
||||||
const pgType = getPgForTs(tsType);
|
}
|
||||||
assert(pgType);
|
]
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
pgType,
|
|
||||||
tsType,
|
|
||||||
length
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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 { 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 => {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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]
|
[server]
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = 3008
|
port = 3008
|
||||||
mode = "eth_call"
|
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
type = "postgres"
|
type = "postgres"
|
||||||
|
@ -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}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
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 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}}
|
{{/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}});
|
{{~#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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
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 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)}`;
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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'])
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user