ERC20 watcher based on eth_call (#165)

* Implement eth_call for ERC20 totalSupply.

* Use eth_call for erc20-watcher.

* Implement fallback for ERC20 symbol method call.

* Implement fallback for ERC20 name and totalSupply.

* implement fallback for erc20 decimals method.

* Lint fixes.

Co-authored-by: nabarun <nabarun@deepstacksoft.com>
This commit is contained in:
Ashwin Phatak 2021-07-28 10:04:07 +05:30 committed by GitHub
parent fd1ab0780c
commit c677e5942c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 353 additions and 38 deletions

View File

@ -1,6 +1,7 @@
[server] [server]
host = "127.0.0.1" host = "127.0.0.1"
port = 3001 port = 3001
mode = "eth_call"
[database] [database]
type = "postgres" type = "postgres"
@ -25,6 +26,7 @@
[upstream.ethServer] [upstream.ethServer]
gqlApiEndpoint = "http://127.0.0.1:8082/graphql" gqlApiEndpoint = "http://127.0.0.1:8082/graphql"
gqlPostgraphileEndpoint = "http://127.0.0.1:5000/graphql" gqlPostgraphileEndpoint = "http://127.0.0.1:5000/graphql"
rpcProviderEndpoint = "http://127.0.0.1:8545"
[upstream.cache] [upstream.cache]
name = "requests" name = "requests"

View File

@ -0,0 +1,17 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

View File

@ -0,0 +1,17 @@
[
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}
]

View File

@ -21,6 +21,6 @@ export class Allowance {
@Column('numeric') @Column('numeric')
value!: bigint; value!: bigint;
@Column('text') @Column('text', { nullable: true })
proof!: string; proof!: string;
} }

View File

@ -18,6 +18,6 @@ export class Balance {
@Column('numeric') @Column('numeric')
value!: bigint; value!: bigint;
@Column('text') @Column('text', { nullable: true })
proof!: string; proof!: string;
} }

View File

@ -39,11 +39,11 @@ export class EventWatcher {
if (isWatchedContract) { if (isWatchedContract) {
// TODO: Move processing to background task runner. // TODO: Move processing to background task runner.
const { ethTransactionCidByTxId: { ethHeaderCidByHeaderId: { blockHash } } } = receipt; const { ethTransactionCidByTxId: { ethHeaderCidByHeaderId: { blockHash, blockNumber } } } = receipt;
await this._indexer.getEvents(blockHash, contractAddress, null); await this._indexer.getEvents(blockHash, contractAddress, null);
// Trigger other indexer methods based on event topic. // Trigger other indexer methods based on event topic.
await this._indexer.processEvent(blockHash, contractAddress, receipt, logIndex); await this._indexer.processEvent(blockHash, blockNumber, contractAddress, receipt, logIndex);
} }
} }
} }

View File

