import { L2ChainId } from '@/types' import { FreshData, CachedData, BatchPayload, BatchEventPayload, BatchEventsPayload, GetFreshCommitments, GetCashedEventsPayload, } from './@types' import { TornadoPool } from '@/_contracts' import { getTornadoPool } from '@/contracts' import { numbers, workerEvents } from '@/constants/worker' import { getBlocksBatches, controlledPromise } from '@/utilities' import { Keypair, workerProvider } from '@/services' import { CommitmentEvents } from '@/services/events/@types' import { GetDecryptedEvents } from '@/services/worker/@types' export interface CommitmentsService { getCachedData: (keypair: Keypair) => Promise fetchCommitments: (keypair: Keypair) => Promise getFreshCommitments: (payload: GetFreshCommitments) => Promise } class Service implements CommitmentsService { public poolContract: TornadoPool private latestBlock: number private commitments: CommitmentEvents public promises: { [key in string]: null | { promise: Promise resolve: (value: CommitmentEvents | PromiseLike) => void reject: (value: Error) => void } } public static async getBatchEvents({ blockFrom, blockTo, cachedEvents, keypair, index }: BatchEventPayload) { try { const batchEvents = await workerProvider.openEventsChannel( workerEvents.GET_BATCH_EVENTS, { blockFrom, blockTo, cachedEvents, publicKey: keypair.pubkey, privateKey: keypair.privkey }, index, ) return batchEvents } catch (err) { throw new Error(`getFreshData error: ${err}`) } } public static async getBatchEventsData({ batch, cachedEvents, keypair, index }: BatchPayload): Promise { const [from, to] = batch const { commitments } = await Service.getBatchEvents({ index, keypair, blockTo: to, blockFrom: from, cachedEvents, }) return commitments } public constructor(chainId: L2ChainId) { this.poolContract = getTornadoPool(chainId) this.promises = {} this.commitments = [] this.latestBlock = numbers.DEPLOYED_BLOCK } public async getFreshCommitments({ keypair, latestBlock, commitments }: GetFreshCommitments): Promise { const currentBlock = await this.poolContract.provider.getBlockNumber() const interval = currentBlock - latestBlock let batchesCount = workerProvider.eventsWorkers.length if (interval <= numbers.MIN_BLOCKS_INTERVAL_LINE) { batchesCount = numbers.TWO } const batches = getBlocksBatches(latestBlock, currentBlock, batchesCount).reverse() const promises = batches.map( // eslint-disable-next-line (batch, index) => this.fetchCommitmentsBatch({ batch, index, keypair, cachedEvents: commitments }), ) const freshCommitments = await Promise.all(promises) return { freshCommitments: freshCommitments.flat(), lastBlock: currentBlock } } public async getCachedData(keypair: Keypair): Promise { try { const currentBlock = await this.poolContract.provider.getBlockNumber() const cachedEvents = await workerProvider.openEventsChannel( workerEvents.GET_CACHED_COMMITMENTS_EVENTS, { publicKey: keypair.pubkey, privateKey: keypair.privkey, storeName: 'commitment_events_100', }, ) if (cachedEvents?.lastSyncBlock && cachedEvents?.commitments?.length) { const newBlockFrom = Number(cachedEvents.lastSyncBlock) + numbers.ONE const latestBlock = newBlockFrom > currentBlock ? currentBlock : newBlockFrom return { ...cachedEvents, latestBlock } } return { latestBlock: this.latestBlock, commitments: this.commitments } } catch (err) { throw new Error(`getCachedData error: ${err}`) } } public async fetchCommitments(keypair: Keypair): Promise { const knownPromise = this.promises[keypair.pubkey._hex]?.promise if (knownPromise) { return await knownPromise } Object.keys(this.promises).forEach((promiseKey) => { const promise = this.promises[promiseKey] if (promise) { promise.reject(new Error('Account was changed')) // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.promises[promiseKey] } }) const controlled = controlledPromise(this.fetchData(keypair)) this.promises[keypair.pubkey._hex] = controlled return await controlled.promise } private async fetchData(keypair: Keypair): Promise { try { const { latestBlock, commitments } = await this.getCachedData(keypair) const { freshCommitments, lastBlock } = await this.getFreshCommitments({ keypair, latestBlock, commitments }) this.latestBlock = lastBlock this.commitments = commitments.concat(freshCommitments.flat()) return this.commitments } catch (err) { throw new Error(`getBalance error: ${err}`) } finally { this.promises[keypair.pubkey._hex] = null } } private async fetchCommitmentsBatch(payload: BatchPayload): Promise { const commitments = await Service.getBatchEventsData(payload) if (!this.promises[payload.keypair.pubkey._hex]) { return [] } return commitments } } class CommitmentsFactory { public instances = new Map() public getService = (chainId: L2ChainId) => { if (this.instances.has(chainId)) { return this.instances.get(chainId) } const instance = new Service(chainId) this.instances.set(chainId, instance) return instance } } const commitmentsFactory = new CommitmentsFactory() export { commitmentsFactory }