Codegen flag for watcher kind and eventsInRange query support (#254)

* Add watcher kind argument.

* Process eventsInRange query.
This commit is contained in:
prathamesh0 2021-09-28 10:22:21 +05:30 committed by GitHub
parent 338cef9954
commit 8e3093c684
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 83 additions and 40 deletions

View File

@ -13,7 +13,7 @@
* 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 | all] --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] --kind [lazy | active]
```
* `input-file`(alias: `i`): Input contract file path or an URL (required).
@ -21,24 +21,25 @@
* `output-folder`(alias: `o`): Output folder path. (logs output using `stdout` if not provided).
* `mode`(alias: `m`): Code generation mode (default: `all`).
* `flatten`(alias: `f`): Flatten the input contract file (default: `true`).
* `kind` (alias: `k`): Kind of watcher (default; `active`).
**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.
Generate code in both `eth_call` and `storage` mode, `active` kind.
```bash
yarn codegen --input-file ./test/examples/contracts/ERC20.sol --contract-name ERC20 --output-folder ../my-erc20-watcher --mode all
yarn codegen --input-file ./test/examples/contracts/ERC20.sol --contract-name ERC20 --output-folder ../my-erc20-watcher --mode all --kind active
```
Generate code in eth_call mode using a contract provided by URL.
Generate code in `eth_call` mode using a contract provided by an 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.
Generate code in `storage` mode, `lazy` kind.
```bash
yarn codegen --input-file ./test/examples/contracts/ERC721.sol --contract-name ERC721 --output-folder ../my-erc721-watcher --mode storage
yarn codegen --input-file ./test/examples/contracts/ERC721.sol --contract-name ERC721 --output-folder ../my-erc721-watcher --mode storage --kind lazy
```
## Demo
@ -60,7 +61,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
yarn codegen --input-file ../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --contract-name ERC20 --output-folder ../demo-erc20-watcher --mode eth_call --kind active
```
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.
@ -68,7 +69,7 @@
* 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
yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../demo-erc721-watcher --mode all --kind active
```
## References

View File

@ -11,13 +11,15 @@ const TEMPLATE_FILE = './templates/config-template.handlebars';
/**
* Writes the config file generated from a template to a stream.
* @param watcherKind Watcher kind to be passed to the template.
* @param folderName Watcher folder name to be passed to the template.
* @param outStream A writable output stream to write the config file to.
*/
export function exportConfig (folderName: string, outStream: Writable): void {
export function exportConfig (watcherKind: string, folderName: string, outStream: Writable): void {
const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString();
const template = Handlebars.compile(templateString);
const config = template({
watcherKind,
folderName
});
outStream.write(config);

View File

@ -10,6 +10,7 @@ import { hideBin } from 'yargs/helpers';
import { flatten } from '@poanet/solidity-flattener';
import { parse, visit } from '@solidity-parser/parser';
import { KIND_ACTIVE, KIND_LAZY } from '@vulcanize/util';
import { MODE_ETH_CALL, MODE_STORAGE, MODE_ALL } from './utils/constants';
import { Visitor } from './visitor';
@ -51,6 +52,13 @@ const main = async (): Promise<void> => {
default: MODE_ALL,
choices: [MODE_ETH_CALL, MODE_STORAGE, MODE_ALL]
})
.option('kind', {
alias: 'k',
describe: 'Watcher kind.',
type: 'string',
default: KIND_ACTIVE,
choices: [KIND_ACTIVE, KIND_LAZY]
})
.option('flatten', {
alias: 'f',
describe: 'Flatten the input contract file.',
@ -146,7 +154,7 @@ function generateWatcher (data: string, visitor: Visitor, argv: any) {
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'environments/local.toml'))
: process.stdout;
exportConfig(path.basename(outputDir), outStream);
exportConfig(argv.kind, path.basename(outputDir), outStream);
outStream = outputDir
? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${inputFileName}.json`))

View File

@ -1,6 +1,7 @@
[server]
host = "127.0.0.1"
port = 3008
kind = "{{watcherKind}}"
[database]
type = "postgres"

View File