@ -4,7 +4,8 @@ import { invert } from 'lodash';
import { JsonFragment } from '@ethersproject/abi'; import { JsonFragment } from '@ethersproject/abi';
import { DeepPartial } from 'typeorm'; import { DeepPartial } from 'typeorm';
import JSONbig from 'json-bigint'; import JSONbig from 'json-bigint';
import { ethers } from 'ethers'; import { BigNumber, ethers } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { PubSub } from 'apollo-server-express'; import { PubSub } from 'apollo-server-express';
import { EthClient, topictoAddress } from '@vulcanize/ipld-eth-client'; import { EthClient, topictoAddress } from '@vulcanize/ipld-eth-client';
@ -12,9 +13,12 @@ import { getEventNameTopics, getStorageValue, GetStorageAt, StorageLayout } from
import { Database } from './database'; import { Database } from './database';
import { Event } from './entity/Event'; import { Event } from './entity/Event';
import { fetchTokenDecimals, fetchTokenName, fetchTokenSymbol, fetchTokenTotalSupply } from './utils';
const log = debug('vulcanize:indexer'); const log = debug('vulcanize:indexer');
const ETH_CALL_MODE = 'eth_call';
interface Artifacts { interface Artifacts {
abi: JsonFragment[]; abi: JsonFragment[];
storageLayout: StorageLayout; storageLayout: StorageLayout;
@ -22,7 +26,7 @@ interface Artifacts {
export interface ValueResult { export interface ValueResult {
value: string | bigint; value: string | bigint;
proof: { proof?: {
data: string; data: string;
} }
} }
@ -36,7 +40,7 @@ type EventsResult = Array<{
value?: BigInt; value?: BigInt;
__typename: string; __typename: string;
} }
proof: string; proof?: string;
}> }>
export class Indexer { export class Indexer {
@ -44,12 +48,14 @@ export class Indexer {
_ethClient: EthClient _ethClient: EthClient
_pubsub: PubSub _pubsub: PubSub
_getStorageAt: GetStorageAt _getStorageAt: GetStorageAt
_ethProvider: BaseProvider
_abi: JsonFragment[] _abi: JsonFragment[]
_storageLayout: StorageLayout _storageLayout: StorageLayout
_contract: ethers.utils.Interface _contract: ethers.utils.Interface
_serverMode: string
constructor (db: Database, ethClient: EthClient, pubsub: PubSub, artifacts: Artifacts) { constructor (db: Database, ethClient: EthClient, ethProvider: BaseProvider, pubsub: PubSub, artifacts: Artifacts, serverMode: string) {
assert(db); assert(db);
assert(ethClient); assert(ethClient);
assert(pubsub); assert(pubsub);
@ -62,8 +68,10 @@ export class Indexer {
this._db = db; this._db = db;
this._ethClient = ethClient; this._ethClient = ethClient;
this._ethProvider = ethProvider;
this._pubsub = pubsub; this._pubsub = pubsub;
this._getStorageAt = this._ethClient.getStorageAt.bind(this._ethClient); this._getStorageAt = this._ethClient.getStorageAt.bind(this._ethClient);
this._serverMode = serverMode;
this._abi = abi; this._abi = abi;
this._storageLayout = storageLayout; this._storageLayout = storageLayout;
@ -76,10 +84,18 @@ export class Indexer {
} }
async totalSupply (blockHash: string, token: string): Promise<ValueResult> { async totalSupply (blockHash: string, token: string): Promise<ValueResult> {
const result = await this._getStorageValue(blockHash, token, '_totalSupply'); let result: ValueResult;
if (this._serverMode === ETH_CALL_MODE) {
const value = await fetchTokenTotalSupply(this._ethProvider, token);
result = { value };
} else {
result = await this._getStorageValue(blockHash, token, '_totalSupply');
}
// https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-521460510 // https://github.com/GoogleChromeLabs/jsbi/issues/30#issuecomment-521460510
// log(JSONbig.stringify(result, null, 2)); log(JSONbig.stringify(result, null, 2));
return result; return result;
} }
@ -96,9 +112,25 @@ export class Indexer {
} }
log('balanceOf: db miss, fetching from upstream server'); log('balanceOf: db miss, fetching from upstream server');
const result = await this._getStorageValue(blockHash, token, '_balances', owner); let result: ValueResult;
// log(JSONbig.stringify(result, null, 2)); if (this._serverMode === ETH_CALL_MODE) {
const contract = new ethers.Contract(token, this._abi, this._ethProvider);
const { block } = await this._ethClient.getBlockByHash(blockHash);
const { number } = block;
const blockNumber = BigNumber.from(number).toNumber();
// eth_call doesnt support calling method by blockHash https://eth.wiki/json-rpc/API#the-default-block-parameter
const value = await contract.balanceOf(owner, { blockTag: blockNumber });
result = {
value: BigInt(value.toString())
};
} else {
result = await this._getStorageValue(blockHash, token, '_balances', owner);
}
log(JSONbig.stringify(result, null, 2));
const { value, proof } = result; const { value, proof } = result;
await this._db.saveBalance({ blockHash, token, owner, value: BigInt(value), proof: JSONbig.stringify(proof) }); await this._db.saveBalance({ blockHash, token, owner, value: BigInt(value), proof: JSONbig.stringify(proof) });
@ -118,7 +150,21 @@ export class Indexer {
} }
log('allowance: db miss, fetching from upstream server'); log('allowance: db miss, fetching from upstream server');
const result = await this._getStorageValue(blockHash, token, '_allowances', owner, spender); let result: ValueResult;
if (this._serverMode === ETH_CALL_MODE) {
const contract = new ethers.Contract(token, this._abi, this._ethProvider);
const { block } = await this._ethClient.getBlockByHash(blockHash);
const { number } = block;
const blockNumber = BigNumber.from(number).toNumber();
const value = await contract.allowance(owner, spender, { blockTag: blockNumber });
result = {
value: BigInt(value.toString())
};
} else {
result = await this._getStorageValue(blockHash, token, '_allowances', owner, spender);
}
// log(JSONbig.stringify(result, null, 2)); // log(JSONbig.stringify(result, null, 2));
@ -129,7 +175,15 @@ export class Indexer {
} }
async name (blockHash: string, token: string): Promise<ValueResult> { async name (blockHash: string, token: string): Promise<ValueResult> {
const result = await this._getStorageValue(blockHash, token, '_name'); let result: ValueResult;
if (this._serverMode === ETH_CALL_MODE) {
const value = await fetchTokenName(this._ethProvider, token);
result = { value };
} else {
result = await this._getStorageValue(blockHash, token, '_name');
}
// log(JSONbig.stringify(result, null, 2)); // log(JSONbig.stringify(result, null, 2));
@ -137,20 +191,37 @@ export class Indexer {
} }
async symbol (blockHash: string, token: string): Promise<ValueResult> { async symbol (blockHash: string, token: string): Promise<ValueResult> {
const result = await this._getStorageValue(blockHash, token, '_symbol'); let result: ValueResult;
if (this._serverMode === ETH_CALL_MODE) {
const value = await fetchTokenSymbol(this._ethProvider, token);
result = { value };
} else {
result = await this._getStorageValue(blockHash, token, '_symbol');
}
// log(JSONbig.stringify(result, null, 2)); // log(JSONbig.stringify(result, null, 2));
return result; return result;
} }
async decimals (): Promise<void> { async decimals (blockHash: string, token: string): Promise<ValueResult> {
let result: ValueResult;
if (this._serverMode === ETH_CALL_MODE) {
const value = await fetchTokenDecimals(this._ethProvider, token);
result = { value };
} else {
// Not a state variable, uses hardcoded return value in contract function. // Not a state variable, uses hardcoded return value in contract function.
// See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L86 // See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L86
throw new Error('Not implemented.'); throw new Error('Not implemented.');
} }
return result;
}
async getEvents (blockHash: string, token: string, name: string | null): Promise<EventsResult> { async getEvents (blockHash: string, token: string, name: string | null): Promise<EventsResult> {
const didSyncEvents = await this._db.didSyncEvents({ blockHash, token }); const didSyncEvents = await this._db.didSyncEvents({ blockHash, token });
if (!didSyncEvents) { if (!didSyncEvents) {
@ -206,7 +277,7 @@ export class Indexer {
return result; return result;
} }
async triggerIndexingOnEvent (blockHash: string, token: string, receipt: any, logIndex: number): Promise<void> { async triggerIndexingOnEvent (blockHash: string, blockNumber: number, token: string, receipt: any, logIndex: number): Promise<void> {
const topics = []; const topics = [];
// We only care about the event type for now. // We only care about the event type for now.
@ -257,9 +328,9 @@ export class Indexer {
}); });
} }
async processEvent (blockHash: string, token: string, receipt: any, logIndex: number): Promise<void> { async processEvent (blockHash: string, blockNumber: number, token: string, receipt: any, logIndex: number): Promise<void> {
// Trigger indexing of data based on the event. // Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(blockHash, token, receipt, logIndex); await this.triggerIndexingOnEvent(blockHash, blockNumber, token, receipt, logIndex);
// Also trigger downstream event watcher subscriptions. // Also trigger downstream event watcher subscriptions.
await this.publishEventToSubscribers(blockHash, token, logIndex); await this.publishEventToSubscribers(blockHash, token, logIndex);

View File

@ -62,7 +62,7 @@ export const createResolvers = async (indexer: Indexer): Promise<any> => {
decimals: (_: any, { blockHash, token }: { blockHash: string, token: string }) => { decimals: (_: any, { blockHash, token }: { blockHash: string, token: string }) => {
log('decimals', blockHash, token); log('decimals', blockHash, token);
return indexer.decimals(); return indexer.decimals(blockHash, token);
}, },
events: async (_: any, { blockHash, token, name }: { blockHash: string, token: string, name: string }) => { events: async (_: any, { blockHash, token, name }: { blockHash: string, token: string, name: string }) => {

View File

@ -7,6 +7,7 @@ import { hideBin } from 'yargs/helpers';
import debug from 'debug'; import debug from 'debug';
import 'graphql-import-node'; import 'graphql-import-node';
import { createServer } from 'http'; import { createServer } from 'http';
import { getDefaultProvider } from 'ethers';
import { getCache } from '@vulcanize/cache'; import { getCache } from '@vulcanize/cache';
import { EthClient } from '@vulcanize/ipld-eth-client'; import { EthClient } from '@vulcanize/ipld-eth-client';
@ -37,7 +38,7 @@ export const main = async (): Promise<any> => {
assert(config.server, 'Missing server config'); assert(config.server, 'Missing server config');
const { host, port } = config.server; const { host, port, mode } = config.server;
const { upstream, database: dbConfig } = config; const { upstream, database: dbConfig } = config;
@ -47,7 +48,7 @@ export const main = async (): Promise<any> => {
await db.init(); await db.init();
assert(upstream, 'Missing upstream config'); assert(upstream, 'Missing upstream config');
const { ethServer: { gqlApiEndpoint, gqlPostgraphileEndpoint }, cache: cacheConfig } = upstream; const { ethServer: { gqlApiEndpoint, gqlPostgraphileEndpoint, rpcProviderEndpoint }, cache: cacheConfig } = upstream;
assert(gqlApiEndpoint, 'Missing upstream ethServer.gqlApiEndpoint'); assert(gqlApiEndpoint, 'Missing upstream ethServer.gqlApiEndpoint');
assert(gqlPostgraphileEndpoint, 'Missing upstream ethServer.gqlPostgraphileEndpoint'); assert(gqlPostgraphileEndpoint, 'Missing upstream ethServer.gqlPostgraphileEndpoint');
@ -58,10 +59,12 @@ export const main = async (): Promise<any> => {
cache cache
}); });
const ethProvider = getDefaultProvider(rpcProviderEndpoint);
// 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, pubsub, artifacts); const indexer = new Indexer(db, ethClient, ethProvider, pubsub, artifacts, mode);
const eventWatcher = new EventWatcher(ethClient, indexer); const eventWatcher = new EventWatcher(ethClient, indexer);
await eventWatcher.start(); await eventWatcher.start();

View File

@ -0,0 +1,110 @@
import { Contract, utils } from 'ethers';
import { BaseProvider } from '@ethersproject/providers';
import { abi } from '../artifacts/ERC20.json';
import ERC20SymbolBytesABI from '../artifacts/ERC20SymbolBytes.json';
import ERC20NameBytesABI from '../artifacts/ERC20NameBytes.json';
import { StaticTokenDefinition } from './static-token-definition';
export const fetchTokenSymbol = async (ethProvider: BaseProvider, tokenAddress: string): Promise<string> => {
const contract = new Contract(tokenAddress, abi, ethProvider);
const contractSymbolBytes = new Contract(tokenAddress, ERC20SymbolBytesABI, ethProvider);
let symbolValue = 'unknown';
// Try types string and bytes32 for symbol.
try {
const result = await contract.symbol();
symbolValue = result;
} catch (error) {
try {
const result = await contractSymbolBytes.symbol();
// For broken pairs that have no symbol function exposed.
if (!isNullEthValue(result)) {
symbolValue = utils.parseBytes32String(result);
} else {
// Try with the static definition.
const staticTokenDefinition = StaticTokenDefinition.fromAddress(tokenAddress);
if (staticTokenDefinition !== null) {
symbolValue = staticTokenDefinition.symbol;
}
}
} catch (error) {
// symbolValue is unknown if the calls revert.
}
}
return symbolValue;
};
export const fetchTokenName = async (ethProvider: BaseProvider, tokenAddress: string): Promise<string> => {
const contract = new Contract(tokenAddress, abi, ethProvider);
const contractNameBytes = new Contract(tokenAddress, ERC20NameBytesABI, ethProvider);
let nameValue = 'unknown';
// Try types string and bytes32 for name.
try {
const result = await contract.name();
nameValue = result;
} catch (error) {
try {
const result = await contractNameBytes.name();
// For broken pairs that have no name function exposed.
if (!isNullEthValue(result)) {
nameValue = utils.parseBytes32String(result);
} else {
// Try with the static definition.
const staticTokenDefinition = StaticTokenDefinition.fromAddress(tokenAddress);
if (staticTokenDefinition !== null) {
nameValue = staticTokenDefinition.name;
}
}
} catch (error) {
// nameValue is unknown if the calls revert.
}
}
return nameValue;
};
export const fetchTokenTotalSupply = async (ethProvider: BaseProvider, tokenAddress: string): Promise<bigint> => {
const contract = new Contract(tokenAddress, abi, ethProvider);
let totalSupplyValue = null;
try {
const result = await contract.totalSupply();
totalSupplyValue = result.toString();
} catch (error) {
totalSupplyValue = 0;
}
return BigInt(totalSupplyValue);
};
export const fetchTokenDecimals = async (ethProvider: BaseProvider, tokenAddress: string): Promise<bigint> => {
const contract = new Contract(tokenAddress, abi, ethProvider);
// Try types uint8 for decimals.
let decimalValue = null;
try {
const result = await contract.decimals();
decimalValue = result.toString();
} catch (error) {
// Try with the static definition.
const staticTokenDefinition = StaticTokenDefinition.fromAddress(tokenAddress);
if (staticTokenDefinition != null) {
return staticTokenDefinition.decimals;
}
}
return BigInt(decimalValue);
};
const isNullEthValue = (value: string): boolean => {
return value === '0x0000000000000000000000000000000000000000000000000000000000000001';
};

View File

@ -0,0 +1,94 @@
import { utils } from 'ethers';
// Initialize a Token Definition with the attributes.
export class StaticTokenDefinition {
address : string
symbol: string
name: string
decimals: bigint
// Initialize a Token Definition with its attributes.
constructor (address: string, symbol: string, name: string, decimals: bigint) {
this.address = address;
this.symbol = symbol;
this.name = name;
this.decimals = decimals;
}
// Get all tokens with a static defintion
static getStaticDefinitions (): Array<StaticTokenDefinition> {
const staticDefinitions = new Array<StaticTokenDefinition>(6);
// Add DGD.
const tokenDGD = new StaticTokenDefinition(
utils.getAddress('0xe0b7927c4af23765cb51314a0e0521a9645f0e2a'),
'DGD',
'DGD',
BigInt(9)
);
staticDefinitions.push(tokenDGD);
// Add AAVE.
const tokenAAVE = new StaticTokenDefinition(
utils.getAddress('0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9'),
'AAVE',
'Aave Token',
BigInt(18)
);
staticDefinitions.push(tokenAAVE);
// Add LIF.
const tokenLIF = new StaticTokenDefinition(
utils.getAddress('0xeb9951021698b42e4399f9cbb6267aa35f82d59d'),
'LIF',
'Lif',
BigInt(18)
);
staticDefinitions.push(tokenLIF);
// Add SVD.
const tokenSVD = new StaticTokenDefinition(
utils.getAddress('0xbdeb4b83251fb146687fa19d1c660f99411eefe3'),
'SVD',
'savedroid',
BigInt(18)
);
staticDefinitions.push(tokenSVD);
// Add TheDAO.
const tokenTheDAO = new StaticTokenDefinition(
utils.getAddress('0xbb9bc244d798123fde783fcc1c72d3bb8c189413'),
'TheDAO',
'TheDAO',
BigInt(16)
);
staticDefinitions.push(tokenTheDAO);
// Add HPB.
const tokenHPB = new StaticTokenDefinition(
utils.getAddress('0x38c6a68304cdefb9bec48bbfaaba5c5b47818bb2'),
'HPB',
'HPBCoin',
BigInt(18)
);
staticDefinitions.push(tokenHPB);
return staticDefinitions;
}
// Helper for hardcoded tokens.
static fromAddress (tokenAddress: string) : StaticTokenDefinition | null {
const staticDefinitions = this.getStaticDefinitions();
// Search the definition using the address.
for (let i = 0; i < staticDefinitions.length; i++) {
const staticDefinition = staticDefinitions[i];
if (utils.getAddress(staticDefinition.address) === utils.getAddress(tokenAddress)) {
return staticDefinition;
}
}
// If not found, return null.
return null;
}
}

View File

@ -56,15 +56,14 @@ query allEthHeaderCids($blockNumber: BigInt) {
`; `;
export const getBlockByHash = gql` export const getBlockByHash = gql`
query allEthHeaderCids($blockHash: Bytes32) { query block($blockHash: Bytes32) {
allEthHeaderCids(condition: { blockHash: $blockHash }) { block(hash: $blockHash) {
nodes { number
cid hash
blockNumber parent {
blockHash hash
parentHash
timestamp
} }
timestamp
} }
} }
`; `;

View File

@ -648,7 +648,7 @@ export class Database {
async _getPrevEntityVersion<Entity> (repo: Repository<Entity>, findOptions: { [key: string]: any }): Promise<Entity | undefined> { async _getPrevEntityVersion<Entity> (repo: Repository<Entity>, findOptions: { [key: string]: any }): Promise<Entity | undefined> {
assert(findOptions.order.blockNumber); assert(findOptions.order.blockNumber);
const { canonicalBlockNumber, blockHashes } = await this._getBranchInfo(findOptions.where.blockHash); const { canonicalBlockNumber, blockHashes } = await this._getBranchInfo(findOptions.where.blockHash);
findOptions.where.blockHash = In(blockHashes) findOptions.where.blockHash = In(blockHashes);
let entity = await repo.findOne(findOptions); let entity = await repo.findOne(findOptions);
if (!entity) { if (!entity) {

View File

@ -47,7 +47,7 @@ export class EventWatcher {
await this.initEventProcessingOnCompleteHandler(); await this.initEventProcessingOnCompleteHandler();
} }
async watchBlocksAtChainHead () { async watchBlocksAtChainHead (): Promise<void> {
log('Started watching upstream blocks...'); log('Started watching upstream blocks...');
this._subscription = await this._ethClient.watchBlocks(async (value) => { this._subscription = await this._ethClient.watchBlocks(async (value) => {
const { blockHash, blockNumber, parentHash } = _.get(value, 'data.listen.relatedNode'); const { blockHash, blockNumber, parentHash } = _.get(value, 'data.listen.relatedNode');
@ -59,7 +59,7 @@ export class EventWatcher {
}); });
} }
async initBlockProcessingOnCompleteHandler () { async initBlockProcessingOnCompleteHandler (): Promise<void> {
this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => { this._jobQueue.onComplete(QUEUE_BLOCK_PROCESSING, async (job) => {
const { data: { request: { data: { blockHash, blockNumber } } } } = job; const { data: { request: { data: { blockHash, blockNumber } } } } = job;
log(`Job onComplete block ${blockHash} ${blockNumber}`); log(`Job onComplete block ${blockHash} ${blockNumber}`);
@ -70,7 +70,7 @@ export class EventWatcher {
}); });
} }
async initEventProcessingOnCompleteHandler () { async initEventProcessingOnCompleteHandler (): Promise<void> {
this._jobQueue.onComplete(QUEUE_EVENT_PROCESSING, async (job) => { this._jobQueue.onComplete(QUEUE_EVENT_PROCESSING, async (job) => {
const { data: { request, failed, state, createdOn } } = job; const { data: { request, failed, state, createdOn } } = job;

View File

@ -12,6 +12,7 @@ export interface Config {
server: { server: {
host: string; host: string;
port: number; port: number;
mode: string;
}; };
database: ConnectionOptions; database: ConnectionOptions;
upstream: { upstream: {
@ -19,6 +20,7 @@ export interface Config {
ethServer: { ethServer: {
gqlApiEndpoint: string; gqlApiEndpoint: string;
gqlPostgraphileEndpoint: string; gqlPostgraphileEndpoint: string;
rpcProviderEndpoint: string
} }
traceProviderEndpoint: string; traceProviderEndpoint: string;
uniWatcher: { uniWatcher: {