import { isEmpty } from 'lodash' import { BigNumber, Contract } from 'ethers' import { IndexedDB } from './services/idb' import { BatchEventsService } from './services/batch' import { getAllNullifiers } from './services/graph' import { ExtendedProvider } from './services/provider' import { POOL_CONTRACT, RPC_LIST, FALLBACK_RPC_LIST, workerEvents, numbers } from './services/constants' import { sleep } from './services/utilities' import { poolAbi } from './services/pool' import { downloadEvents } from './services/downloadEvents' const getProviderWithSigner = (chainId) => { return new ExtendedProvider(RPC_LIST[chainId], chainId, FALLBACK_RPC_LIST[chainId]) } const initDataBase = async () => { try { const options = { stores: [ { keyPath: 'nullifier', name: `nullifier_events_100`, indexes: [{ name: 'transactionHash', unique: false }], }, ], dbName: 'tornado_pool_nullifier', } const instance = new IndexedDB(options) await instance.initDB() self.$indexedDB = instance } catch (err) { console.log('err', err.message) } } const initWorker = (chainId) => { self.chainId = chainId const provider = getProviderWithSigner(chainId) setTornadoPool(chainId, provider) initDataBase() } const setTornadoPool = (chainId, provider) => { self.poolContract = new Contract(POOL_CONTRACT[chainId], poolAbi, provider) self.BatchEventsService = new BatchEventsService({ provider, contract: self.poolContract }) } const saveEvents = async ({ events }) => { try { const isIdbEnable = await getIsDBEnabled() if (isIdbEnable || !getIsWhitelistedDomain() || isEmpty(events)) { return } self.$indexedDB.createMultipleTransactions({ data: events, storeName: 'nullifier_events_100', }) } catch (err) { console.error(`saveEvents has error: ${err.message}`) } } const checkUnspent = async (decryptedEvent, nullifierEvents) => { try { const { nullifierHash } = decryptedEvent const isIdbEnable = await getIsDBEnabled() if (isIdbEnable) { const lastEvent = await self.$indexedDB.getFromIndex({ key: nullifierHash, indexName: 'nullifier', storeName: 'nullifier_events_100', }) if (lastEvent) { return undefined } } const event = nullifierEvents.find((event) => { return event.nullifier === nullifierHash }) if (event) { return undefined } return decryptedEvent } catch (err) { throw new Error(`Method getNullifierEvent has error: ${err.message}`) } } const getCachedEvents = async () => { let blockFrom = numbers.DEPLOYED_BLOCK if (!self.$indexedDB) { await sleep(numbers.RECALL_DELAY) } const cachedEvents = await self.$indexedDB.getAll({ storeName: 'nullifier_events_100', }) if (cachedEvents && cachedEvents.length) { const [latestEvent] = cachedEvents.sort((a, b) => b.blockNumber - a.blockNumber) const currentBlock = await self.poolContract.provider.getBlockNumber() const newBlockFrom = Number(latestEvent.blockNumber) + numbers.ONE if (latestEvent.blockNumber === currentBlock) { return { blockFrom, cachedEvents } } blockFrom = newBlockFrom > currentBlock ? currentBlock : newBlockFrom } else { const downloadedEvents = await downloadEvents(`nullifiers_${self.chainId}.json`, blockFrom) if (downloadedEvents.events.length) { cachedEvents.push(...downloadedEvents.events) blockFrom = downloadedEvents.lastBlock } } return { blockFrom, cachedEvents } } const getNullifiers = async (blockFrom) => { try { const events = [] let { events: graphEvents, lastSyncBlock } = await getAllNullifiers({ fromBlock: blockFrom, chainId }) if (lastSyncBlock) { console.log({ graphEvents }) events.push(...graphEvents) blockFrom = lastSyncBlock } let nodeEvents = await self.BatchEventsService.getBatchEvents({ fromBlock: blockFrom, type: 'NewNullifier' }) if (nodeEvents && nodeEvents.length) { nodeEvents = nodeEvents.map(({ blockNumber, transactionHash, args }) => ({ blockNumber, transactionHash, nullifier: args.nullifier, })) console.log({ nodeEvents }) events.push(...nodeEvents) } return events } catch (err) { console.error('getNullifiers', err.message) return [] } } const getNullifierEvents = async (cachedNullifiers, withCache = true) => { let cached = { blockFrom: numbers.DEPLOYED_BLOCK, cachedEvents: [] } try { if (cachedNullifiers && cachedNullifiers.length) { const [latestEvent] = cachedNullifiers.sort((a, b) => b.blockNumber - a.blockNumber) const currentBlock = await self.poolContract.provider.getBlockNumber() const newBlockFrom = Number(latestEvent.blockNumber) + numbers.ONE if (latestEvent.blockNumber === currentBlock) { cached.blockFrom = numbers.DEPLOYED_BLOCK } cached.blockFrom = newBlockFrom > currentBlock ? currentBlock : newBlockFrom cached.cachedEvents = cachedNullifiers } else { cached = await getCachedEvents() } const { blockFrom = numbers.DEPLOYED_BLOCK, cachedEvents = [] } = cached const nullifiers = await getNullifiers(blockFrom) if (nullifiers.length) { saveEvents({ events: nullifiers }) } return withCache ? cachedEvents.concat(nullifiers) : nullifiers } catch (err) { throw new Error(`Method getNullifierEvents has error: ${err.message}`) } } const checkUnspentEvents = async ({ cachedNullifiers, decryptedEvents }) => { try { if (decryptedEvents.length === numbers.ZERO) { return { unspentUtxo: [], totalAmount: 0, } } const nullifierEvents = await getNullifierEvents(cachedNullifiers) let totalAmount = BigNumber.from('0') const unspentUtxo = [] const unspentEvents = await Promise.all(decryptedEvents.map((event) => checkUnspent(event, nullifierEvents))) unspentEvents.forEach((event) => { if (event && !BigNumber.from(event.amount).isZero()) { unspentUtxo.push(event) totalAmount = totalAmount.add(event.amount) } }) return { totalAmount, unspentUtxo } } catch (err) { throw new Error(`Method checkUnspentEvents has error: ${err.message}`) } } const getNullifierEventsFromTxHash = async ({ cachedNullifiers, txHash }, [getUnspentPort]) => { try { const lastEvents = await self.$indexedDB.getAllFromIndex({ key: txHash.toLowerCase(), indexName: 'transactionHash', storeName: 'nullifier_events_100', }) if (lastEvents && lastEvents.length > numbers.ZERO) { getUnspentPort.postMessage({ result: lastEvents }) return } const nullifierEvents = await getNullifierEvents(cachedNullifiers, true) const findingEvents = nullifierEvents.filter((event) => { return event.transactionHash.toLowerCase() === txHash.toLowerCase() }) getUnspentPort.postMessage({ result: findingEvents }) } catch (err) { getUnspentPort.postMessage({ errorMessage: err.message }) } } const getNullifierEvent = async ({ cachedNullifiers, nullifierHash }, [getUnspentPort]) => { try { const lastEvent = await self.$indexedDB.getFromIndex({ key: nullifierHash, indexName: 'nullifier', storeName: 'nullifier_events_100', }) if (lastEvent) { getUnspentPort.postMessage({ result: lastEvent }) return } const events = await getNullifierEvents(cachedNullifiers) const [event] = events.filter((event) => { return event.nullifier === nullifierHash }) getUnspentPort.postMessage({ result: event }) } catch (err) { getUnspentPort.postMessage({ errorMessage: err.message }) } } const updateNullifierEvents = async (cachedNullifiers, [getUnspentPort]) => { try { const events = await getNullifierEvents(cachedNullifiers) getUnspentPort.postMessage({ result: events }) } catch (err) { getUnspentPort.postMessage({ errorMessage: err.message }) } } const getUnspentEvents = async ({ decryptedEvents, cachedNullifiers }, [getUnspentPort]) => { try { const unspentEvents = await checkUnspentEvents({ decryptedEvents, cachedNullifiers }) getUnspentPort.postMessage({ result: unspentEvents }) } catch (err) { getUnspentPort.postMessage({ errorMessage: err.message }) } } const listener = ({ data, ports }) => { self.postMessage(data) switch (data.eventName) { case workerEvents.INIT_WORKER: initWorker(data.payload) break case workerEvents.GET_NULLIFIER_EVENT: getNullifierEvent(data.payload, ports) break case workerEvents.UPDATE_NULLIFIER_EVENTS: updateNullifierEvents(data.payload, ports) break case workerEvents.GET_UNSPENT_EVENTS: getUnspentEvents(data.payload, ports) break case workerEvents.GET_NULLIFIER_EVENTS_FROM_TX_HASH: getNullifierEventsFromTxHash(data.payload, ports) break } } // helpers const getIsDBEnabled = async () => { if (!self.$indexedDB) { await sleep(numbers.RECALL_DELAY) } return !self.$indexedDB || self.$indexedDB.isBlocked } const getIsWhitelistedDomain = () => { const domainWhiteList = ['localhost:3000', 'nova.tornadocash.eth', 'nova.tornadocash.eth.link', 'nova.tornadocash.eth.limo'] if (self.location.host.includes('compassionate-payne-b9dc6b.netlify.app')) { return true } return domainWhiteList.includes(self.location.host) } self.addEventListener('message', listener, false)