@ -45,7 +45,7 @@ export class Database {
{{#each queries as | query |}}
// eslint-disable-next-line camelcase
{{#if (banTypeCheck (capitalize query.name tillIndex=1)) }}
{{#if (reservedNameCheck (capitalize query.name tillIndex=1)) }}
// eslint-disable-next-line @typescript-eslint/ban-types
{{/if}}
async get{{capitalize query.name tillIndex=1}} ({ blockHash, contractAddress
@ -65,7 +65,7 @@ export class Database {
{{~#each queries as | query |}}
// eslint-disable-next-line camelcase
{{#if (banTypeCheck (capitalize query.name tillIndex=1)) }}
{{#if (reservedNameCheck (capitalize query.name tillIndex=1)) }}
// eslint-disable-next-line @typescript-eslint/ban-types
{{/if}}
async save{{capitalize query.name tillIndex=1}} ({ blockHash, contractAddress

View File

@ -201,6 +201,14 @@ export class Indexer {
return this._baseIndexer.isWatchedContract(address);
}
async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> {
return this._baseIndexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
}
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<Event>> {
return this._baseIndexer.getEventsInRange(fromBlockNumber, toBlockNumber);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
return this._baseIndexer.getSyncStatus();
}

View File

@ -15,7 +15,9 @@
createdb {{folderName}}
```
* Create database for the job queue and enable the `pgcrypto` extension on them (https://github.com/timgit/pg-boss/blob/master/docs/usage.md#intro):
* If the watcher is an `active` watcher:
Create database for the job queue and enable the `pgcrypto` extension on them (https://github.com/timgit/pg-boss/blob/master/docs/usage.md#intro):
```
createdb {{folderName}}-job-queue
@ -45,16 +47,18 @@
yarn server
```
* Run the job-runner:
```bash
yarn job-runner
```
GQL console: http://localhost:3008/graphql
* To watch a contract:
* If the watcher is an `active` watcher:
```bash
yarn watch:contract --address CONTRACT_ADDRESS --kind {{contractName}} --starting-block BLOCK_NUMBER
```
* Run the job-runner:
```bash
yarn job-runner
```
* To watch a contract:
```bash
yarn watch:contract --address CONTRACT_ADDRESS --kind {{contractName}} --starting-block BLOCK_NUMBER
```

View File

@ -48,6 +48,18 @@ export const createResolvers = async (indexer: Indexer): Promise<any> => {
const events = await indexer.getEventsByFilter(blockHash, contractAddress, name);
return events.map(event => indexer.getResultEvent(event));
},
eventsInRange: async (_: any, { fromBlockNumber, toBlockNumber }: { fromBlockNumber: number, toBlockNumber: number }) => {
log('eventsInRange', fromBlockNumber, toBlockNumber);
const { expected, actual } = await indexer.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
if (expected !== actual) {
throw new Error(`Range not available, expected ${expected}, got ${actual} blocks in range`);
}
const events = await indexer.getEventsInRange(fromBlockNumber, toBlockNumber);
return events.map(event => indexer.getResultEvent(event));
}
}
};

View File

@ -17,7 +17,7 @@ import { getDefaultProvider } from 'ethers';
import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { DEFAULT_CONFIG_PATH, getConfig, JobQueue } from '@vulcanize/util';
import { DEFAULT_CONFIG_PATH, getConfig, JobQueue, KIND_ACTIVE } from '@vulcanize/util';
import { createResolvers } from './resolvers';
import { Indexer } from './indexer';
@ -41,7 +41,7 @@ export const main = async (): Promise<any> => {
assert(config.server, 'Missing server config');
const { host, port } = config.server;
const { host, port, kind: watcherKind } = config.server;
const { upstream, database: dbConfig, jobQueue: jobQueueConfig } = config;
@ -64,20 +64,23 @@ export const main = async (): Promise<any> => {
const ethProvider = getDefaultProvider(rpcProviderEndpoint);
// 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);
assert(jobQueueConfig, 'Missing job queue config');
const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig;
assert(dbConnectionString, 'Missing job queue db connection string');
if (watcherKind === KIND_ACTIVE) {
// 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 jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs });
await jobQueue.start();
assert(jobQueueConfig, 'Missing job queue config');
const { dbConnectionString, maxCompletionLagInSecs } = jobQueueConfig;
assert(dbConnectionString, 'Missing job queue db connection string');
const eventWatcher = new EventWatcher(ethClient, indexer, pubsub, jobQueue);
await eventWatcher.start();
const jobQueue = new JobQueue({ dbConnectionString, maxCompletionLag: maxCompletionLagInSecs });
await jobQueue.start();
const eventWatcher = new EventWatcher(ethClient, indexer, pubsub, jobQueue);
await eventWatcher.start();
}
const resolvers = await createResolvers(indexer);

View File

@ -5,12 +5,12 @@
import assert from 'assert';
import Handlebars from 'handlebars';
import { bannedTypes } from './types';
import { reservedNames } from './types';
export function registerHandlebarHelpers (): void {
Handlebars.registerHelper('compare', compareHelper);
Handlebars.registerHelper('capitalize', capitalizeHelper);
Handlebars.registerHelper('banTypeCheck', banTypeCheckHelper);
Handlebars.registerHelper('reservedNameCheck', reservedNameCheckHelper);
}
/**
@ -54,6 +54,6 @@ function capitalizeHelper (value: string, options: any): string {
return result;
}
function banTypeCheckHelper (value: string): boolean {
return bannedTypes.has(value);
function reservedNameCheckHelper (value: string): boolean {
return reservedNames.has(value);
}

View File

@ -7,6 +7,6 @@ export interface Param {
type: string;
}
export const bannedTypes = new Set([
export const reservedNames = new Set([
'Symbol'
]);

View File

@ -23,6 +23,7 @@ export interface Config {
host: string;
port: number;
mode: string;
kind: string;
};
database: ConnectionOptions;
upstream: {

View File

@ -14,3 +14,6 @@ export const JOB_KIND_PRUNE = 'prune';
export const DEFAULT_CONFIG_PATH = 'environments/local.toml';
export const UNKNOWN_EVENT_NAME = '__unknown__';
export const KIND_ACTIVE = 'active';
export const KIND_LAZY = 'lazy';