mirror of
https://github.com/cerc-io/watcher-ts
synced 2025-07-27 02:32:07 +00:00
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:
parent
fd1ab0780c
commit
c677e5942c
@ -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"
|
||||||
|
17
packages/erc20-watcher/src/artifacts/ERC20NameBytes.json
Normal file
17
packages/erc20-watcher/src/artifacts/ERC20NameBytes.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "name",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "",
|
||||||
|
"type": "bytes32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]
|
17
packages/erc20-watcher/src/artifacts/ERC20SymbolBytes.json
Normal file
17
packages/erc20-watcher/src/artifacts/ERC20SymbolBytes.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "symbol",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "",
|
||||||
|
"type": "bytes32"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,18 +191,35 @@ 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> {
|
||||||
// Not a state variable, uses hardcoded return value in contract function.
|
let result: ValueResult;
|
||||||
// See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L86
|
|
||||||
|
|
||||||
throw new Error('Not implemented.');
|
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.
|
||||||
|
// See https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L86
|
||||||
|
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> {
|
||||||
@ -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);
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -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();
|
||||||
|
110
packages/erc20-watcher/src/utils/index.ts
Normal file
110
packages/erc20-watcher/src/utils/index.ts
Normal 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';
|
||||||
|
};
|
94
packages/erc20-watcher/src/utils/static-token-definition.ts
Normal file
94
packages/erc20-watcher/src/utils/static-token-definition.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user