2021-08-19 07:57:32 +00:00
|
|
|
//
|
|
|
|
// Copyright 2021 Vulcanize, Inc.
|
|
|
|
//
|
|
|
|
|
|
|
|
import assert from 'assert';
|
2021-09-21 11:13:55 +00:00
|
|
|
import { DeepPartial, FindConditions, Not } from 'typeorm';
|
2021-08-20 13:32:57 +00:00
|
|
|
import debug from 'debug';
|
2021-09-21 11:13:55 +00:00
|
|
|
import { ethers } from 'ethers';
|
2021-08-19 07:57:32 +00:00
|
|
|
|
2021-08-20 13:32:57 +00:00
|
|
|
import { EthClient } from '@vulcanize/ipld-eth-client';
|
2021-09-21 11:13:55 +00:00
|
|
|
import { GetStorageAt, getStorageValue, StorageLayout } from '@vulcanize/solidity-mapper';
|
2021-08-20 13:32:57 +00:00
|
|
|
|
2021-09-21 11:13:55 +00:00
|
|
|
import { BlockProgressInterface, DatabaseInterface, EventInterface, SyncStatusInterface, ContractInterface } from './types';
|
|
|
|
import { UNKNOWN_EVENT_NAME } from './constants';
|
2021-08-20 13:32:57 +00:00
|
|
|
|
2021-08-24 06:25:29 +00:00
|
|
|
const MAX_EVENTS_BLOCK_RANGE = 1000;
|
|
|
|
|
2021-08-20 13:32:57 +00:00
|
|
|
const log = debug('vulcanize:indexer');
|
2021-08-19 07:57:32 +00:00
|
|
|
|
2021-09-21 11:13:55 +00:00
|
|
|
export interface ValueResult {
|
|
|
|
value: any;
|
|
|
|
proof?: {
|
|
|
|
data: string;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-19 07:57:32 +00:00
|
|
|
export class Indexer {
|
|
|
|
_db: DatabaseInterface;
|
2021-08-20 13:32:57 +00:00
|
|
|
_ethClient: EthClient;
|
2021-09-21 11:13:55 +00:00
|
|
|
_getStorageAt: GetStorageAt
|
2021-08-19 07:57:32 +00:00
|
|
|
|
2021-08-20 13:32:57 +00:00
|
|
|
constructor (db: DatabaseInterface, ethClient: EthClient) {
|
2021-08-19 07:57:32 +00:00
|
|
|
this._db = db;
|
2021-08-20 13:32:57 +00:00
|
|
|
this._ethClient = ethClient;
|
2021-09-21 11:13:55 +00:00
|
|
|
this._getStorageAt = this._ethClient.getStorageAt.bind(this._ethClient);
|
2021-08-20 13:32:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async getSyncStatus (): Promise<SyncStatusInterface | undefined> {
|
|
|
|
const dbTx = await this._db.createTransactionRunner();
|
|
|
|
let res;
|
|
|
|
|
|
|
|
try {
|
|
|
|
res = await this._db.getSyncStatus(dbTx);
|
|
|
|
await dbTx.commitTransaction();
|
|
|
|
} catch (error) {
|
|
|
|
await dbTx.rollbackTransaction();
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
await dbTx.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
2021-08-19 07:57:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async updateSyncStatusIndexedBlock (blockHash: string, blockNumber: number): Promise<SyncStatusInterface> {
|
|
|
|
const dbTx = await this._db.createTransactionRunner();
|
|
|
|
let res;
|
|
|
|
|
|
|
|
try {
|
|
|
|
res = await this._db.updateSyncStatusIndexedBlock(dbTx, blockHash, blockNumber);
|
|
|
|
await dbTx.commitTransaction();
|
|
|
|
} catch (error) {
|
|
|
|
await dbTx.rollbackTransaction();
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
await dbTx.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateSyncStatusChainHead (blockHash: string, blockNumber: number): Promise<SyncStatusInterface> {
|
|
|
|
const dbTx = await this._db.createTransactionRunner();
|
|
|
|
let res;
|
|
|
|
|
|
|
|
try {
|
|
|
|
res = await this._db.updateSyncStatusChainHead(dbTx, blockHash, blockNumber);
|
|
|
|
await dbTx.commitTransaction();
|
|
|
|
} catch (error) {
|
|
|
|
await dbTx.rollbackTransaction();
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
await dbTx.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateSyncStatusCanonicalBlock (blockHash: string, blockNumber: number): Promise<SyncStatusInterface> {
|
|
|
|
const dbTx = await this._db.createTransactionRunner();
|
|
|
|
let res;
|
|
|
|
|
|
|
|
try {
|
|
|
|
res = await this._db.updateSyncStatusCanonicalBlock(dbTx, blockHash, blockNumber);
|
|
|
|
await dbTx.commitTransaction();
|
|
|
|
} catch (error) {
|
|
|
|
await dbTx.rollbackTransaction();
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
await dbTx.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
2021-08-20 13:32:57 +00:00
|
|
|
async getBlock (blockHash: string): Promise<any> {
|
|
|
|
const { block } = await this._ethClient.getLogs({ blockHash });
|
|
|
|
return block;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getBlockProgress (blockHash: string): Promise<BlockProgressInterface | undefined> {
|
|
|
|
return this._db.getBlockProgress(blockHash);
|
|
|
|
}
|
|
|
|
|
2021-08-19 07:57:32 +00:00
|
|
|
async getBlocksAtHeight (height: number, isPruned: boolean): Promise<BlockProgressInterface[]> {
|
|
|
|
return this._db.getBlocksAtHeight(height, isPruned);
|
|
|
|
}
|
|
|
|
|
2021-08-23 11:36:35 +00:00
|
|
|
async markBlocksAsPruned (blocks: BlockProgressInterface[]): Promise<void> {
|
2021-08-19 07:57:32 +00:00
|
|
|
const dbTx = await this._db.createTransactionRunner();
|
|
|
|
|
|
|
|
try {
|
2021-08-23 11:36:35 +00:00
|
|
|
await this._db.markBlocksAsPruned(dbTx, blocks);
|
2021-08-19 07:57:32 +00:00
|
|
|
await dbTx.commitTransaction();
|
|
|
|
} catch (error) {
|
|
|
|
await dbTx.rollbackTransaction();
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
await dbTx.release();
|
|
|
|
}
|
|
|
|
}
|
2021-08-20 13:32:57 +00:00
|
|
|
|
|
|
|
async updateBlockProgress (blockHash: string, lastProcessedEventIndex: number): Promise<void> {
|
|
|
|
const dbTx = await this._db.createTransactionRunner();
|
|
|
|
let res;
|
|
|
|
|
|
|
|
try {
|
|
|
|
res = await this._db.updateBlockProgress(dbTx, blockHash, lastProcessedEventIndex);
|
|
|
|
await dbTx.commitTransaction();
|
|
|
|
} catch (error) {
|
|
|
|
await dbTx.rollbackTransaction();
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
await dbTx.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getEvent (id: string): Promise<EventInterface | undefined> {
|
|
|
|
return this._db.getEvent(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getOrFetchBlockEvents (block: DeepPartial<BlockProgressInterface>, fetchAndSaveEvents: (block: DeepPartial<BlockProgressInterface>) => Promise<void>): Promise<Array<EventInterface>> {
|
|
|
|
assert(block.blockHash);
|
|
|
|
const blockProgress = await this._db.getBlockProgress(block.blockHash);
|
|
|
|
if (!blockProgress) {
|
|
|
|
// Fetch and save events first and make a note in the event sync progress table.
|
|
|
|
log(`getBlockEvents: db miss, fetching from upstream server ${block.blockHash}`);
|
|
|
|
await fetchAndSaveEvents(block);
|
|
|
|
}
|
|
|
|
|
|
|
|
const events = await this._db.getBlockEvents(block.blockHash);
|
|
|
|
log(`getBlockEvents: db hit, ${block.blockHash} num events: ${events.length}`);
|
|
|
|
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getBlockEvents (blockHash: string): Promise<Array<EventInterface>> {
|
|
|
|
return this._db.getBlockEvents(blockHash);
|
|
|
|
}
|
2021-08-23 11:36:35 +00:00
|
|
|
|
2021-09-21 11:13:55 +00:00
|
|
|
async getEventsByFilter (blockHash: string, contract: string, name: string | null): Promise<Array<EventInterface>> {
|
|
|
|
if (contract) {
|
|
|
|
const watchedContract = await this.isWatchedContract(contract);
|
|
|
|
if (!watchedContract) {
|
|
|
|
throw new Error('Not a watched contract');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const where: FindConditions<EventInterface> = {
|
|
|
|
eventName: Not(UNKNOWN_EVENT_NAME)
|
|
|
|
};
|
|
|
|
|
|
|
|
if (contract) {
|
|
|
|
where.contract = contract;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (name) {
|
|
|
|
where.eventName = name;
|
|
|
|
}
|
|
|
|
|
|
|
|
const events = await this._db.getBlockEvents(blockHash, where);
|
|
|
|
log(`getEvents: db hit, num events: ${events.length}`);
|
|
|
|
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
2021-08-23 11:36:35 +00:00
|
|
|
async getAncestorAtDepth (blockHash: string, depth: number): Promise<string> {
|
|
|
|
return this._db.getAncestorAtDepth(blockHash, depth);
|
|
|
|
}
|
2021-08-24 06:25:29 +00:00
|
|
|
|
|
|
|
async saveEventEntity (dbEvent: EventInterface): Promise<EventInterface> {
|
|
|
|
const dbTx = await this._db.createTransactionRunner();
|
|
|
|
let res;
|
|
|
|
|
|
|
|
try {
|
|
|
|
res = this._db.saveEventEntity(dbTx, dbEvent);
|
|
|
|
await dbTx.commitTransaction();
|
|
|
|
} catch (error) {
|
|
|
|
await dbTx.rollbackTransaction();
|
|
|
|
throw error;
|
|
|
|
} finally {
|
|
|
|
await dbTx.release();
|
|
|
|
}
|
|
|
|
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
async getProcessedBlockCountForRange (fromBlockNumber: number, toBlockNumber: number): Promise<{ expected: number, actual: number }> {
|
|
|
|
return this._db.getProcessedBlockCountForRange(fromBlockNumber, toBlockNumber);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getEventsInRange (fromBlockNumber: number, toBlockNumber: number): Promise<Array<EventInterface>> {
|
|
|
|
if (toBlockNumber <= fromBlockNumber) {
|
|
|
|
throw new Error('toBlockNumber should be greater than fromBlockNumber');
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((toBlockNumber - fromBlockNumber) > MAX_EVENTS_BLOCK_RANGE) {
|
|
|
|
throw new Error(`Max range (${MAX_EVENTS_BLOCK_RANGE}) exceeded`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._db.getEventsInRange(fromBlockNumber, toBlockNumber);
|
|
|
|
}
|
2021-09-21 11:13:55 +00:00
|
|
|
|
|
|
|
async isWatchedContract (address : string): Promise<ContractInterface | undefined> {
|
|
|
|
assert(this._db.getContract);
|
|
|
|
|
|
|
|
return this._db.getContract(ethers.utils.getAddress(address));
|
|
|
|
}
|
|
|
|
|
|
|
|
async getStorageValue (storageLayout: StorageLayout, blockHash: string, token: string, variable: string, ...mappingKeys: any[]): Promise<ValueResult> {
|
|
|
|
return getStorageValue(
|
|
|
|
storageLayout,
|
|
|
|
this._getStorageAt,
|
|
|
|
blockHash,
|
|
|
|
token,
|
|
|
|
variable,
|
|
|
|
...mappingKeys
|
|
|
|
);
|
|
|
|
}
|
2021-08-19 07:57:32 +00:00
|
|
|
}
|