nova-ui/services/utxoService/index.ts
2022-12-04 07:02:30 +01:00

327 lines
11 KiB
TypeScript

import { BigNumber } from 'ethers'
import { L2ChainId } from '@/types'
import {
CachedData,
CachedEvents,
GetUtxoPayload,
DecryptedHashes,
UnspentUtxoData,
BatchEventPayload,
BatchEventsPayload,
RestoreUtxoPayload,
FetchUnspentUtxoRes,
GetFreshUtxoPayload,
UnspentUtxoDataBatch,
GetBatchUtxoPayload,
GetCashedEventsPayload,
FetchBatchUtxoPayload,
} from './@types'
import { TornadoPool } from '@/_contracts'
import { getTornadoPool } from '@/contracts'
import { BG_ZERO } from '@/constants'
import { numbers, workerEvents } from '@/constants/worker'
import { getBlocksBatches, controlledPromise, uniqBy } from '@/utilities'
import { CustomUtxo } from '@/services/core/@types'
import { Keypair, Utxo, workerProvider } from '@/services'
import {
UnspentData,
DecryptedEvents,
GetUnspentEvents,
GetDecryptedEvents,
GetDecryptBatchData,
GetUnspentUtxoPayload,
GetFreshUnspentUtxoRes,
} from '@/services/worker/@types'
import { CommitmentEvents, NullifierEvents } from '@/services/events/@types'
export interface UtxoService {
restoreUnspentUtxo: ({ keypair, callback }: RestoreUtxoPayload) => Promise<CachedData>
fetchUnspentUtxo: ({ keypair, callbacks }: GetUtxoPayload) => Promise<FetchUnspentUtxoRes>
}
class Service implements UtxoService {
public poolContract: TornadoPool
public promises: {
[key in string]: null | {
promise: Promise<FetchUnspentUtxoRes>
resolve: (value: FetchUnspentUtxoRes | PromiseLike<FetchUnspentUtxoRes>) => void
reject: (value: Error) => void
}
}
private latestBlock: number
private decrypted: DecryptedEvents
private nullifiers: NullifierEvents
private readonly accountAddress: string
public static async getCachedData(currentBlock: number, keypair: Keypair): Promise<CachedEvents> {
try {
const cachedEvents = await workerProvider.openEventsChannel<GetCashedEventsPayload, GetDecryptedEvents>(
workerEvents.GET_CACHED_EVENTS,
{
publicKey: keypair.pubkey,
privateKey: keypair.privkey,
storeName: 'decrypted_events_100',
},
)
if (cachedEvents?.lastSyncBlock && cachedEvents?.decrypted?.length) {
const newBlockFrom = Number(cachedEvents.lastSyncBlock) + numbers.ONE
const latestBlock = newBlockFrom > currentBlock ? currentBlock : newBlockFrom
return { ...cachedEvents, latestBlock }
}
return { latestBlock: numbers.DEPLOYED_BLOCK, commitments: [], decrypted: [] }
} catch (err) {
throw new Error(`getCachedData error: ${err}`)
}
}
public static async getBatchEvents({ blockFrom, blockTo, cachedEvents, keypair, index }: BatchEventPayload) {
try {
const batchEvents = await workerProvider.openEventsChannel<BatchEventsPayload, GetDecryptedEvents>(
workerEvents.GET_BATCH_EVENTS,
{ blockFrom, blockTo, publicKey: keypair.pubkey, cachedEvents, privateKey: keypair.privkey },
index,
)
return batchEvents
} catch (err) {
throw new Error(`getFreshData error: ${err}`)
}
}
public constructor(
chainId: L2ChainId,
accountAddress: string,
decrypted = [],
nullifiers = [],
latestBlock = numbers.DEPLOYED_BLOCK,
) {
this.poolContract = getTornadoPool(chainId)
this.promises = {}
this.accountAddress = accountAddress
this.decrypted = decrypted
this.nullifiers = nullifiers
this.latestBlock = latestBlock
}
public async getUnspentUtxo({ decryptedEvents, keypair, index }: GetUnspentUtxoPayload): Promise<UnspentUtxoData> {
try {
const { unspentUtxo, totalAmount } = await workerProvider.openNullifierChannel<GetUnspentEvents, UnspentData>(
workerEvents.GET_UNSPENT_EVENTS,
{
decryptedEvents,
cachedNullifiers: this.nullifiers,
},
index,
)
return {
accountAddress: this.accountAddress,
totalAmount: BigNumber.from(totalAmount),
unspentUtxo: unspentUtxo.map((customUtxo: CustomUtxo) => {
const utxo = new Utxo({ ...customUtxo, keypair })
utxo.transactionHash = customUtxo.transactionHash
return utxo as CustomUtxo
}),
}
} catch (err) {
throw new Error(`getUnspentUtxo error: ${err}`)
}
}
public async getBatchEventsData({
batch,
decryptedEvents,
keypair,
index,
}: FetchBatchUtxoPayload): Promise<UnspentUtxoDataBatch> {
const [from, to] = batch
const { decrypted, commitments, decryptedHashes } = await Service.getBatchEvents({
index,
keypair,
blockTo: to,
blockFrom: from,
cachedEvents: decryptedEvents,
})
const { totalAmount, unspentUtxo } = await this.getUnspentUtxo({ decryptedEvents: decrypted, keypair, index })
return { totalAmount, unspentUtxo, decrypted, commitments, decryptedHashes }
}
public async getNullifierEventsFromTxHash(txHash: string): Promise<NullifierEvents> {
try {
return await workerProvider.getNullifierEventsFromTxHash(this.nullifiers, txHash)
} catch (err) {
throw new Error(`getNullifierEventsFromTxHash error: ${err}`)
}
}
public async getCachedEventsData(keypair: Keypair, currentBlock: number): Promise<CachedData> {
const { latestBlock, decrypted } = await Service.getCachedData(currentBlock, keypair)
const decryptedEvents = decrypted?.length ? decrypted : this.decrypted
const { totalAmount, unspentUtxo } = await this.getUnspentUtxo({
keypair,
decryptedEvents,
index: numbers.ZERO,
})
return {
totalAmount,
unspentUtxo,
decryptedEvents,
accountAddress: this.accountAddress,
latestBlock: decrypted?.length ? latestBlock : this.latestBlock,
}
}
public async restoreUnspentUtxo({ keypair, callback }: RestoreUtxoPayload): Promise<CachedData> {
const currentBlock = await this.poolContract.provider.getBlockNumber()
const cachedData = await this.getCachedEventsData(keypair, currentBlock)
callback({ totalAmount: cachedData.totalAmount, unspentUtxo: cachedData.unspentUtxo, accountAddress: this.accountAddress })
return cachedData
}
public async getFreshUnspentUtxo({ keypair, cachedData, callback }: GetFreshUtxoPayload): Promise<GetFreshUnspentUtxoRes> {
const currentBlock = await this.poolContract.provider.getBlockNumber()
const interval = currentBlock - cachedData.latestBlock
let batchesCount = workerProvider.eventsWorkers.length
if (interval <= numbers.MIN_BLOCKS_INTERVAL_LINE) {
batchesCount = numbers.TWO
}
const batches = getBlocksBatches(cachedData.latestBlock, currentBlock, batchesCount).reverse()
const promises = batches.map(
// eslint-disable-next-line
(batch, index) => this.fetchUnspentUtxoBatch({ batch, index, keypair, callback, decryptedEvents: cachedData.decryptedEvents }),
)
const freshBatchesData = await Promise.all(promises)
workerProvider.openEventsChannel<{ storeName: string; data: CommitmentEvents }, null>(workerEvents.SAVE_EVENTS, {
storeName: 'commitment_events_100',
data: freshBatchesData.map((el) => el.commitments).flat(),
})
workerProvider.openEventsChannel<{ storeName: string; data: DecryptedHashes }, null>(workerEvents.SAVE_EVENTS, {
storeName: 'decrypted_events_100',
data: freshBatchesData.map((el) => el.decryptedHashes).flat(),
})
return { freshBatchesData, lastBlock: currentBlock }
}
public async fetchUnspentUtxo({ keypair, callbacks }: GetUtxoPayload): Promise<FetchUnspentUtxoRes> {
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<FetchUnspentUtxoRes>(this.fetchData({ keypair, callbacks }))
this.promises[keypair.pubkey._hex] = controlled
return await controlled.promise
}
private async fetchData({ keypair, callbacks }: GetUtxoPayload): Promise<FetchUnspentUtxoRes> {
try {
this.nullifiers = await workerProvider.openNullifierChannel<NullifierEvents, NullifierEvents>(
workerEvents.UPDATE_NULLIFIER_EVENTS,
this.nullifiers,
)
const cachedData = await this.restoreUnspentUtxo({ keypair, callback: callbacks.set })
const { freshBatchesData, lastBlock } = await this.getFreshUnspentUtxo({ keypair, cachedData, callback: callbacks.update })
const accumulator = {
totalAmount: cachedData.totalAmount,
unspentUtxo: [],
decrypted: [],
commitments: [],
decryptedHashes: [],
}
const freshData = freshBatchesData.reduce(this.compileData, accumulator)
if (lastBlock) {
await workerProvider.openEventsChannel<{ lastSyncBlock: number }, null>(workerEvents.SAVE_LAST_SYNC_BLOCK, {
lastSyncBlock: lastBlock,
})
}
this.latestBlock = lastBlock
this.decrypted = uniqBy(cachedData.decryptedEvents.concat(freshData.decrypted), 'commitment._hex')
return {
totalAmount: freshData.totalAmount,
freshUnspentUtxo: freshData.unspentUtxo,
freshDecryptedEvents: freshData.decrypted,
accountAddress: this.accountAddress,
unspentUtxo: cachedData.unspentUtxo.concat(freshData.unspentUtxo),
}
} catch (err) {
throw new Error(`getBalance error: ${err}`)
} finally {
this.promises[keypair.pubkey._hex] = null
}
}
private async fetchUnspentUtxoBatch(payload: GetBatchUtxoPayload): Promise<GetDecryptBatchData> {
const { totalAmount, unspentUtxo, decrypted, commitments, decryptedHashes } = await this.getBatchEventsData(payload)
if (!this.promises[payload.keypair.pubkey._hex]) {
return { totalAmount: BG_ZERO, unspentUtxo: [], decrypted: [], commitments: [], decryptedHashes: [] }
}
if (!totalAmount.isZero()) {
payload.callback({ totalAmount, unspentUtxo, accountAddress: this.accountAddress })
}
return { totalAmount, unspentUtxo, decrypted, commitments, decryptedHashes }
}
private compileData(acc: UnspentUtxoDataBatch, { totalAmount, unspentUtxo, decrypted }: UnspentUtxoDataBatch) {
acc.totalAmount = acc.totalAmount.add(totalAmount)
acc.unspentUtxo = acc.unspentUtxo.concat(unspentUtxo)
acc.decrypted = acc.decrypted.concat(decrypted)
return acc
}
}
class UtxoFactory {
public instances = new Map()
public getService = (chainId: L2ChainId, accountAddress: string) => {
const key = `${chainId}_${accountAddress}`
if (this.instances.has(key)) {
return this.instances.get(key)
}
const instance = new Service(chainId, accountAddress)
this.instances.set(key, instance)
return instance
}
}
const utxoFactory = new UtxoFactory()
export { utxoFactory }