UNPKG

0xweb

Version:

Contract package manager and other web3 tools

183 lines (155 loc) 6.18 kB
import alot from 'alot' import { TAddress } from '@dequanto/models/TAddress' import { IEventsIndexerStore } from './interfaces' import { TAbiInput, TAbiItem } from '@dequanto/types/TAbi' import { ContractBase } from '@dequanto/contracts/ContractBase' import { ITxLogItem } from '@dequanto/txs/receipt/ITxLogItem' import { $date } from '@dequanto/utils/$date' import { JsonArrayMultiStore } from '@dequanto/json/JsonArrayMultiStore' import { FsEventsStoreUtils } from './FsEventsStoreUtils' import { $require } from '@dequanto/utils/$require' import { JsonArrayStore } from '@dequanto/json/JsonArrayStore' import { File } from 'atma-io' export class FsEventsIndexerStore <T extends ContractBase> implements IEventsIndexerStore { private store: JsonArrayMultiStore<ITxLogItem<any>> /** @deprecated For migration only */ private storeV0: JsonArrayStore<ITxLogItem<any>> private range: number; private abi: Record<string, TAbiItem>; constructor(public contract: T, public options: { // Load events from the contract that was deployed to multiple addresses addresses?: TAddress[] name?: string initialBlockNumber?: number fs?: { /** Base directory, in case the path is calculated from chain, name and addresses */ directory?: string /** Relative final directory path to be used as is for store files */ path?: string // the events will be splitted into multiple files by block range // default ~1week rangeSeconds?: number // default is taken from Web3Client blockTimeAvg?: number // Use a single file. Deprecated, used for migration only singleFile?: boolean } }) { let client = contract.client; let blockTimeAvg = options?.fs?.blockTimeAvg ?? client.options.blockTimeAvg; let rangeSeconds = options?.fs?.rangeSeconds ?? $date.parseTimespan('1week', { get: 's' }); this.range = Math.floor(rangeSeconds / blockTimeAvg); this.abi = alot(this.contract.abi).filter(x => x.type === 'event').toDictionary(x => x.name, x => x); let path = FsEventsStoreUtils.getDirectory(contract, { name: options.name, addresses: options.addresses, path: options.fs?.path, directory: options.fs?.directory, }); this.store = new JsonArrayMultiStore<ITxLogItem<any>>({ path: path, key: x => x.id, map: x => this.map(x), serialize: x => this.serialize(x), groupKey: x => x.blockNumber, groupSize: this.range }); this.storeV0 = new JsonArrayStore<ITxLogItem<any>>({ path: path.replace(/\/$/, '.json'), key: x => x.id, map: x => this.map(x), serialize: x => this.serialize(x), }); } async upsertMany(logs: ITxLogItem<any, string>[]): Promise<ITxLogItem<any, string>[]> { return await this.store.upsertMany(logs); } async removeMany(logs: ITxLogItem<any, string>[]): Promise<any> { return await this.store.removeMany(logs); } async fetch(filter?: { fromBlock?: number toBlock?: number }): Promise<ITxLogItem<any, string>[]> { return await this.store.fetch({ groupKey: { from: filter?.fromBlock, to: filter?.toBlock } }); } async merge (store: IEventsIndexerStore) { let arr = await store.fetch(); await this.upsertMany(arr); } /** @deprecated For migration only */ async ensureMigrated () { let arr = await this.storeV0.getAll(); if (arr.length > 0) { await this.upsertMany(arr); await File.removeAsync(this.storeV0.options.path); } } private map (x): ITxLogItem<any> { let abi = this.abi[x.event]; $require.notNull(abi, `Abi for ${x.event} not found.`) let blockNumber = Math.floor(x.id / 100000); let logIndex = x.id % 100000; x.params = EventAbiInputs.deserialize(x.params, abi); return { ...x, blockNumber, logIndex, arguments: abi.inputs.map(input => { return x.params[input.name]; }) }; } private serialize (x: ITxLogItem<any>) { return { ...x, // remove redundant data arguments: void 0, blockNumber: void 0, logIndex: void 0, }; } } namespace EventAbiInputs { // primary to convert BigInt from JSON export function deserialize (params: Record<string, any>, abi: TAbiItem): Record<string, any> { let inputs = alot(abi.inputs).toDictionary(x => x.name, x => x); for (let key in params) { let abiInput = inputs[key]; if (abiInput) { params[key] = deserializeValue(params[key], abiInput); } } return params; } function deserializeValue (value: any, abiInput: TAbiInput): any { if (value == null || abiInput == null) { return value; } if (typeof value === 'string' && abiInput.type.startsWith('uint')) { return BigInt(value); } let arrayRgx = /\[\d*\]$/; let isArrayType = arrayRgx.test(abiInput.type); if (isArrayType && Array.isArray(value)) { let type = abiInput.type.replace(arrayRgx, ''); let abiItem = { ...abiInput, type }; return value.map(x => deserializeValue(x, abiItem)); } if (abiInput.type === 'tuple' && abiInput.components != null && typeof value === 'object') { let result = {}; let abiComponents = alot(abiInput.components).toDictionary(x => x.name, x => x); for (let key in value) { let abiItem = abiComponents[key]; result[key] = deserializeValue(value[key], abiItem); } return result; } return value; } }