// ts-essentials import { DeepRequired, Merge } from 'ts-essentials' // Ethers import { Provider } from '@ethersproject/providers' import { BigNumber, EventFilter } from 'ethers' // Contract types import { RelayerRegistry } from './deth/RelayerRegistry.js' // Local imports import { AsyncUtils, ErrorUtils } from '@tornado/sdk-utils' import { Docs, Cache, Onchain, Options as DataOptions } from '@tornado/sdk-data' import { Chain, Synchronizer, Contracts, Options as ChainOptions, syncErrorHandler } from '@tornado/sdk-chain' // @ts-ignore import { toIndexableString } from 'pouchdb-collate' // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ DECLARATIONS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ export namespace Options { export type Cache = DataOptions.Cache export type Sync = ChainOptions.Sync } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ IMPLEMENTATIONS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ export class Registration extends Docs.Base { ensName: string address: string relayer: string stakedAmount: string constructor(obj: any) { const blockNumber = obj['blockNumber'] const ensName = obj['args']['ensName'] super(toIndexableString([blockNumber, ensName])) this.ensName = ensName this.relayer = obj['args']['relayer'] this.address = obj['args']['address'] this.stakedAmount = (obj['args']['stakedAmount'] as BigNumber).toString() } } export class Stake extends Docs.Base { relayer: string amountStakeAdded: string constructor(obj: any) { const blockNumber = obj['blockNumber'] super(toIndexableString([blockNumber])) this.relayer = obj['args']['relayer'] this.amountStakeAdded = (obj['args']['amountStakeAdded'] as BigNumber).toString() } } export class Burn extends Docs.Base { relayer: string amountBurned: string constructor(obj: any) { const blockNumber = obj['blockNumber'] super(toIndexableString([blockNumber])) this.relayer = obj['args']['relayer'] this.amountBurned = (obj['args']['amountBurned'] as BigNumber).toString() } } export class RegistrationsCache extends Cache.Syncable { buildDoc(response: any): Registration { return new Registration(response) } getErrorHandlers(): Array { return [syncErrorHandler] } getCallbacks(registry: RelayerRegistry): Array { return [ (fromBlock: number, toBlock: number) => { return registry.queryFilter( registry.filters['RelayerRegistered(bytes32,string,address,uint256)'](null, null, null, null), fromBlock, toBlock ) } ] } } export class StakesCache extends Cache.Syncable { buildDoc(response: any): Stake { return new Stake(response) } getErrorHandlers(): Array { return [syncErrorHandler] } getCallbacks(registry: RelayerRegistry): Array { return [ (fromBlock: number, toBlock: number) => { return registry.queryFilter( registry.filters['StakeAddedToRelayer(address,uint256)'](null, null), fromBlock, toBlock ) } ] } } export class BurnsCache extends Cache.Syncable { buildDoc(response: any): Burn { return new Burn(response) } getErrorHandlers(): Array { return [syncErrorHandler] } getCallbacks(registry: RelayerRegistry): Array { return [ (fromBlock: number, toBlock: number) => { return registry.queryFilter( registry.filters['StakeBurned(address,uint256)'](null, null), fromBlock, toBlock ) } ] } } export class Registry extends Synchronizer { private _chain?: Chain get chain(): Chain { this._checkProvider('chain') return this._chain! } constructor() { super() this.caches = new Map>() } private _checkProvider(parentCallName: string): void { if (!this._chain) throw ErrorUtils.getError('Core.' + parentCallName + ': you must first connect a provider!') } async connect(provider: Provider): Promise { if (!this._chain) this._chain = new Chain(provider) else this._chain.provider = provider await this._chain.fetchChainData() } getContract(): RelayerRegistry { return Contracts.getRegistry(this.chain.provider) } async syncRegistrations( networkId?: string | number, options?: Merge ): Promise { await this._sync('RelayerRegistered', networkId, options) } async syncStakes(networkId?: string | number, options?: Merge): Promise { await this._sync('StakeAddedToRelayer', networkId, options) } async syncBurns(networkId?: string | number, options?: Merge): Promise { await this._sync('StakeBurned', networkId, options) } protected async _sync( event: string | EventFilter, networkId?: string | number, options?: Merge ): Promise { networkId = networkId ?? '1' if (this.chain.id !== +networkId) throw ErrorUtils.getError('Registry._sync: provider bound to wrong chain!') let registry = this.getContract(), needsFilter = typeof event !== 'string', filter: EventFilter = event as EventFilter, emitterName, cache options = options ?? {} options.startBlock = options.startBlock ?? (await Onchain.getRegistryDeployBlockNum('' + networkId)) if (event === 'RelayerRegistered') { cache = this.loadRegistrations(networkId, options) emitterName = 'registration' if (needsFilter) filter = registry.filters.RelayerRegistered(null, null, null, null) } else if (event === 'StakeAddedToRelayer') { cache = this.loadStakes(networkId, options) emitterName = 'stake' if (needsFilter) filter = registry.filters.StakeAddedToRelayer(null, null) } else if (event === 'StakeBurned') { cache = this.loadBurns(networkId, options) emitterName = 'burn' if (needsFilter) filter = registry.filters['StakeBurned(address,uint256)'](null, null) } else throw ErrorUtils.getError(`Registry._sync: such an event ${event} is not supported`) await this.sync(emitterName, filter, registry, cache, options) } loadRegistrations(networkId?: number | string, options?: Options.Cache): RegistrationsCache { networkId = networkId ?? '1' return this.loadCache('Registrations' + networkId, options) } loadStakes(networkId?: number | string, options?: Options.Cache): StakesCache { networkId = networkId ?? '1' return this.loadCache('Stakes' + networkId, options) } loadBurns(networkId?: number | string, options?: Options.Cache): BurnsCache { networkId = networkId ?? '1' return this.loadCache('Burns' + networkId, options) } loadCache = Cache.Syncable>( name: string, options?: Options.Cache ): C { let constructor, cache = super.loadCache(name) if (!cache) { const regexp = /([A-Za-z]+)([0-9]+)/ const matches = name.match(regexp) if (!matches) throw ErrorUtils.getError(`Registry.loadCache: name ${name} has wrong format for cache`) if (matches.length === 2) name += '1' if (matches[1] === 'Registrations') { constructor = (name: string, options?: Options.Cache) => new RegistrationsCache(name, options) } else if (matches[1] === 'Burns') { constructor = (name: string, options?: Options.Cache) => new BurnsCache(name, options) } else if (matches[1] === 'Stakes') { constructor = (name: string, options?: Options.Cache) => new StakesCache(name, options) } else throw ErrorUtils.getError(`Registry.loadCache: there exists no such event ${matches[1]}`) cache = constructor(name, options) this.caches.set(name, cache) } return cache as C } async exportRegistrationsZip( networkId?: number | string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { networkId = networkId ?? '1' await this._exportCacheZip('Registrations' + networkId, outDirPath, close, options) } async exportStakesZip( networkId?: number | string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { networkId = networkId ?? '1' await this._exportCacheZip('Stakes' + networkId, outDirPath, close, options) } async exportBurnsZip( networkId?: number | string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { networkId = networkId ?? '1' await this._exportCacheZip('Burns' + networkId, outDirPath, close, options) } async exportRegistrationsJson( networkId?: number | string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { networkId = networkId ?? '1' await this._exportCacheJson('Registrations' + networkId, outDirPath, close, options) } async exportStakesJson( networkId?: number | string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { networkId = networkId ?? '1' await this._exportCacheJson('Stakes' + networkId, outDirPath, close, options) } async exportBurnsJson( networkId?: number | string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { networkId = networkId ?? '1' await this._exportCacheJson('Burns' + networkId, outDirPath, close, options) } protected async _exportCacheZip( cacheName: string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { const cache = new Cache.Base(cacheName, options) await cache.zip(outDirPath, close) if (close === true) this.caches.delete(cacheName) } protected async _exportCacheJson( cacheName: string, outDirPath?: string, close?: boolean, options?: Options.Cache ): Promise { const cache = new Cache.Base(cacheName, options) await cache.jsonify(outDirPath) if (close === true) this.caches.delete(cacheName) } protected async _populateSyncOptions(options: Options.Sync): Promise> { options.targetBlock = options.targetBlock ?? (await this.chain.latestBlockNum()) return super._populateSyncOptions(options) } }