diff --git a/README.md b/README.md index 45436f7..a6613bc 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,29 @@ ## Build Setup +If you use the latest Node.js version, you should modify your NODE_OPTIONS env + +```bash +export NODE_OPTIONS="--openssl-legacy-provider" +``` + ```bash # install dependencies $ yarn install -# serve with hot reload at localhost:3000 -$ yarn dev - # build for production and launch server $ yarn build $ yarn start # generate static project $ yarn generate + +# serve with hot reload at localhost:3000 +# should do yarn build first if worker files are changed +$ yarn dev + +# update cached events from node & subgraphs +$ yarn update:events ``` For detailed explanation on how things work, check out [Nuxt.js docs](https://nuxtjs.org). diff --git a/assets/events.worker.js b/assets/events.worker.js index 8660cb0..6e150bb 100644 --- a/assets/events.worker.js +++ b/assets/events.worker.js @@ -1,17 +1,17 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ +import { AES, HmacSHA256, enc } from 'crypto-js' +import { isEmpty } from 'lodash' +import { BigNumber, Contract } from 'ethers' +import { poseidon } from '@tornado/circomlib' +import { decrypt } from 'eth-sig-util' -const { AES, HmacSHA256, enc } = require('crypto-js') -const { isEmpty } = require('lodash') -const { BigNumber } = require('ethers') -const { poseidon } = require('@tornado/circomlib') -const { decrypt } = require('eth-sig-util') -const { IndexedDB } = require('../services/idb') -const { sleep } = require('../utilities/helpers') -const { workerEvents, numbers } = require('../constants/worker') -const { ExtendedProvider } = require('../services/ether/ExtendedProvider') - -const { POOL_CONTRACT, RPC_LIST, FALLBACK_RPC_LIST } = require('../constants/contracts') -const { TornadoPool__factory: TornadoPoolFactory } = require('../_contracts') +import { IndexedDB } from './services/idb' +import { BatchEventsService } from './services/batch' +import { getAllCommitments } 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]) @@ -61,22 +61,75 @@ const initWorker = (chainId) => { setTornadoPool(chainId, provider) } const setTornadoPool = (chainId, provider) => { - self.poolContract = TornadoPoolFactory.connect(POOL_CONTRACT[chainId], provider) + self.poolContract = new Contract(POOL_CONTRACT[chainId], poolAbi, provider) + + self.BatchEventsService = new BatchEventsService({ + provider, + contract: self.poolContract + }) } const getCommitmentBatch = async ({ blockFrom, blockTo, cachedEvents, withCache }) => { - const filter = self.poolContract.filters.NewCommitment() - const events = await self.poolContract.queryFilter(filter, blockFrom, blockTo) + const events = [] - const commitmentEvents = events.map(({ blockNumber, transactionHash, args }) => ({ - blockNumber, - transactionHash, - index: Number(args.index), - commitment: args.commitment, - encryptedOutput: args.encryptedOutput, - })) + let { events: graphEvents, lastSyncBlock } = await getAllCommitments({ + fromBlock: blockFrom, + toBlock: blockTo, + chainId + }) - return commitmentEvents.filter((el) => { + if (lastSyncBlock) { + graphEvents = graphEvents + .filter(({ blockNumber }) => { + if (blockFrom && blockTo) { + return Number(blockFrom) <= Number(blockNumber) && Number(blockNumber) <= Number(blockTo) + } else if (blockTo) { + return Number(blockNumber) <= Number(blockTo) + } + // does not filter by default + return true + }) + .map(({ blockNumber, transactionHash, index, commitment, encryptedOutput }) => ({ + blockNumber, + transactionHash, + index: Number(index), + commitment, + encryptedOutput, + })) + + console.log({ + graphEvents + }) + + events.push(...graphEvents) + blockFrom = lastSyncBlock + } + + if (!blockTo || blockTo > blockFrom) { + let nodeEvents = await self.BatchEventsService.getBatchEvents({ + fromBlock: blockFrom, + toBlock: blockTo, + type: 'NewCommitment' + }) + + if (nodeEvents && nodeEvents.length) { + nodeEvents = nodeEvents.map(({ blockNumber, transactionHash, args }) => ({ + blockNumber, + transactionHash, + index: Number(args.index), + commitment: args.commitment, + encryptedOutput: args.encryptedOutput, + })) + + console.log({ + nodeEvents + }) + + events.push(...nodeEvents) + } + } + + return events.filter((el) => { if (!withCache && cachedEvents && cachedEvents.length) { return cachedEvents.find((cached) => { return el.transactionHash === cached.transactionHash && el.index === cached.index @@ -113,6 +166,14 @@ const getCommitments = async ({ withCache, lastSyncBlock }) => { return { commitmentEvents: cachedEvents } } blockFrom = newBlockFrom > currentBlock ? currentBlock : newBlockFrom + } else { + const downloadedEvents = await downloadEvents(`commitments_${self.chainId}.json`, blockFrom) + + if (downloadedEvents.events.length) { + cachedEvents.push(...downloadedEvents.events) + + blockFrom = downloadedEvents.lastBlock + } } const commitmentEvents = await getCommitmentBatch({ blockFrom, blockTo: currentBlock, cachedEvents, withCache }) diff --git a/assets/nullifier.worker.js b/assets/nullifier.worker.js index e3f6cf2..73b7f1d 100644 --- a/assets/nullifier.worker.js +++ b/assets/nullifier.worker.js @@ -1,14 +1,14 @@ -/* eslint-disable @typescript-eslint/no-require-imports */ -const { isEmpty } = require('lodash') -const { BigNumber } = require('ethers') +import { isEmpty } from 'lodash' +import { BigNumber, Contract } from 'ethers' -const { IndexedDB } = require('../services/idb') -const { sleep } = require('../utilities/helpers') -const { workerEvents, numbers } = require('../constants/worker') -const { ExtendedProvider } = require('../services/ether/ExtendedProvider') - -const { POOL_CONTRACT, RPC_LIST, FALLBACK_RPC_LIST } = require('../constants/contracts') -const { TornadoPool__factory: TornadoPoolFactory } = require('../_contracts') +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]) @@ -47,7 +47,12 @@ const initWorker = (chainId) => { } const setTornadoPool = (chainId, provider) => { - self.poolContract = TornadoPoolFactory.connect(POOL_CONTRACT[chainId], provider) + self.poolContract = new Contract(POOL_CONTRACT[chainId], poolAbi, provider) + + self.BatchEventsService = new BatchEventsService({ + provider, + contract: self.poolContract + }) } const saveEvents = async ({ events }) => { @@ -116,6 +121,14 @@ const getCachedEvents = async () => { 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 } @@ -123,14 +136,39 @@ const getCachedEvents = async () => { const getNullifiers = async (blockFrom) => { try { - const filter = self.poolContract.filters.NewNullifier() - const events = await self.poolContract.queryFilter(filter, blockFrom) + const events = [] + + let { events: graphEvents, lastSyncBlock } = await getAllNullifiers({ fromBlock: blockFrom, chainId }) + + if (lastSyncBlock) { + console.log({ + graphEvents + }) - return events.map(({ blockNumber, transactionHash, args }) => ({ - blockNumber, - transactionHash, - nullifier: args.nullifier, - })) + 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 [] diff --git a/assets/services/batch.js b/assets/services/batch.js new file mode 100644 index 0000000..e0030ab --- /dev/null +++ b/assets/services/batch.js @@ -0,0 +1,85 @@ +import { sleep, getBatches } from './utilities' + +export class BatchEventsService { + constructor({ + provider, + contract, + concurrencySize = 10, + blocksPerRequest = 2000, + shouldRetry = true, + retryMax = 5, + retryOn = 500, + }) { + this.provider = provider; + this.contract = contract; + this.concurrencySize = concurrencySize; + this.blocksPerRequest = blocksPerRequest; + this.shouldRetry = shouldRetry; + this.retryMax = retryMax; + this.retryOn = retryOn; + } + + async getPastEvents({ fromBlock, toBlock, type }) { + let err; + let retries = 0; + + // eslint-disable-next-line no-unmodified-loop-condition + while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) { + try { + return (await this.contract.queryFilter(type, fromBlock, toBlock)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e) { + err = e; + retries++; + + // If provider.getBlockNumber returned last block that isn't accepted (happened on Avalanche/Gnosis), + // get events to last accepted block + if (e.message.includes('after last accepted block')) { + const acceptedBlock = parseInt(e.message.split('after last accepted block ')[1]); + toBlock = acceptedBlock; + } + + // retry on 0.5 seconds + await sleep(this.retryOn); + } + } + + throw err; + } + + createBatchRequest(batchArray) { + return batchArray.map(async (event, index) => { + await sleep(20 * index); + + return this.getPastEvents(event); + }); + } + + async getBatchEvents({ fromBlock, toBlock, type = '*' }) { + if (!toBlock) { + toBlock = await this.provider.getBlockNumber(); + } + + const eventsToSync = []; + + for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) { + const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1; + + eventsToSync.push({ fromBlock: i, toBlock: j, type }); + } + + const events = []; + const eventChunk = getBatches(eventsToSync, this.concurrencySize); + + let chunkCount = 0; + + for (const chunk of eventChunk) { + chunkCount++; + + const fetchedEvents = (await Promise.all(this.createBatchRequest(chunk))).flat(); + events.push(...fetchedEvents); + } + + return events; + } +} \ No newline at end of file diff --git a/assets/services/bridgeHelper.js b/assets/services/bridgeHelper.js new file mode 100644 index 0000000..ee69d62 --- /dev/null +++ b/assets/services/bridgeHelper.js @@ -0,0 +1,237 @@ +export const bridgeAbi = [ + { + inputs: [ + { + internalType: "contract IOmnibridge", + name: "_bridge", + type: "address", + }, + { + internalType: "contract IWETH", + name: "_weth", + type: "address", + }, + { + internalType: "address", + name: "_owner", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "key", + type: "bytes", + }, + ], + name: "PublicKey", + type: "event", + }, + { + inputs: [], + name: "WETH", + outputs: [ + { + internalType: "contract IWETH", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "bridge", + outputs: [ + { + internalType: "contract IOmnibridge", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_token", + type: "address", + }, + { + internalType: "address", + name: "_to", + type: "address", + }, + ], + name: "claimTokens", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_token", + type: "address", + }, + { + internalType: "uint256", + name: "_value", + type: "uint256", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "onTokenBridged", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "bytes", + name: "publicKey", + type: "bytes", + }, + ], + internalType: "struct L1Helper.Account", + name: "_account", + type: "tuple", + }, + ], + name: "register", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_newOwner", + type: "address", + }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "wrapAndRelayTokens", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_receiver", + type: "address", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "wrapAndRelayTokens", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_receiver", + type: "address", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + components: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "bytes", + name: "publicKey", + type: "bytes", + }, + ], + internalType: "struct L1Helper.Account", + name: "_account", + type: "tuple", + }, + ], + name: "wrapAndRelayTokens", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_receiver", + type: "address", + }, + ], + name: "wrapAndRelayTokens", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] \ No newline at end of file diff --git a/assets/services/constants.js b/assets/services/constants.js new file mode 100644 index 0000000..3656b00 --- /dev/null +++ b/assets/services/constants.js @@ -0,0 +1,177 @@ +export const BSC_CHAIN_ID = 56 +export const XDAI_CHAIN_ID = 100 +export const MAINNET_CHAIN_ID = 1 + +export const ChainId = { + BSC: BSC_CHAIN_ID, + XDAI: XDAI_CHAIN_ID, + MAINNET: MAINNET_CHAIN_ID, +} + +export const OFFCHAIN_ORACLE_CONTRACT = '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb' + +export const POOL_CONTRACT = { + [ChainId.XDAI]: '0xD692Fd2D0b2Fbd2e52CFa5B5b9424bC981C30696', // ETH + // [ChainId.XDAI]: '0x772F007F13604ac286312C85b9Cd9B2D691B353E', // BNB +} +export const REDGISTRY_CONTRACT = { + [ChainId.MAINNET]: '0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2', +} + +export const AGGREGATOR_FACTORY = { + [ChainId.MAINNET]: '0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49', +} + +export const WRAPPED_TOKEN = { + [ChainId.MAINNET]: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', // WETH on mainnet + [ChainId.XDAI]: '0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1', // WETH on xdai + [ChainId.BSC]: '0xCa8d20f3e0144a72C6B5d576e9Bd3Fd8557E2B04', // WBNB on xdai +} + +export const RPC_LIST = { + [ChainId.BSC]: 'https://tornadocash-rpc.com/bsc', + [ChainId.MAINNET]: 'https://tornadocash-rpc.com/mainnet', + [ChainId.XDAI]: 'https://tornadocash-rpc.com/gnosis', +} + +export const FALLBACK_RPC_LIST = { + [ChainId.BSC]: [ + 'https://binance.nodereal.io', + // 'https://rpc.ankr.com/bsc/dbe08b852ba176a8aeac783cc1fa8becaf4f107235dfdae79241063fbf52ca4a', + ], + [ChainId.MAINNET]: [ + 'https://rpc.mevblocker.io', + // 'https://rpc.ankr.com/eth/dbe08b852ba176a8aeac783cc1fa8becaf4f107235dfdae79241063fbf52ca4a', + ], + [ChainId.XDAI]: [ + // 'https://rpc.ankr.com/gnosis/dbe08b852ba176a8aeac783cc1fa8becaf4f107235dfdae79241063fbf52ca4a', + 'https://tornadocash-rpc.com/gnosis', + ], +} + +export const RPC_WS_LIST = { + [ChainId.MAINNET]: 'wss://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607', + [ChainId.BSC]: 'wss://bsc-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607', + [ChainId.XDAI]: 'wss://gnosis-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607', +} + +export const MULTICALL = { + [ChainId.BSC]: '0xf072f255A3324198C7F653237B44E1C4e66f8C42', + [ChainId.XDAI]: '0x8677b93D543d0217B32B8FDc20F2316E138D619B', + [ChainId.MAINNET]: '0x1F98415757620B543A52E61c46B32eB19261F984', +} + +export const BRIDGE_PROXY = { + [ChainId.BSC]: '0x05185872898b6f94AA600177EF41B9334B1FA48B', + [ChainId.MAINNET]: '0x4c36d2919e407f0cc2ee3c993ccf8ac26d9ce64e', +} + +export const AMB_BRIDGE = { + [ChainId.XDAI]: '0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59', // ETH + // [ChainId.XDAI]: '0x162E898bD0aacB578C8D5F8d6ca588c13d2A383F', // BNB + [ChainId.MAINNET]: '0x162E898bD0aacB578C8D5F8d6ca588c13d2A383F', +} + +export const BRIDGE_HELPER = { + [ChainId.MAINNET]: '0xCa0840578f57fE71599D29375e16783424023357', + [ChainId.BSC]: '0x8845F740F8B01bC7D9A4C82a6fD4A60320c07AF1', +} + +export const BRIDGE_FEE_MANAGER = { + [ChainId.XDAI]: '0x5dbC897aEf6B18394D845A922BF107FA98E3AC55', +} + +export const FOREIGN_OMNIBRIDGE = { + [ChainId.MAINNET]: '0x88ad09518695c6c3712AC10a214bE5109a655671', +} + +export const OMNIBRIDGE = { + [ChainId.XDAI]: '0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d', +} + +export const SANCTION_LIST = { + [ChainId.MAINNET]: '0x40C57923924B5c5c5455c48D93317139ADDaC8fb', +} + +export const CHAINS = { + [ChainId.XDAI]: { + symbol: 'XDAI', + name: 'xdai', + shortName: 'xdai', + icon: 'ethereum', + network: 'XDAI', + blockDuration: 3000, // ms + deployBlock: 19097755, // ETH + // deployBlock: 20446605, // BNB + blockGasLimit: 144000000, // rpc block gas limit + hexChainId: '0x64', + isEipSupported: false, + ensSubdomainKey: 'gnosis-nova', + blockExplorerUrl: 'https://gnosisscan.io' + }, + [ChainId.MAINNET]: { + symbol: 'ETH', + name: 'ethereum', + shortName: 'eth', + icon: 'ethereum', + network: 'Mainnet', + deployBlock: 13494216, + blockDuration: 15000, + blockGasLimit: 144000000, + hexChainId: '0x1', + isEipSupported: true, + ensSubdomainKey: 'mainnet-tornado', + blockExplorerUrl: 'https://etherscan.io' + }, + [ChainId.BSC]: { + symbol: 'BNB', + name: 'bsc', + shortName: 'bsc', + icon: 'binance', + network: 'BSC', + deployBlock: 14931075, + blockDuration: 3000, + blockGasLimit: 144000000, + hexChainId: '0x38', + isEipSupported: false, + ensSubdomainKey: 'bsc-tornado', + blockExplorerUrl: 'https://bscscan.com' + }, +} + + +export const workerEvents = { + INIT_WORKER: 'initWorker', + GET_COMMITMENT_EVENTS: 'get_commitment_events', + // nullifier + GET_UNSPENT_EVENTS: 'get_unspent_events', + GET_NULLIFIER_EVENT: 'get_nullifier_event', + GET_NULLIFIER_EVENTS_FROM_TX_HASH: 'get_nullifier_events_from_tx_hash', + UPDATE_NULLIFIER_EVENTS: 'update_nullifier_events', + // events + GET_BATCH_EVENTS: 'get_batch_events', + GET_BATCH_COMMITMENTS_EVENTS: 'get_batch_commitments_events', + GET_EVENTS_FROM_TX_HASH: 'get_events_from_tx_hash', + SAVE_EVENTS: 'save_events', + GET_CACHED_EVENTS: 'get_cached_events', + GET_CACHED_COMMITMENTS_EVENTS: 'get_cached_commitments_events', + SAVE_LAST_SYNC_BLOCK: 'save_last_sync_block', +} + +export const numbers = { + ZERO: 0, + TWO: 2, + ONE: 1, + BYTES_31: 31, + BYTES_62: 62, + IS_SPENT_INDEX: 1, + OX_LENGTH: 2, + RECALL_DELAY: 500, + NULLIFIER_LENGTH: 66, + NONCE_BUF_LENGTH: 24, + COMMITMENTS_CHAIN: 100, + DEPLOYED_BLOCK: 19097755, + DECRYPT_WORKERS_COUNT: 8, + MIN_BLOCKS_INTERVAL_LINE: 200000, + EPHEM_PUBLIC_KEY_BUF_LENGTH: 56, +} \ No newline at end of file diff --git a/assets/services/downloadEvents.js b/assets/services/downloadEvents.js new file mode 100644 index 0000000..5f312a2 --- /dev/null +++ b/assets/services/downloadEvents.js @@ -0,0 +1,37 @@ +import { unzipAsync } from "./zip" + +export async function downloadEvents(fileName, deployedBlock) { + fileName = fileName.toLowerCase() + + // @ts-ignore + const prefix = __webpack_public_path__.slice(0, -7) + + try { + const resp = await fetch(`${prefix}/${fileName}.zip`, { + method: 'GET', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }) + + const arrayBuffer = await resp.arrayBuffer() + + const { [fileName]: content } = await unzipAsync(new Uint8Array(arrayBuffer)) + + const events = JSON.parse(new TextDecoder().decode(content)) + + const lastBlock = events && Array.isArray(events) && events[events.length - 1] + ? events[events.length - 1].blockNumber + : deployedBlock + + return { + events, + lastBlock + } + } catch { + return { + events: [], + lastBlock: deployedBlock + } + } +} \ No newline at end of file diff --git a/assets/services/graph/index.js b/assets/services/graph/index.js new file mode 100644 index 0000000..1b52991 --- /dev/null +++ b/assets/services/graph/index.js @@ -0,0 +1,279 @@ +import { isEmpty } from 'lodash' +import { ApolloClient, InMemoryCache, gql } from '@apollo/client/core' +import { utils } from 'ethers' + +import { GET_ACCOUNTS, GET_COMMITMENT, GET_NULLIFIER } from './queries' +import { ChainId, numbers } from '../constants' + +const { getAddress } = utils + +const first = 1000 +const breakLength = 900 + +const CHAIN_GRAPH_URLS = { + [ChainId.BSC]: 'https://api.thegraph.com/subgraphs/name/dan1kov/bsc-tornado-pool-subgraph', + [ChainId.MAINNET]: 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/mainnet-tornado-pool-subgraph', + [ChainId.XDAI]: 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/gnosis-tornado-nova-subgraph', +} + +const link = (operation) => { + const { chainId } = operation.getContext() + return CHAIN_GRAPH_URLS[chainId] +} + +const client = new ApolloClient({ + uri: link, + cache: new InMemoryCache(), +}) + +export async function getAccounts({ fromBlock, chainId }) { + const { data } = await client.query({ + context: { + chainId, + }, + query: gql(GET_ACCOUNTS), + variables: { first, fromBlock }, + }) + + if (!data) { + return { + results: [], + lastSyncBlock: data._meta.block.number + } + } + + return { + results: data.accounts, + lastSyncBlock: data._meta.block.number + } +} + +export async function getAllAccounts({ fromBlock, toBlock, chainId }) { + try { + let accounts = [] + let lastSyncBlock + + while (true) { + let { results, lastSyncBlock: lastBlock } = await getAccounts({ fromBlock, chainId }) + + lastSyncBlock = lastBlock + + if (isEmpty(results)) { + break + } + + if (results.length < breakLength) { + accounts = accounts.concat(results) + break + } + + const [lastEvent] = results.slice(-numbers.ONE) + + results = results.filter((e) => e.blockNumber !== lastEvent.blockNumber) + fromBlock = Number(lastEvent.blockNumber) + + accounts = accounts.concat(results) + + if (toBlock && fromBlock >= Number(toBlock)) { + break + } + } + + if (!accounts) { + return { + lastSyncBlock, + events: [], + } + } + + const data = accounts.map((e) => ({ + key: e.key, + owner: getAddress(e.owner), + blockNumber: Number(e.blockNumber), + })) + + const [lastEvent] = data.slice(-numbers.ONE) + + return { + events: data, + lastSyncBlock: (lastEvent && lastEvent.blockNumber >= lastSyncBlock) + ? lastEvent.blockNumber + numbers.ONE + : lastSyncBlock, + } + } catch (err) { + console.log('Error from getAllAccounts') + console.log(err) + return { + lastSyncBlock: '', + events: [], + } + } +} + +export async function getCommitments({ fromBlock, chainId }) { + const { data } = await client.query({ + context: { + chainId, + }, + query: gql(GET_COMMITMENT), + variables: { first, fromBlock }, + }) + + if (!data) { + return { + results: [], + lastSyncBlock: data._meta.block.number + } + } + + return { + results: data.commitments, + lastSyncBlock: data._meta.block.number + } +} + +export async function getAllCommitments({ fromBlock, toBlock, chainId }) { + try { + let commitments = [] + let lastSyncBlock + + while (true) { + let { results, lastSyncBlock: lastBlock } = await getCommitments({ fromBlock, chainId }) + + lastSyncBlock = lastBlock + + if (isEmpty(results)) { + break + } + + if (results.length < breakLength) { + commitments = commitments.concat(results) + break + } + + const [lastEvent] = results.slice(-numbers.ONE) + + results = results.filter((e) => e.blockNumber !== lastEvent.blockNumber) + fromBlock = Number(lastEvent.blockNumber) + + commitments = commitments.concat(results) + + if (toBlock && fromBlock >= Number(toBlock)) { + break + } + } + + if (!commitments) { + return { + lastSyncBlock, + events: [], + } + } + + const data = commitments + .map((e) => ({ + blockNumber: Number(e.blockNumber), + transactionHash: e.transactionHash, + index: Number(e.index), + commitment: e.commitment, + encryptedOutput: e.encryptedOutput + })) + .sort((a, b) => a.index - b.index) + + const [lastEvent] = data.slice(-numbers.ONE) + + return { + events: data, + lastSyncBlock: (lastEvent && lastEvent.blockNumber >= lastSyncBlock) + ? lastEvent.blockNumber + numbers.ONE + : lastSyncBlock, + } + } catch (err) { + console.log('Error from getAllCommitments') + console.log(err) + return { + lastSyncBlock: '', + events: [], + } + } +} + +export async function getNullifiers({ fromBlock, chainId }) { + const { data } = await client.query({ + context: { + chainId, + }, + query: gql(GET_NULLIFIER), + variables: { first, fromBlock }, + }) + + if (!data) { + return { + results: [], + lastSyncBlock: data._meta.block.number + } + } + + return { + results: data.nullifiers, + lastSyncBlock: data._meta.block.number + } +} + +export async function getAllNullifiers({ fromBlock, chainId }) { + try { + let nullifiers = [] + let lastSyncBlock + + while (true) { + let { results, lastSyncBlock: lastBlock } = await getNullifiers({ fromBlock, chainId }) + + lastSyncBlock = lastBlock + + if (isEmpty(results)) { + break + } + + if (results.length < breakLength) { + nullifiers = nullifiers.concat(results) + break + } + + const [lastEvent] = results.slice(-numbers.ONE) + + results = results.filter((e) => e.blockNumber !== lastEvent.blockNumber) + fromBlock = Number(lastEvent.blockNumber) + + nullifiers = nullifiers.concat(results) + } + + if (!nullifiers) { + return { + lastSyncBlock, + events: [], + } + } + + const data = nullifiers.map((e) => ({ + nullifier: e.nullifier, + blockNumber: Number(e.blockNumber), + transactionHash: e.transactionHash + })) + + const [lastEvent] = data.slice(-numbers.ONE) + + return { + events: data, + lastSyncBlock: (lastEvent && lastEvent.blockNumber >= lastSyncBlock) + ? lastEvent.blockNumber + numbers.ONE + : lastSyncBlock, + } + } catch (err) { + console.log('Error from getAllNullifiers') + console.log(err) + return { + lastSyncBlock: '', + events: [], + } + } +} \ No newline at end of file diff --git a/assets/services/graph/queries.js b/assets/services/graph/queries.js new file mode 100644 index 0000000..41ec38a --- /dev/null +++ b/assets/services/graph/queries.js @@ -0,0 +1,56 @@ +export const GET_ACCOUNTS = ` + query getAccounts($first: Int, $fromBlock: Int) { + accounts(first: $first, orderBy: blockNumber, orderDirection: asc, where: { + blockNumber_gte: $fromBlock + }) { + id + key + owner + blockNumber + } + _meta { + block { + number + } + hasIndexingErrors + } + } +` + +export const GET_COMMITMENT = ` + query getCommitment($first: Int, $fromBlock: Int) { + commitments(first: $first, orderBy: blockNumber, orderDirection: asc, where: { + blockNumber_gte: $fromBlock + }) { + index + commitment + blockNumber + encryptedOutput + transactionHash + } + _meta { + block { + number + } + hasIndexingErrors + } + } +` + +export const GET_NULLIFIER = ` + query getNullifier($first: Int, $fromBlock: Int) { + nullifiers(first: $first, orderBy: blockNumber, orderDirection: asc, where: { + blockNumber_gte: $fromBlock + }) { + nullifier + blockNumber + transactionHash + } + _meta { + block { + number + } + hasIndexingErrors + } + } +` \ No newline at end of file diff --git a/assets/services/idb.js b/assets/services/idb.js new file mode 100644 index 0000000..67c49c3 --- /dev/null +++ b/assets/services/idb.js @@ -0,0 +1,222 @@ +import { deleteDB, openDB } from 'idb' + +export const VERSION_ERROR = 'less than the existing version' +export const INDEX_DB_ERROR = 'A mutation operation was attempted on a database that did not allow mutations.' + +export const IDB_VERSION = 9 + +// TODO method for migration, remove indexed +export class IndexedDB { + constructor({ stores, dbName }) { + this.dbExists = false + this.isBlocked = false + + this.options = { + upgrade(db) { + Object.values(db.objectStoreNames).forEach((value) => { + db.deleteObjectStore(value) + }) + + stores.forEach(({ name, keyPath, indexes }) => { + const store = db.createObjectStore(name, { + keyPath, + autoIncrement: true, + }) + + if (Array.isArray(indexes)) { + indexes.forEach(({ name, unique = false }) => { + store.createIndex(name, String(name), { unique }) + }) + } + }) + }, + } + + this.dbName = dbName + } + + async initDB() { + try { + if (this.dbExists) { + return + } + + this.db = await openDB(this.dbName, IDB_VERSION, this.options) // version (optional): Schema version, or undefined to open the current version. + this.onEventHandler() + + this.dbExists = true + } catch (err) { + // need for private mode firefox browser + if (err.message.includes(INDEX_DB_ERROR)) { + this.isBlocked = true + return + } + + if (err.message.includes(VERSION_ERROR)) { + await this.removeExist() + } + + console.error(`initDB has error: ${err.message}`) + } + } + + async createTransactions({ storeName, data, mode = 'readwrite' }) { + try { + const tx = this.db.transaction(storeName, mode) + const storedItem = tx.objectStore(storeName) + + if (storedItem.add) { + await storedItem.add(data) + await tx.done + } + } catch (err) { + throw new Error(`Method createTransactions has error: ${err.message}`) + } + } + + createMultipleTransactions({ + storeName, + data, + index, + mode = 'readwrite', + }) { + try { + const tx = this.db.transaction(storeName, mode) + + data.forEach((item) => { + if (item && tx.store && tx.store.put) { + tx.store.put({ ...item, ...index }) + } + }) + } catch (err) { + throw new Error(`Method createMultipleTransactions has error: ${err.message}`) + } + } + + async getFromIndex(params) { + if (this.isBlocked) { + return + } + + try { + const item = await this.getFromIndexHandler(params) + return item + } catch (err) { + return undefined + } + } + + async getItem({ storeName, key }) { + try { + if (this.isBlocked) { + return + } + + const store = this.db.transaction(storeName).objectStore(storeName) + + const value = await store.get(key) + return value + } catch (err) { + throw new Error(`Method getItem has error: ${err.message}`) + } + } + + async addItem({ storeName, data, key }) { + try { + const tx = this.db.transaction(storeName, 'readwrite') + const isExist = await tx.objectStore(storeName).get(key) + + if (!isExist) { + await tx.objectStore(storeName).add(data) + } + } catch (err) { + throw new Error(`Method addItem has error: ${err.message}`) + } + } + + async putItem({ storeName, data }) { + try { + if (this.isBlocked) { + return + } + + const tx = this.db.transaction(storeName, 'readwrite') + await tx.objectStore(storeName).put(data) + } catch (err) { + throw new Error(`Method putItem has error: ${err.message}`) + } + } + + async getAll({ storeName }) { + try { + if (this.isBlocked || !this.dbExists) { + return [] + } + + const tx = this.db.transaction(storeName, 'readonly') + const store = tx.objectStore(storeName) + const data = await store.getAll() + return data + } catch (err) { + throw new Error(`Method getAll has error: ${err.message}`) + } + } + + async clearStore({ storeName, mode = 'readwrite' }) { + try { + const tx = this.db.transaction(storeName, mode) + const storedItem = tx.objectStore(storeName) + + if (storedItem.clear) { + await storedItem.clear() + } + } catch (err) { + throw new Error(`Method clearStore has error: ${err.message}`) + } + } + + async getAllFromIndex(params) { + if (this.isBlocked) { + return [] + } + + try { + const items = await this.getAllFromIndexHandler(params) + return items + } catch (err) { + return [] + } + } + + onEventHandler() { + // eslint-disable-next-line @typescript-eslint/no-misused-promises + this.db.addEventListener('onupgradeneeded', async () => { + await this.removeExist() + }) + } + + async removeExist() { + await deleteDB(this.dbName) + this.dbExists = false + + await this.initDB() + } + + async getFromIndexHandler({ storeName, indexName, key }) { + try { + const value = await this.db.getFromIndex(storeName, indexName, key) + return value + } catch (err) { + throw new Error(`Method getFromIndexHandler has error: ${err.message}`) + } + } + + async getAllFromIndexHandler({ storeName, indexName, key, count }) { + try { + const value = await this.db.getAllFromIndex(storeName, indexName, key, count) + return value + } catch (err) { + throw new Error(`Method getAllFromIndex has error: ${err.message}`) + } + } +} diff --git a/assets/services/pool.js b/assets/services/pool.js new file mode 100644 index 0000000..dcd750e --- /dev/null +++ b/assets/services/pool.js @@ -0,0 +1,1040 @@ +export const poolAbi = [ + { + inputs: [ + { + internalType: "contract IVerifier", + name: "_verifier2", + type: "address", + }, + { + internalType: "contract IVerifier", + name: "_verifier16", + type: "address", + }, + { + internalType: "uint32", + name: "_levels", + type: "uint32", + }, + { + internalType: "address", + name: "_hasher", + type: "address", + }, + { + internalType: "contract IERC6777", + name: "_token", + type: "address", + }, + { + internalType: "address", + name: "_omniBridge", + type: "address", + }, + { + internalType: "address", + name: "_l1Unwrapper", + type: "address", + }, + { + internalType: "address", + name: "_governance", + type: "address", + }, + { + internalType: "uint256", + name: "_l1ChainId", + type: "uint256", + }, + { + internalType: "address", + name: "_multisig", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bytes32", + name: "commitment", + type: "bytes32", + }, + { + indexed: false, + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "encryptedOutput", + type: "bytes", + }, + ], + name: "NewCommitment", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bytes32", + name: "nullifier", + type: "bytes32", + }, + ], + name: "NewNullifier", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "key", + type: "bytes", + }, + ], + name: "PublicKey", + type: "event", + }, + { + inputs: [], + name: "FIELD_SIZE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MAX_EXT_AMOUNT", + outputs: [ + { + internalType: "int256", + name: "", + type: "int256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MAX_FEE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MIN_EXT_AMOUNT_LIMIT", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ROOT_HISTORY_SIZE", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ZERO_VALUE", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ambBridge", + outputs: [ + { + internalType: "contract IAMB", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "int256", + name: "_extAmount", + type: "int256", + }, + { + internalType: "uint256", + name: "_fee", + type: "uint256", + }, + ], + name: "calculatePublicAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_minimalWithdrawalAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "_maximumDepositAmount", + type: "uint256", + }, + ], + name: "configureLimits", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "currentRootIndex", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "filledSubtrees", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getLastRoot", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "_left", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "_right", + type: "bytes32", + }, + ], + name: "hashLeftRight", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "hasher", + outputs: [ + { + internalType: "contract IHasher", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_minimalWithdrawalAmount", + type: "uint256", + }, + { + internalType: "uint256", + name: "_maximumDepositAmount", + type: "uint256", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "isCalledByOwner", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "_root", + type: "bytes32", + }, + ], + name: "isKnownRoot", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "_nullifierHash", + type: "bytes32", + }, + ], + name: "isSpent", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "l1Unwrapper", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "lastBalance", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "levels", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "maximumDepositAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minimalWithdrawalAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "multisig", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "nextIndex", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + name: "nullifierHashes", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "omniBridge", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC6777", + name: "_token", + type: "address", + }, + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "onTokenBridged", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes", + name: "proof", + type: "bytes", + }, + { + internalType: "bytes32", + name: "root", + type: "bytes32", + }, + { + internalType: "bytes32[]", + name: "inputNullifiers", + type: "bytes32[]", + }, + { + internalType: "bytes32[2]", + name: "outputCommitments", + type: "bytes32[2]", + }, + { + internalType: "uint256", + name: "publicAmount", + type: "uint256", + }, + { + internalType: "bytes32", + name: "extDataHash", + type: "bytes32", + }, + ], + internalType: "struct TornadoPool.Proof", + name: "_args", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "int256", + name: "extAmount", + type: "int256", + }, + { + internalType: "address", + name: "relayer", + type: "address", + }, + { + internalType: "uint256", + name: "fee", + type: "uint256", + }, + { + internalType: "bytes", + name: "encryptedOutput1", + type: "bytes", + }, + { + internalType: "bytes", + name: "encryptedOutput2", + type: "bytes", + }, + { + internalType: "bool", + name: "isL1Withdrawal", + type: "bool", + }, + { + internalType: "uint256", + name: "l1Fee", + type: "uint256", + }, + ], + internalType: "struct TornadoPool.ExtData", + name: "_extData", + type: "tuple", + }, + ], + name: "onTransact", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ownerChainId", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "bytes", + name: "publicKey", + type: "bytes", + }, + ], + internalType: "struct TornadoPool.Account", + name: "_account", + type: "tuple", + }, + ], + name: "register", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "owner", + type: "address", + }, + { + internalType: "bytes", + name: "publicKey", + type: "bytes", + }, + ], + internalType: "struct TornadoPool.Account", + name: "_account", + type: "tuple", + }, + { + components: [ + { + internalType: "bytes", + name: "proof", + type: "bytes", + }, + { + internalType: "bytes32", + name: "root", + type: "bytes32", + }, + { + internalType: "bytes32[]", + name: "inputNullifiers", + type: "bytes32[]", + }, + { + internalType: "bytes32[2]", + name: "outputCommitments", + type: "bytes32[2]", + }, + { + internalType: "uint256", + name: "publicAmount", + type: "uint256", + }, + { + internalType: "bytes32", + name: "extDataHash", + type: "bytes32", + }, + ], + internalType: "struct TornadoPool.Proof", + name: "_proofArgs", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "int256", + name: "extAmount", + type: "int256", + }, + { + internalType: "address", + name: "relayer", + type: "address", + }, + { + internalType: "uint256", + name: "fee", + type: "uint256", + }, + { + internalType: "bytes", + name: "encryptedOutput1", + type: "bytes", + }, + { + internalType: "bytes", + name: "encryptedOutput2", + type: "bytes", + }, + { + internalType: "bool", + name: "isL1Withdrawal", + type: "bool", + }, + { + internalType: "uint256", + name: "l1Fee", + type: "uint256", + }, + ], + internalType: "struct TornadoPool.ExtData", + name: "_extData", + type: "tuple", + }, + ], + name: "registerAndTransact", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IERC6777", + name: "_token", + type: "address", + }, + { + internalType: "address payable", + name: "_to", + type: "address", + }, + { + internalType: "uint256", + name: "_balance", + type: "uint256", + }, + ], + name: "rescueTokens", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "roots", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token", + outputs: [ + { + internalType: "contract IERC6777", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes", + name: "proof", + type: "bytes", + }, + { + internalType: "bytes32", + name: "root", + type: "bytes32", + }, + { + internalType: "bytes32[]", + name: "inputNullifiers", + type: "bytes32[]", + }, + { + internalType: "bytes32[2]", + name: "outputCommitments", + type: "bytes32[2]", + }, + { + internalType: "uint256", + name: "publicAmount", + type: "uint256", + }, + { + internalType: "bytes32", + name: "extDataHash", + type: "bytes32", + }, + ], + internalType: "struct TornadoPool.Proof", + name: "_args", + type: "tuple", + }, + { + components: [ + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "int256", + name: "extAmount", + type: "int256", + }, + { + internalType: "address", + name: "relayer", + type: "address", + }, + { + internalType: "uint256", + name: "fee", + type: "uint256", + }, + { + internalType: "bytes", + name: "encryptedOutput1", + type: "bytes", + }, + { + internalType: "bytes", + name: "encryptedOutput2", + type: "bytes", + }, + { + internalType: "bool", + name: "isL1Withdrawal", + type: "bool", + }, + { + internalType: "uint256", + name: "l1Fee", + type: "uint256", + }, + ], + internalType: "struct TornadoPool.ExtData", + name: "_extData", + type: "tuple", + }, + ], + name: "transact", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "verifier16", + outputs: [ + { + internalType: "contract IVerifier", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "verifier2", + outputs: [ + { + internalType: "contract IVerifier", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes", + name: "proof", + type: "bytes", + }, + { + internalType: "bytes32", + name: "root", + type: "bytes32", + }, + { + internalType: "bytes32[]", + name: "inputNullifiers", + type: "bytes32[]", + }, + { + internalType: "bytes32[2]", + name: "outputCommitments", + type: "bytes32[2]", + }, + { + internalType: "uint256", + name: "publicAmount", + type: "uint256", + }, + { + internalType: "bytes32", + name: "extDataHash", + type: "bytes32", + }, + ], + internalType: "struct TornadoPool.Proof", + name: "_args", + type: "tuple", + }, + ], + name: "verifyProof", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "i", + type: "uint256", + }, + ], + name: "zeros", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "pure", + type: "function", + }, +] \ No newline at end of file diff --git a/assets/services/provider.js b/assets/services/provider.js new file mode 100644 index 0000000..5d8741c --- /dev/null +++ b/assets/services/provider.js @@ -0,0 +1,86 @@ +import { ethers } from 'ethers' +import { fetchJson } from 'ethers/lib/utils' +import { numbers } from './constants' + +const defaultRetryAttempt = 0 + +export class ExtendedProvider extends ethers.providers.StaticJsonRpcProvider { + constructor(url, network, fallbackRpcs) { + super(url, network) + this.fallbackRpcs = fallbackRpcs + } + + async send(method, params, retryAttempt = defaultRetryAttempt) { + try { + return await super.send(method, params) + } catch (err) { + if (!retryAttempt) { + const TIME_OUT = 3000 + + await this.sleep(TIME_OUT) + + if (this.fallbackRpcs) { + return await this.fallbackSend(method, params, this.fallbackRpcs) + } + return this.send(method, params, ++retryAttempt) + } + throw err + } + } + + // eslint-disable-next-line + async fallbackSend(method, params, fallbackRpcs, retryAttempt = defaultRetryAttempt) { + + function getResult(payload) { + if (payload.error) { + const error = new Error(payload.error.message) + error.code = payload.error.code + error.data = payload.error.data + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw error + } + return payload.result + } + + try { + const request = { + method: method, + params: params, + id: this._nextId + numbers.ONE, + jsonrpc: '2.0', + } + + const result = fetchJson({ url: fallbackRpcs[retryAttempt] }, JSON.stringify(request), getResult).then( + (result) => result, + (error) => { + throw error + }, + ) + + return await result + } catch (err) { + retryAttempt += numbers.ONE + if (!fallbackRpcs[retryAttempt]) { + throw err + } else { + return await this.fallbackSend(method, params, fallbackRpcs, retryAttempt) + } + } + } + + async sleep(ms) { + return await new Promise((resolve) => setTimeout(resolve, ms)) + } + + // private checkRpcError(err: { data: string; code: string; message: string }) { + // const code = String(err?.code) + // const data = err.data?.toLowerCase() + // const message = err.message?.toLowerCase() + + // const ERROR_DATA = 'too many concurrent request' + // const ERROR_MESSAGE = 'timeout' + // const ERROR_CODE = '-32017' + + // return (data?.includes(ERROR_DATA) || message?.includes(ERROR_MESSAGE)) && code === ERROR_CODE + // } +} \ No newline at end of file diff --git a/assets/services/utilities.js b/assets/services/utilities.js new file mode 100644 index 0000000..ad2e9b6 --- /dev/null +++ b/assets/services/utilities.js @@ -0,0 +1,13 @@ +export const ZERO_ELEMENT = 0 + +export function getBatches(array, batchSize) { + const batches = [] + while (array.length) { + batches.push(array.splice(ZERO_ELEMENT, batchSize)) + } + return batches +} + +export async function sleep(ms) { + return await new Promise((resolve) => setTimeout(resolve, ms)) +} \ No newline at end of file diff --git a/assets/services/zip.js b/assets/services/zip.js new file mode 100644 index 0000000..fb9da92 --- /dev/null +++ b/assets/services/zip.js @@ -0,0 +1,25 @@ +import { zip, unzip } from 'fflate' + +export function zipAsync(file) { + return new Promise((res, rej) => { + zip(file, { mtime: new Date('1/1/1980') }, (err, data) => { + if (err) { + rej(err); + return; + } + res(data); + }); + }); +} + +export function unzipAsync(data) { + return new Promise((res, rej) => { + unzip(data, {}, (err, data) => { + if (err) { + rej(err); + return; + } + res(data); + }); + }); +} \ No newline at end of file diff --git a/assets/syncEvents.js b/assets/syncEvents.js new file mode 100644 index 0000000..28c34db --- /dev/null +++ b/assets/syncEvents.js @@ -0,0 +1,285 @@ +import path from 'path' +import { stat, readFile, writeFile } from 'fs/promises' +import { Contract, providers, utils } from 'ethers' + +import { BatchEventsService } from './services/batch' +import { getAllAccounts, getAllCommitments, getAllNullifiers } from './services/graph' +import { POOL_CONTRACT, BRIDGE_HELPER, RPC_LIST, ChainId, CHAINS, numbers } from './services/constants' +import { zipAsync, unzipAsync } from './services/zip' +import { poolAbi } from './services/pool' +import { bridgeAbi } from './services/bridgeHelper' + +const { getAddress } = utils +const { StaticJsonRpcProvider } = providers + +const EVENT_PATH = './static' + +async function existsAsync(fileOrDir) { + try { + await stat(fileOrDir); + + return true; + } catch { + return false; + } +} + +const getProvider = (chainId) => { + return new StaticJsonRpcProvider({ skipFetchSetup: true, url: RPC_LIST[chainId] }, chainId) +} + +const getTornadoPool = (chainId, provider) => { + const TornadoPool = new Contract(POOL_CONTRACT[chainId], poolAbi, provider) + + return { + TornadoPool, + BatchEventsService: new BatchEventsService({ + provider, + contract: TornadoPool + }) + } +} + +const getBridgeHelper = (chainId, provider) => { + const BridgeHelper = new Contract(BRIDGE_HELPER[chainId], bridgeAbi, provider) + + return { + BridgeHelper, + BridgeEventsService: new BatchEventsService({ + provider, + contract: BridgeHelper + }) + } +} + +const loadEvents = async (fileName, deployedBlock) => { + fileName = fileName.toLowerCase() + + const filePath = path.join(EVENT_PATH, fileName + '.zip') + + if (!(await existsAsync(filePath))) { + return { + events: [], + lastBlock: deployedBlock + } + } + + try { + const data = await readFile(filePath) + const { [fileName]: content } = await unzipAsync(data) + + const events = JSON.parse(new TextDecoder().decode(content)) + + const lastBlock = events && Array.isArray(events) && events[events.length - 1] + ? events[events.length - 1].blockNumber + : deployedBlock + + return { + events, + lastBlock + } + } catch { + return { + events: [], + lastBlock: deployedBlock + } + } +} + +const saveEvents = async (fileName, events) => { + fileName = fileName.toLowerCase() + + const filePath = path.join(EVENT_PATH, fileName + '.zip') + + const payload = await zipAsync({ + [fileName]: new TextEncoder().encode(JSON.stringify(events, null, 2) + '\n') + }) + + await writeFile(filePath, payload) +} + +const syncAccounts = async (chainId, BatchEventsService) => { + const fileName = `accounts_${chainId}.json` + + console.log(`Syncing ${fileName}`) + + const cachedEvents = await loadEvents(fileName, CHAINS[chainId].deployBlock) + + const events = [...cachedEvents.events] + let fromBlock = cachedEvents.lastBlock + numbers.ONE + + console.log({ + cachedEvents: events.length, + cachedBlock: fromBlock + }) + + const { events: graphEvents, lastSyncBlock } = await getAllAccounts({ + fromBlock, + chainId + }) + + console.log({ + graphEvents: graphEvents.length, + graphBlock: lastSyncBlock + }) + + if (lastSyncBlock) { + events.push(...graphEvents) + fromBlock = lastSyncBlock + } + + let nodeEvents = await BatchEventsService.getBatchEvents({ + fromBlock, + type: 'PublicKey' + }) + + console.log({ + nodeEvents: nodeEvents.length, + nodeBlock: nodeEvents && nodeEvents[nodeEvents.length - 1] ? nodeEvents[nodeEvents.length - 1].blockNumber : undefined + }) + + if (nodeEvents && nodeEvents.length) { + nodeEvents = nodeEvents.map(({ blockNumber, args }) => ({ + key: args.key, + owner: getAddress(args.owner), + blockNumber, + })) + + events.push(...nodeEvents) + } + + await saveEvents(fileName, events) +} + +const syncCommitments = async (chainId, BatchEventsService) => { + const fileName = `commitments_${chainId}.json` + + console.log(`Syncing ${fileName}`) + + const cachedEvents = await loadEvents(fileName, CHAINS[chainId].deployBlock) + + const events = [...cachedEvents.events] + let fromBlock = cachedEvents.lastBlock + numbers.ONE + + console.log({ + cachedEvents: events.length, + cachedBlock: fromBlock + }) + + const { events: graphEvents, lastSyncBlock } = await getAllCommitments({ + fromBlock, + chainId + }) + + console.log({ + graphEvents: graphEvents.length, + graphBlock: lastSyncBlock + }) + + if (lastSyncBlock) { + events.push(...graphEvents) + fromBlock = lastSyncBlock + } + + let nodeEvents = await BatchEventsService.getBatchEvents({ + fromBlock, + type: 'NewCommitment' + }) + + console.log({ + nodeEvents: nodeEvents.length, + nodeBlock: nodeEvents && nodeEvents[nodeEvents.length - 1] ? nodeEvents[nodeEvents.length - 1].blockNumber : undefined + }) + + if (nodeEvents && nodeEvents.length) { + nodeEvents = nodeEvents.map(({ blockNumber, transactionHash, args }) => ({ + blockNumber, + transactionHash, + index: Number(args.index), + commitment: args.commitment, + encryptedOutput: args.encryptedOutput, + })) + + events.push(...nodeEvents) + } + + await saveEvents(fileName, events) +} + +const syncNullifiers = async (chainId, BatchEventsService) => { + const fileName = `nullifiers_${chainId}.json` + + console.log(`Syncing ${fileName}`) + + const cachedEvents = await loadEvents(fileName, CHAINS[chainId].deployBlock) + + const events = [...cachedEvents.events] + let fromBlock = cachedEvents.lastBlock + numbers.ONE + + console.log({ + cachedEvents: events.length, + cachedBlock: fromBlock + }) + + const { events: graphEvents, lastSyncBlock } = await getAllNullifiers({ + fromBlock, + chainId + }) + + console.log({ + graphEvents: graphEvents.length, + graphBlock: lastSyncBlock + }) + + if (lastSyncBlock) { + events.push(...graphEvents) + fromBlock = lastSyncBlock + } + + let nodeEvents = await BatchEventsService.getBatchEvents({ + fromBlock, + type: 'NewNullifier' + }) + + console.log({ + nodeEvents: nodeEvents.length, + nodeBlock: nodeEvents && nodeEvents[nodeEvents.length - 1] ? nodeEvents[nodeEvents.length - 1].blockNumber : undefined + }) + + if (nodeEvents && nodeEvents.length) { + nodeEvents = nodeEvents.map(({ blockNumber, transactionHash, args }) => ({ + blockNumber, + transactionHash, + nullifier: args.nullifier, + })) + + events.push(...nodeEvents) + } + + await saveEvents(fileName, events) +} + +const main = async () => { + const chainId = ChainId.XDAI + + const ethChainId = ChainId.MAINNET + + const provider = getProvider(chainId) + + const ethProvider = getProvider(ethChainId) + + const { BatchEventsService } = getTornadoPool(chainId, provider) + + const { BridgeEventsService } = getBridgeHelper(ethChainId, ethProvider) + + console.log(`Connected with ${chainId}: (block: ${await provider.getBlockNumber()})`) + + console.log(`Connected with ${ethChainId}: (block: ${await ethProvider.getBlockNumber()})`) + + await syncAccounts(ethChainId, BridgeEventsService) + + await syncCommitments(chainId, BatchEventsService) + + await syncNullifiers(chainId, BatchEventsService) +} +main() \ No newline at end of file diff --git a/containers/Provider/Provider.vue b/containers/Provider/Provider.vue index 7dae6d5..42f1fc0 100644 --- a/containers/Provider/Provider.vue +++ b/containers/Provider/Provider.vue @@ -55,7 +55,6 @@ export default { 'setProvider', 'changeChain', 'checkNetwork', - 'checkSanction', 'setWalletParams', 'getWalletBalance', ]), @@ -93,9 +92,6 @@ export default { const address = await provider.setupProvider() const network = await provider.checkNetworkVersion() - if (address) { - await this.checkSanction(address) - } await this.setProvider({ network, name: key }) await this.setAccountData(address) @@ -126,8 +122,6 @@ export default { if (address) { const checksumAddress = toChecksumAddress(address) - await this.checkSanction(checksumAddress) - if (!this.isConnected) { return } diff --git a/copyFile.ts b/copyFile.ts new file mode 100644 index 0000000..ca38712 --- /dev/null +++ b/copyFile.ts @@ -0,0 +1,15 @@ +import { argv } from 'process' +import { copyFile } from 'fs' + +function copyFiles() { + const [, , inFile, outFile] = argv + + copyFile(inFile, outFile, function(err) { + if (err) { + throw err + } + + console.log(`Copied ${inFile} to ${outFile}`) + }) +} +copyFiles() diff --git a/nuxt.config.ts b/nuxt.config.ts index 6e8ce9f..140a5b5 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -123,6 +123,7 @@ const config: NuxtConfig = { config.output.globalObject = 'this' } if (config?.module != null) { + /** if (isClient) { config.module.rules.push({ test: /\.worker\.js$/, @@ -133,6 +134,7 @@ const config: NuxtConfig = { }, }) } + **/ config.module.rules.push({ test: /\.bin$/, diff --git a/package.json b/package.json index 00284c2..35889cb 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,12 @@ "lint": "eslint --ext .js,.ts", "lint:fix": "eslint --ext .js,.ts --quiet --fix", "compile": "typechain --target ethers-v5 --out-dir ./_contracts './abi/*.json'", - "generate": "nuxt generate && cp dist/404.html dist/ipfs-404.html", + "copyFile": "node --loader ts-node/esm copyFile.ts", + "generate": "yarn worker:compile && nuxt generate && yarn copyFile dist/404.html dist/ipfs-404.html", "prepare": "husky install", "ipfs:upload": "node --loader ts-node/esm ipfsUpload.ts", - "worker:compile": "nuxt generate && yarn compile:events && yarn compile:nullifier", - "compile:events": "babel dist/_nuxt/workers/events.worker.js --out-file static/events.worker.js", - "compile:nullifier": "babel dist/_nuxt/workers/nullifier.worker.js --out-file static/nullifier.worker.js" + "worker:compile": "webpack", + "update:events": "webpack && node ./syncEvents.cjs" }, "dependencies": { "@apollo/client": "^3.4.16", @@ -74,7 +74,7 @@ "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-vue": "^7.16.0", - "fibers": "^5.0.0", + "fflate": "^0.8.2", "form-data": "^4.0.0", "husky": "^6.0.0", "lint-staged": "10.2.11", @@ -87,6 +87,7 @@ "typechain": "^5.1.0", "typescript": "^4.3.4", "vue-eslint-parser": "^7.6.0", + "webpack-cli": "^4.10.0", "worker-loader": "^3.0.8" }, "husky": { diff --git a/services/events/batch.ts b/services/events/batch.ts new file mode 100644 index 0000000..0925c75 --- /dev/null +++ b/services/events/batch.ts @@ -0,0 +1,109 @@ +import { Provider, Contract, EventLog } from "ethers"; +import { sleep, getBatches } from "@/utilities"; + +export interface BatchEventServiceConstructor { + provider: Provider; + contract: Contract; + concurrencySize?: number; + blocksPerRequest?: number; + shouldRetry?: boolean; + retryMax?: number; + retryOn?: number; +} + +export type EventInput = { + fromBlock: number; + toBlock: number; + type: string; +}; + +export class BatchEventsService { + provider: Provider; + contract: Contract; + concurrencySize: number; + blocksPerRequest: number; + shouldRetry: boolean; + retryMax: number; + retryOn: number; + constructor({ + provider, + contract, + concurrencySize = 10, + blocksPerRequest = 2000, + shouldRetry = true, + retryMax = 5, + retryOn = 500, + }: BatchEventServiceConstructor) { + this.provider = provider; + this.contract = contract; + this.concurrencySize = concurrencySize; + this.blocksPerRequest = blocksPerRequest; + this.shouldRetry = shouldRetry; + this.retryMax = retryMax; + this.retryOn = retryOn; + } + + async getPastEvents({ fromBlock, toBlock, type }: EventInput): Promise { + let err; + let retries = 0; + + // eslint-disable-next-line no-unmodified-loop-condition + while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) { + try { + return (await this.contract.queryFilter(type, fromBlock, toBlock)) as EventLog[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + err = e; + retries++; + + // If provider.getBlockNumber returned last block that isn't accepted (happened on Avalanche/Gnosis), + // get events to last accepted block + if (e.message.includes('after last accepted block')) { + const acceptedBlock = parseInt(e.message.split('after last accepted block ')[1]); + toBlock = acceptedBlock; + } + + // retry on 0.5 seconds + await sleep(this.retryOn); + } + } + + throw err; + } + + createBatchRequest(batchArray: EventInput[]): Promise[] { + return batchArray.map(async (event: EventInput, index: number) => { + await sleep(20 * index); + + return this.getPastEvents(event); + }); + } + + async getBatchEvents({ fromBlock, toBlock, type = '*' }: EventInput): Promise { + if (!toBlock) { + toBlock = await this.provider.getBlockNumber(); + } + + const eventsToSync: EventInput[] = []; + + for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) { + const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1; + + eventsToSync.push({ fromBlock: i, toBlock: j, type } as EventInput); + } + + const events = []; + const eventChunk = getBatches(eventsToSync, this.concurrencySize); + + let chunkCount = 0; + + for (const chunk of eventChunk) { + chunkCount++; + + const fetchedEvents = (await Promise.all(this.createBatchRequest(chunk))).flat(); + events.push(...fetchedEvents); + } + + return events; + } +} diff --git a/services/events/index.ts b/services/events/index.ts index c365c51..40bd0ea 100644 --- a/services/events/index.ts +++ b/services/events/index.ts @@ -7,6 +7,9 @@ import { isEmpty, sleep, toChecksumAddress } from '@/utilities' import { getBridgeHelper, getBridgeProxy, getAmbBridge } from '@/contracts' import { EventsClass, GetAffirmationParams, GetRelayedMessageParams, SaveEventsParams } from './@types' +import { downloadEvents } from './load' + +export * from './batch' class EventAggregator implements EventsClass { public async getBackupedAddressFromPublicKey(publicKey: string) { @@ -31,19 +34,31 @@ class EventAggregator implements EventsClass { storeName: `${IndexDBStores.ACCOUNT_EVENTS}_${chainId}`, }) + const newEvents = [] + if (cachedEvents?.length) { const [latestEvent] = cachedEvents.slice(-numbers.ONE) blockFrom = Number(latestEvent.blockNumber) + numbers.ONE + } else { + const downloadedEvents = await downloadEvents(`accounts_${chainId}.json`, blockFrom) + + if (downloadedEvents.events.length) { + newEvents.push(...downloadedEvents.events) + + blockFrom = downloadedEvents.lastBlock + } } const { events: graphEvents, lastSyncBlock } = await getAllAccounts({ fromBlock: blockFrom, chainId }) - const [account] = graphEvents.filter((e: { key: string }) => e.key === publicKey) + newEvents.push(...graphEvents) + + const [account] = newEvents.filter((e: { key: string }) => e.key === publicKey) if (account) { this.saveEvents({ chainId, - events: graphEvents, + events: newEvents, storeName: IndexDBStores.ACCOUNT_EVENTS, }) return account.owner @@ -66,7 +81,7 @@ class EventAggregator implements EventsClass { } }) - const newEvents = graphEvents.concat(accountEvents) + newEvents.push(...accountEvents) this.saveEvents({ chainId, @@ -74,7 +89,7 @@ class EventAggregator implements EventsClass { storeName: IndexDBStores.ACCOUNT_EVENTS, }) - const events = cachedEvents.concat(newEvents).filter((e: { key: string }) => e.key === publicKey) + const events = newEvents.filter((e: { key: string }) => e.key === publicKey) if (isEmpty(events)) { return undefined @@ -85,6 +100,7 @@ class EventAggregator implements EventsClass { return event.owner } catch (err) { + console.log(err) return undefined } } @@ -111,19 +127,30 @@ class EventAggregator implements EventsClass { storeName: `${IndexDBStores.ACCOUNT_EVENTS}_${chainId}`, }) + const newEvents = [] + if (cachedEvents?.length) { const [latestEvent] = cachedEvents.slice(-numbers.ONE) blockFrom = Number(latestEvent.blockNumber) + numbers.ONE + } else { + const downloadedEvents = await downloadEvents(`accounts_${chainId}.json`, blockFrom) + + if (downloadedEvents.events.length) { + newEvents.push(...downloadedEvents.events) + + blockFrom = downloadedEvents.lastBlock + } } const { events: graphEvents, lastSyncBlock } = await getAllAccounts({ fromBlock: blockFrom, chainId }) + newEvents.push(...graphEvents) - const [account] = graphEvents.filter((e: { owner: string }) => toChecksumAddress(e.owner) === toChecksumAddress(address)) + const [account] = newEvents.filter((e: { owner: string }) => toChecksumAddress(e.owner) === toChecksumAddress(address)) if (account) { this.saveEvents({ chainId, - events: graphEvents, + events: newEvents, storeName: IndexDBStores.ACCOUNT_EVENTS, }) return account.key @@ -146,7 +173,7 @@ class EventAggregator implements EventsClass { } }) - const newEvents = graphEvents.concat(accountEvents) + newEvents.push(...accountEvents) this.saveEvents({ chainId, @@ -167,6 +194,7 @@ class EventAggregator implements EventsClass { return event.key } catch (err) { + console.log(err) return undefined } } diff --git a/services/events/load.ts b/services/events/load.ts new file mode 100644 index 0000000..332ce02 --- /dev/null +++ b/services/events/load.ts @@ -0,0 +1,49 @@ +import { unzip } from 'fflate' + +export function unzipAsync(data: Uint8Array) { + return new Promise((res, rej) => { + unzip(data, {}, (err, data) => { + if (err) { + rej(err); + return; + } + res(data); + }); + }); +} + +export async function downloadEvents(fileName: string, deployedBlock: number) { + fileName = fileName.toLowerCase() + + // @ts-ignore + const prefix = __webpack_public_path__.slice(0, -7) + + try { + const resp = await fetch(`${prefix}/${fileName}.zip`, { + method: 'GET', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + }) + + const arrayBuffer = await resp.arrayBuffer() + + const { [fileName]: content } = (await unzipAsync(new Uint8Array(arrayBuffer))) as any + + const events = JSON.parse(new TextDecoder().decode(content)) + + const lastBlock = events && Array.isArray(events) && events[events.length - 1] + ? events[events.length - 1].blockNumber + : deployedBlock + + return { + events, + lastBlock + } + } catch { + return { + events: [], + lastBlock: deployedBlock + } + } +} \ No newline at end of file diff --git a/services/graph/@types/index.ts b/services/graph/@types/index.ts index 64c6558..5432e6d 100644 --- a/services/graph/@types/index.ts +++ b/services/graph/@types/index.ts @@ -12,3 +12,21 @@ export type Account = { } export type Accounts = Account[] + +export type Commitment = { + index: string + commitment: string + blockNumber: string + encryptedOutput: string + transactionHash: string +} + +export type Commitments = Commitment[] + +export type Nullifier = { + nullifier: string + blockNumber: string + transactionHash: string +} + +export type Nullifiers = Nullifier[] \ No newline at end of file diff --git a/services/graph/index.ts b/services/graph/index.ts index b176a97..06e7149 100644 --- a/services/graph/index.ts +++ b/services/graph/index.ts @@ -5,8 +5,8 @@ import { ChainId } from '@/types' import { numbers } from '@/constants' import { isEmpty, toChecksumAddress } from '@/utilities' -import { Params, Accounts } from './@types' -import { _META, GET_ACCOUNTS, GET_REGISTERED } from './queries' +import { Params, Accounts, Commitments, Nullifiers } from './@types' +import { _META, GET_ACCOUNTS, GET_REGISTERED, GET_COMMITMENT, GET_NULLIFIER } from './queries' const first = 1000 const breakLength = 900 @@ -18,7 +18,8 @@ const link = (operation: Operation) => { const CHAIN_GRAPH_URLS: { [chainId in ChainId]: string } = { [ChainId.BSC]: 'https://api.thegraph.com/subgraphs/name/dan1kov/bsc-tornado-pool-subgraph', - [ChainId.MAINNET]: 'https://api.thegraph.com/subgraphs/name/tornadocash/mainnet-tornado-pool-subgraph', + [ChainId.MAINNET]: 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/mainnet-tornado-pool-subgraph', + [ChainId.XDAI]: 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/gnosis-tornado-nova-subgraph', } const client = new ApolloClient({ @@ -27,7 +28,7 @@ const client = new ApolloClient({ }) const registryClient = new ApolloClient({ - uri: 'https://api.thegraph.com/subgraphs/name/tornadocash/tornado-relayer-registry', + uri: 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/tornado-relayer-registry', cache: new InMemoryCache(), }) @@ -169,3 +170,166 @@ async function getMeta({ chainId }: Params) { return undefined } } + + +export async function getCommitments({ fromBlock, chainId }: Params): Promise<{ + results: Commitments, + lastSyncBlock: number +}> { + const { data } = await client.query({ + context: { + chainId, + }, + query: gql(GET_COMMITMENT), + variables: { first, fromBlock }, + }) + + if (!data) { + return { + results: [], + lastSyncBlock: data._meta.block.number + } + } + + return { + results: data.commitments, + lastSyncBlock: data._meta.block.number + } +} + +export async function getAllCommitments({ fromBlock, chainId }: Params) { + try { + let commitments: Commitments = [] + let lastSyncBlock + + while (true) { + let { results, lastSyncBlock: lastBlock } = await getCommitments({ fromBlock, chainId }) + + lastSyncBlock = lastBlock + + if (isEmpty(results)) { + break + } + + if (results.length < breakLength) { + commitments = commitments.concat(results) + break + } + + const [lastEvent] = results.slice(-numbers.ONE) + + results = results.filter((e) => e.blockNumber !== lastEvent.blockNumber) + fromBlock = Number(lastEvent.blockNumber) + + commitments = commitments.concat(results) + } + + if (!commitments) { + return { + lastSyncBlock, + events: [], + } + } + + const data = commitments + .map((e) => ({ + index: Number(e.index), + commitment: e.commitment, + blockNumber: Number(e.blockNumber), + encryptedOutput: e.encryptedOutput, + transactionHash: e.transactionHash + })) + .sort((a, b) => a.index - b.index) + + const [lastEvent] = data.slice(-numbers.ONE) + + return { + events: data, + lastSyncBlock: lastEvent?.blockNumber > lastSyncBlock ? lastEvent.blockNumber + numbers.ONE : lastSyncBlock, + } + } catch { + return { + lastSyncBlock: '', + events: [], + } + } +} + +export async function getNullifiers({ fromBlock, chainId }: Params): Promise<{ + results: Nullifiers, + lastSyncBlock: number +}> { + const { data } = await client.query({ + context: { + chainId, + }, + query: gql(GET_NULLIFIER), + variables: { first, fromBlock }, + }) + + if (!data) { + return { + results: [], + lastSyncBlock: data._meta.block.number + } + } + + return { + results: data.nullifiers, + lastSyncBlock: data._meta.block.number + } +} + +export async function getAllNullifiers({ fromBlock, chainId }: Params) { + try { + let nullifiers: Nullifiers = [] + let lastSyncBlock + + while (true) { + let { results, lastSyncBlock: lastBlock } = await getNullifiers({ fromBlock, chainId }) + + lastSyncBlock = lastBlock + + if (isEmpty(results)) { + break + } + + if (results.length < breakLength) { + nullifiers = nullifiers.concat(results) + break + } + + const [lastEvent] = results.slice(-numbers.ONE) + + results = results.filter((e) => e.blockNumber !== lastEvent.blockNumber) + fromBlock = Number(lastEvent.blockNumber) + + nullifiers = nullifiers.concat(results) + } + + if (!nullifiers) { + return { + lastSyncBlock, + events: [], + } + } + + const data = nullifiers.map((e) => ({ + nullifier: e.nullifier, + blockNumber: Number(e.blockNumber), + transactionHash: e.transactionHash + })) + + const [lastEvent] = data.slice(-numbers.ONE) + + return { + events: data, + lastSyncBlock: lastEvent?.blockNumber > lastSyncBlock ? lastEvent.blockNumber + numbers.ONE : lastSyncBlock, + } + } catch { + return { + lastSyncBlock: '', + events: [], + } + } +} \ No newline at end of file diff --git a/services/graph/queries.ts b/services/graph/queries.ts index a5de7f7..6ea55b3 100644 --- a/services/graph/queries.ts +++ b/services/graph/queries.ts @@ -33,3 +33,41 @@ export const GET_REGISTERED = ` } } ` + +export const GET_COMMITMENT = ` + query getCommitment($first: Int, $fromBlock: Int) { + commitments(first: $first, orderBy: blockNumber, orderDirection: asc, where: { + blockNumber_gte: $fromBlock + }) { + index + commitment + blockNumber + encryptedOutput + transactionHash + } + _meta { + block { + number + } + hasIndexingErrors + } + } +` + +export const GET_NULLIFIER = ` + query getNullifier($first: Int, $fromBlock: Int) { + nullifiers(first: $first, orderBy: blockNumber, orderDirection: asc, where: { + blockNumber_gte: $fromBlock + }) { + nullifier + blockNumber + transactionHash + } + _meta { + block { + number + } + hasIndexingErrors + } + } +` \ No newline at end of file diff --git a/services/worker/index.ts b/services/worker/index.ts index 918f5ce..2247d69 100644 --- a/services/worker/index.ts +++ b/services/worker/index.ts @@ -7,12 +7,6 @@ import { CommitmentEvents, NullifierEvents } from '@/services/events/@types' import { EventsPayload, DecryptedEvents, GetEventsFromTxHashParams } from './@types' -import '@/assets/events.worker.js' -import '@/assets/nullifier.worker.js' - -// import NWorker from '@/assets/nullifier.worker.js' -// import EWorker from '@/assets/events.worker.js' - export interface WorkerProvider { workerSetup: (chainId: ChainId) => void getCommitmentEvents: () => Promise @@ -41,13 +35,8 @@ class Provider implements WorkerProvider { const basePath = `${window.location.origin}${ipfsPathPrefix}` - this.nullifierWorkers = new Array(CORES).fill('').map(() => new Worker(`${basePath}/_nuxt/workers/nullifier.worker.js`)) - this.eventsWorkers = new Array(CORES).fill('').map(() => new Worker(`${basePath}/_nuxt/workers/events.worker.js`)) - - // // @ts-expect-error - // this.nullifierWorkers = new Array(CORES).fill('').map(() => new NWorker()) - // // @ts-expect-error - // this.eventsWorkers = new Array(CORES).fill('').map(() => new EWorker()) + this.nullifierWorkers = new Array(CORES).fill('').map(() => new Worker(`${basePath}/nullifier.worker.js`)) + this.eventsWorkers = new Array(CORES).fill('').map(() => new Worker(`${basePath}/events.worker.js`)) } public workerSetup = (chainId: ChainId) => { diff --git a/static/accounts_1.json.zip b/static/accounts_1.json.zip new file mode 100644 index 0000000..d329c41 Binary files /dev/null and b/static/accounts_1.json.zip differ diff --git a/static/commitments_100.json.zip b/static/commitments_100.json.zip new file mode 100644 index 0000000..956d16d Binary files /dev/null and b/static/commitments_100.json.zip differ diff --git a/static/nullifiers_100.json.zip b/static/nullifiers_100.json.zip new file mode 100644 index 0000000..48e9726 Binary files /dev/null and b/static/nullifiers_100.json.zip differ diff --git a/store/wallet.ts b/store/wallet.ts index 08762e0..7102646 100644 --- a/store/wallet.ts +++ b/store/wallet.ts @@ -19,17 +19,6 @@ export const actions: ActionTree = { } }, - async checkSanction({ getters }, address) { - const contract = getSanctionList(getters.dependencies.l1ChainId) - - const isSanctioned = await contract.callStatic.isSanctioned(address) - if (isSanctioned) { - window.onbeforeunload = null - // ToDo add type - // @ts-expect-error - window.location = 'https://twitter.com/TornadoCash/status/1514904975037669386' - } - }, checkAppNetwork({ commit }, network) { try { // TODO create a selector for active network diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..b28ddb4 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,53 @@ +import path from 'path' +import webpack from 'webpack' + +export default [ + { + mode: 'production', + entry: './assets/events.worker.js', + output: { + path: path.resolve('static'), + filename: 'events.worker.js', + } + }, + { + mode: 'production', + entry: './assets/nullifier.worker.js', + output: { + path: path.resolve('static'), + filename: 'nullifier.worker.js', + } + }, + { + mode: 'production', + entry: './assets/syncEvents.js', + output: { + path: path.resolve('.'), + filename: 'syncEvents.cjs', + }, + target: 'node', + plugins: [ + new webpack.BannerPlugin({ + banner: '#!/usr/bin/env node\n', + raw: true + }) + ], + module: { + rules: [ + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto' + } + ] + }, + resolve: { + alias: { + 'fflate': 'fflate/esm' + } + }, + optimization: { + minimize: false, + } + } +] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 93283a7..b1d8291 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1320,7 +1320,7 @@ resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz#2cbcf822bf3764c9658c4d2e568bd0c0cb748016" integrity "sha1-LLz4Ir83ZMlljE0uVovQwMt0gBY= sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==" -"@discoveryjs/json-ext@0.5.7": +"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0": version "0.5.7" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity "sha1-HVcr+74Ut3BOC6Dzm3SBW4SHDXA= sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==" @@ -3040,6 +3040,23 @@ "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + "@wry/context@^0.7.0": version "0.7.3" resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.3.tgz#240f6dfd4db5ef54f81f6597f6714e58d4f476a1" @@ -4317,6 +4334,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + clone-response@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" @@ -4371,7 +4397,7 @@ colord@^2.9.1: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity "sha1-T4zpGd5Fbx1cHDaMMH/iDz5Z+0M= sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==" -colorette@^2.0.10: +colorette@^2.0.10, colorette@^2.0.14: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity "sha1-nreT5oMwZ/cjWQL807CZF6AAqVo= sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" @@ -4407,7 +4433,7 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity "sha1-Rqu9FlL44Fm92u+Zu9yyrZzxea4= sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" -commander@^7.2.0: +commander@^7.0.0, commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity "sha1-o2y1fQtQHOEI5NIFWaFQo5HZerc= sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" @@ -5108,11 +5134,6 @@ detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity "sha1-OHHMCmoALow+Wzz38zYmRnXwa50= sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==" -detect-libc@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" - integrity "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" - devalue@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/devalue/-/devalue-2.0.1.tgz#5d368f9adc0928e47b77eea53ca60d2f346f9762" @@ -5364,6 +5385,11 @@ entities@^4.2.0: resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity "sha1-XSaOpecRPsdMTQM7eepaNaSI+0g= sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" +envinfo@^7.7.3: + version "7.13.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" + integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== + errno@^0.1.3, errno@~0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" @@ -6212,6 +6238,11 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + fastfile@0.0.18: version "0.0.18" resolved "https://registry.yarnpkg.com/fastfile/-/fastfile-0.0.18.tgz#2b69bbbfd2fcccc9bc8099c27de1379b89756a4b" @@ -6291,6 +6322,11 @@ ffjavascript@^0.2.48: wasmcurves "0.2.2" web-worker "^1.2.0" +fflate@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" + integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== + ffwasm@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ffwasm/-/ffwasm-0.0.7.tgz#23bb9a3537ecc87c0f24fcfb3a9ddd0e86855fff" @@ -6299,13 +6335,6 @@ ffwasm@0.0.7: big-integer "^1.6.48" wasmbuilder "0.0.10" -fibers@^5.0.0: - version "5.0.3" - resolved "https://registry.yarnpkg.com/fibers/-/fibers-5.0.3.tgz#2fd03acb255db66fe693d15beafbf5ae92193fd7" - integrity "sha1-L9A6yyVdtm/mk9Fb6vv1rpIZP9c= sha512-/qYTSoZydQkM21qZpGLDLuCq8c+B8KhuCQ1kLPvnRNhxhVbvrpmH9l2+Lblf5neDuEsY4bfT7LeO553TXQDvJw==" - dependencies: - detect-libc "^1.0.3" - figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" @@ -6702,6 +6731,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity "sha1-pWiZ0+o8m6uHS7l3O3xe3pL0iV0= sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -7121,6 +7155,13 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + he@1.2.0, he@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" @@ -7326,6 +7367,14 @@ import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -7416,6 +7465,11 @@ internal-slot@^1.0.5: has "^1.0.3" side-channel "^1.0.4" +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + invert-kv@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" @@ -7513,6 +7567,13 @@ is-core-module@^2.11.0: dependencies: has "^1.0.3" +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -9493,7 +9554,7 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0: +pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity "sha1-8JkTPfft5CLoHR2ESCcO6z5CYfM= sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==" @@ -10601,6 +10662,13 @@ readline@^1.3.0: resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c" integrity sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg== +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" @@ -10758,11 +10826,23 @@ resolve-alpn@^1.0.0, resolve-alpn@^1.2.0: resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" integrity "sha1-t629rDVGqq7CC0Xn2CZZJwcnJvk= sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity "sha1-SrzYUq0y3Xuqv+m0DgCjbbXzkuY= sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -10777,6 +10857,15 @@ resolve@^1.1.7, resolve@^1.10.1, resolve@^1.14.2, resolve@^1.22.0, resolve@^1.22 path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + response-iterator@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da" @@ -11149,6 +11238,13 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -12948,6 +13044,24 @@ webpack-bundle-analyzer@^4.9.0: sirv "^1.0.7" ws "^7.3.1" +webpack-cli@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + webpack-dev-middleware@^5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" @@ -12968,6 +13082,15 @@ webpack-hot-middleware@^2.25.3: html-entities "^2.1.0" strip-ansi "^6.0.0" +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + webpack-node-externals@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#1a3407c158d547a9feb4229a9e3385b7b60c9917" @@ -13089,6 +13212,11 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"