watcher-ts/packages/uni-watcher/src/indexer.ts
nikugogoi cfd293f11b
Fix job-runner stop and restart in uni-watcher (#319)
* Fix job-runner stop and restart in uni-watcher

* Fix blocks processed twice after processing missing parent block
2021-12-17 11:57:09 +05:30

537 lines
16 KiB
TypeScript

//
// Copyright 2021 Vulcanize, Inc.
//
import debug from 'debug';
import { DeepPartial, FindConditions, FindManyOptions, QueryRunner } from 'typeorm';
import JSONbig from 'json-bigint';
import { ethers } from 'ethers';
import assert from 'assert';
import { EthClient } from '@vulcanize/ipld-eth-client';
import { IndexerInterface, Indexer as BaseIndexer, ValueResult, JobQueue, Where, QueryOptions } from '@vulcanize/util';
import { Database } from './database';
import { Event, UNKNOWN_EVENT_NAME } from './entity/Event';
import { BlockProgress } from './entity/BlockProgress';
import { Contract, KIND_FACTORY, KIND_POOL, KIND_NFPM } from './entity/Contract';
import { SyncStatus } from './entity/SyncStatus';
import { abi as factoryABI, storageLayout as factoryStorageLayout } from './artifacts/factory.json';
import { abi as nfpmABI, storageLayout as nfpmStorageLayout } from './artifacts/NonfungiblePositionManager.json';
import poolABI from './artifacts/pool.json';
const log = debug('vulcanize:indexer');
type ResultEvent = {
block: any;
tx: any;
contract: string;
eventIndex: number;
event: any;
proof: string;
};
export class Indexer implements IndexerInterface {
_db: Database
_ethClient: EthClient
_postgraphileClient: EthClient
_baseIndexer: BaseIndexer
_ethProvider: ethers.providers.BaseProvider
_factoryContract: ethers.utils.Interface
_poolContract: ethers.utils.Interface
_nfpmContract: ethers.utils.Interface
constructor (db: Database, ethClient: EthClient, postgraphileClient: EthClient, ethProvider: ethers.providers.BaseProvider, jobQueue: JobQueue) {
this._db = db;
this._ethClient = ethClient;
this._postgraphileClient = postgraphileClient;
this._ethProvider = ethProvider;
this._baseIndexer = new BaseIndexer(this._db, this._ethClient, this._postgraphileClient, this._ethProvider, jobQueue);
this._factoryContract = new ethers.utils.Interface(factoryABI);
this._poolContract = new ethers.utils.Interface(poolABI);
this._nfpmContract = new ethers.utils.Interface(nfpmABI);
}
async init (): Promise<void> {
await this._baseIndexer.fetchContracts();
}
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)
};
}
async triggerIndexingOnEvent (dbTx: QueryRunner, dbEvent: Event): Promise<void> {
const re = this.getResultEvent(dbEvent);
switch (re.event.__typename) {
case 'PoolCreatedEvent': {
const poolContract = ethers.utils.getAddress(re.event.pool);
await this.watchContract(poolContract, KIND_POOL, dbEvent.block.blockNumber);
}
}
}
async processEvent (event: Event): Promise<void> {
const dbTx = await this._db.createTransactionRunner();
try {
// Trigger indexing of data based on the event.
await this.triggerIndexingOnEvent(dbTx, event);
await dbTx.commitTransaction();
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
} finally {
await dbTx.release();
}
}
parseEventNameAndArgs (kind: string, logObj: any): any {
let eventName = UNKNOWN_EVENT_NAME;
let eventInfo = {};
const { topics, data } = logObj;
switch (kind) {
case KIND_FACTORY: {
const logDescription = this._factoryContract.parseLog({ data, topics });
switch (logDescription.name) {
case 'PoolCreated': {
eventName = logDescription.name;
const { token0, token1, fee, tickSpacing, pool } = logDescription.args;
eventInfo = { token0, token1, fee, tickSpacing, pool };
break;
}
}
break;
}
case KIND_POOL: {
const logDescription = this._poolContract.parseLog({ data, topics });
switch (logDescription.name) {
case 'Initialize': {
eventName = logDescription.name;
const { sqrtPriceX96, tick } = logDescription.args;
eventInfo = { sqrtPriceX96: sqrtPriceX96.toString(), tick };
break;
}
case 'Mint': {
eventName = logDescription.name;
const { sender, owner, tickLower, tickUpper, amount, amount0, amount1 } = logDescription.args;
eventInfo = {
sender,
owner,
tickLower,
tickUpper,
amount: amount.toString(),
amount0: amount0.toString(),
amount1: amount1.toString()
};
break;
}
case 'Burn': {
eventName = logDescription.name;
const { owner, tickLower, tickUpper, amount, amount0, amount1 } = logDescription.args;
eventInfo = {
owner,
tickLower,
tickUpper,
amount: amount.toString(),
amount0: amount0.toString(),
amount1: amount1.toString()
};
break;
}
case 'Swap': {
eventName = logDescription.name;
const { sender, recipient, amount0, amount1, sqrtPriceX96, liquidity, tick } = logDescription.args;
eventInfo = {
sender,
recipient,
amount0: amount0.toString(),
amount1: amount1.toString(),
sqrtPriceX96: sqrtPriceX96.toString(),
liquidity: liquidity.toString(),
tick
};
break;
}
}
break;
}
case KIND_NFPM: {
const logDescription = this._nfpmContract.parseLog({ data, topics });
switch (logDescription.name) {
case 'IncreaseLiquidity': {
eventName = logDescription.name;
const { tokenId, liquidity, amount0, amount1 } = logDescription.args;
eventInfo = {
tokenId: tokenId.toString(),
liquidity: liquidity.toString(),
amount0: amount0.toString(),
amount1: amount1.toString()
};
break;
}
case 'DecreaseLiquidity': {
eventName = logDescription.name;
const { tokenId, liquidity, amount0, amount1 } = logDescription.args;
eventInfo = {
tokenId: tokenId.toString(),
liquidity: liquidity.toString(),
amount0: amount0.toString(),
amount1: amount1.toString()
};
break;
}
case 'Collect': {
eventName = logDescription.name;
const { tokenId, recipient, amount0, amount1 } = logDescription.args;
eventInfo = {
tokenId: tokenId.toString(),
recipient,
amount0: amount0.toString(),
amount1: amount1.toString()
};
break;
}
case 'Transfer': {
eventName = logDescription.name;
const { from, to, tokenId } = logDescription.args;
eventInfo = {
from,
to,
tokenId: tokenId.toString()
};
break;
}
}
break;
}
}
return { eventName, eventInfo };
}
async position (blockHash: string, tokenId: string): Promise<any> {
const nfpmContract = await this._db.getLatestContract('nfpm');
assert(nfpmContract, 'No NFPM contract watched.');
const { value, proof } = await this._baseIndexer.getStorageValue(nfpmStorageLayout, blockHash, nfpmContract.address, '_positions', BigInt(tokenId));
return {
...value,
proof
};
}
async poolIdToPoolKey (blockHash: string, poolId: string): Promise<any> {
const nfpmContract = await this._db.getLatestContract('nfpm');
assert(nfpmContract, 'No NFPM contract watched.');
const { value, proof } = await this._baseIndexer.getStorageValue(nfpmStorageLayout, blockHash, nfpmContract.address, '_poolIdToPoolKey', BigInt(poolId));
return {
...value,
proof
};
}
async getPool (blockHash: string, token0: string, token1: string, fee: string): Promise<any> {
const factoryContract = await this._db.getLatestContract('factory');
assert(factoryContract, 'No Factory contract watched.');
const { value, proof } = await this._baseIndexer.getStorageValue(factoryStorageLayout, blockHash, factoryContract.address, 'getPool', token0, token1, BigInt(fee));
return {
pool: value,
proof
};
}
async callGetPool (blockHash: string, contractAddress: string, key0: string, key1: string, key2: number): Promise<ValueResult> {
const contract = new ethers.Contract(contractAddress, factoryABI, this._ethProvider);
try {
const value = await contract.getPool(key0, key1, key2, { blockTag: blockHash });
return { value };
} catch (error: any) {
if (error.code === ethers.utils.Logger.errors.CALL_EXCEPTION) {
log('eth_call error');
log(error);
throw new Error(error.code);
}
throw error;
}
}
async positions (blockHash: string, contractAddress: string, tokenId: string): Promise<ValueResult> {
const contract = new ethers.Contract(contractAddress, nfpmABI, this._ethProvider);
try {
const value = await contract.positions(tokenId, { blockTag: blockHash });
return { value };
} catch (error: any) {
if (error.code === ethers.utils.Logger.errors.CALL_EXCEPTION) {
log('eth_call error');
log(error);
throw new Error(error.code);
}
throw error;
}
}
async getContract (type: string): Promise<any> {
const contract = await this._db.getLatestContract(type);
return contract;
}
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 watchContract (address: string, kind: string, startingBlock: number): Promise<void> {
return this._baseIndexer.watchContract(address, kind, startingBlock);
}
cacheContract (contract: Contract): void {
return this._baseIndexer.cacheContract(contract);
}
async saveEventEntity (dbEvent: Event): Promise<Event> {
return this._baseIndexer.saveEventEntity(dbEvent);
}
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);
}
// Note: Some event names might be unknown at this point, as earlier events might not yet be processed.
async fetchBlockEvents (block: DeepPartial<BlockProgress>): Promise<BlockProgress> {
return this._baseIndexer.fetchBlockEvents(block, this._fetchAndSaveEvents.bind(this));
}
async getBlockEvents (blockHash: string, where: Where, queryOptions: QueryOptions): Promise<Array<Event>> {
return this._baseIndexer.getBlockEvents(blockHash, where, queryOptions);
}
async removeUnknownEvents (block: BlockProgress): Promise<void> {
return this._baseIndexer.removeUnknownEvents(Event, block);
}
async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusIndexedBlock(blockHash, blockNumber, force);
}
async updateSyncStatusChainHead (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusChainHead(blockHash, blockNumber, force);
}
async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number, force = false): Promise<SyncStatus> {
return this._baseIndexer.updateSyncStatusCanonicalBlock(blockHash, blockNumber, force);
}
async getSyncStatus (): Promise<SyncStatus | undefined> {
return this._baseIndexer.getSyncStatus();
}
async getBlocks (blockFilter: { blockHash?: string, blockNumber?: number }): Promise<any> {
return this._baseIndexer.getBlocks(blockFilter);
}
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 getBlockProgressEntities (where: FindConditions<BlockProgress>, options: FindManyOptions<BlockProgress>): Promise<BlockProgress[]> {
return this._baseIndexer.getBlockProgressEntities(where, options);
}
async getBlocksAtHeight (height: number, isPruned: boolean): Promise<BlockProgress[]> {
return this._baseIndexer.getBlocksAtHeight(height, isPruned);
}
async markBlocksAsPruned (blocks: BlockProgress[]): Promise<void> {
return this._baseIndexer.markBlocksAsPruned(blocks);
}
async updateBlockProgress (block: BlockProgress, lastProcessedEventIndex: number): Promise<BlockProgress> {
return this._baseIndexer.updateBlockProgress(block, lastProcessedEventIndex);
}
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
return this._baseIndexer.getAncestorAtDepth(blockHash, depth);
}
async _fetchAndSaveEvents ({ blockHash }: DeepPartial<BlockProgress>): Promise<BlockProgress> {
assert(blockHash);
const logsPromise = this._ethClient.getLogs({ blockHash });
const transactionsPromise = this._postgraphileClient.getBlockWithTransactions({ blockHash });
let [
{ block, logs },
{
allEthHeaderCids: {
nodes: [
{
ethTransactionCidsByHeaderId: {
nodes: transactions
}
}
]
}
}
] = await Promise.all([logsPromise, transactionsPromise]);
const transactionMap = transactions.reduce((acc: {[key: string]: any}, transaction: {[key: string]: any}) => {
acc[transaction.txHash] = transaction;
return acc;
}, {});
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 tx = transactionMap[txHash];
const extraInfo = { topics, data, tx };
const contract = ethers.utils.getAddress(address);
const uniContract = await this.isWatchedContract(contract);
if (uniContract) {
const eventDetails = this.parseEventNameAndArgs(uniContract.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
};
const blockProgress = await this._db.saveEvents(dbTx, block, dbEvents);
await dbTx.commitTransaction();
return blockProgress;
} catch (error) {
await dbTx.rollbackTransaction();
throw error;
} finally {
await dbTx.release();
}
}
}