import chai from 'chai' import * as ganache from 'ganache' // External import { once } from 'events' import { solidity } from 'ethereum-waffle' import { providers, BigNumber } from 'ethers' import { parseUnits } from 'ethers/lib/utils' // @ts-expect-error import { parseIndexableString } from 'pouchdb-collate' // Local import { ERC20, TornadoInstance } from './deth' import { Files, Onchain, RelayerProperties } from '@tornado/sdk-data' import { Chain, Contracts } from '@tornado/sdk-chain' import { ErrorUtils } from '@tornado/sdk-utils' import { TorProvider } from '@tornado/sdk-web' import { Core } from '@tornado/sdk-core' // Data import eth01DepositsReference from './resources/deposits_eth_0.1.json' import eth1DepositsReference from './resources/deposits_eth_1.json' import eth10DepositsReference from './resources/deposits_eth_10.json' import eth100DepositsReference from './resources/deposits_eth_100.json' import dai100DepositsReference from './resources/deposits_dai_100.json' import dai1000DepositsReference from './resources/deposits_dai_1000.json' import dai10000DepositsReference from './resources/deposits_dai_10000.json' import dai100KDepositsReference from './resources/deposits_dai_100000.json' chai.use(solidity) const expect = chai.expect describe('Core', () => { const torify = process.env.TORIFY === 'true' const debug = process.env.DEBUG === 'true' if (!process.env.ETH_MAINNET_TEST_RPC) throw ErrorUtils.getError('need a mainnet rpc endpoint.') console.log('\nNote that these tests are time intensive. ā³. ā³.. ā³...\n') console.log( 'Also, we are using ganache because we just need a forked blockchain and not an entire environment. šŸ§' ) let daiAddress: string const daiWhale = '0x5777d92f208679db4b9778590fa3cab3ac9e2168' // Uniswap V3 Something/Dai Pool const mainnetProvider: providers.Provider = torify ? new TorProvider(process.env.ETH_MAINNET_TEST_RPC!, { port: +process.env.TOR_PORT! }) : new providers.JsonRpcProvider(process.env.ETH_MAINNET_TEST_RPC) const _ganacheProvider = ganache.provider({ chain: { chainId: 1 }, // @ts-ignore fork: { url: process.env.ETH_MAINNET_TEST_RPC }, logging: { quiet: true }, wallet: { totalAccounts: 20, unlockedAccounts: [daiWhale] } }) // @ts-expect-error const ganacheProvider = new providers.Web3Provider(_ganacheProvider) const chain = new Chain(ganacheProvider) after(async function () { this.timeout(0) await Files.wipeCache() }) describe('namespace Contracts', () => { it('getClassicInstance: should be able to get a tornado instance', async () => { let instance = Contracts.getInstance(String(1), 'eth', String(1), mainnetProvider) expect(instance.address).to.equal('0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936') }).timeout(0) }) context('Unforked', () => { describe('class Classic', () => { if (!process.env.SYNC_TEST_INSTANCES) throw ErrorUtils.getError('SYNC_TEST_INSTANCES is required for sync tests.') const denominations = process.env.SYNC_TEST_INSTANCES.split(',') if (!denominations.length) throw ErrorUtils.getError('Instances entered were INVALID') let depositReferences: { [key: string]: typeof eth01DepositsReference } = {} depositReferences['1ETH0.1'] = eth01DepositsReference depositReferences['1ETH1'] = eth1DepositsReference depositReferences['1ETH10'] = eth10DepositsReference depositReferences['1ETH100'] = eth100DepositsReference depositReferences['1DAI100'] = dai100DepositsReference depositReferences['1DAI1000'] = dai1000DepositsReference depositReferences['1DAI10000'] = dai10000DepositsReference depositReferences['1DAI100000'] = dai100KDepositsReference const core = new Core() let instances: TornadoInstance[] = [] let logListener = function (...args: any[]) { if (args.length === 3) { console.debug(`\nSync will be started with SB: ${args[0]}, TB: ${args[1]}, BD: ${args[2]}\n`) } else if (args.length == 2) { console.debug(`Syncing from block ${args[0]} to ${args[1]}`) } } before(async function () { this.timeout(0) await core.connect(mainnetProvider) const regexp = /([0-9]+)([A-Za-z]+)([0-9.]+)/ const promises = denominations.map((denom) => { const matches = denom.match(regexp)!.slice(2) return core.getInstance(matches[0].toLowerCase(), +matches[1]) }) ;(await Promise.all(promises)).forEach((instance) => instances.push(instance)) if (debug) core.on('debug', logListener) }) after(async function () { this.timeout(0) if (debug) core.off('debug', logListener) }) it('Should sync all instances.', async function () { for (let i = 0; i < instances.length; i++) { console.log('\n ā™»ļø Syncing ' + denominations[i] + '\n') // This is going to try syncing the entire range await core.syncDeposits(instances[i], { concurrencyLimit: 8, blockDelta: 10000, msTimeout: 300 }) const cache = core.caches.get('Deposits' + denominations[i]) const rows = (await cache!.db.allDocs()).rows const valid = Object.values(depositReferences[denominations[i]]) expect(rows.length).to.be.gte(valid.length) console.log('\nšŸ“„ Validating inputs for ' + denominations[i] + '\n') for (let i = 0, len = valid.length; i < len; i++) { const id = rows[i].id const [bn, leafIndex, commitment] = parseIndexableString(id) const validDoc = valid[i] expect(bn).to.equal(validDoc['blockNumber']) expect(leafIndex).to.equal(validDoc['leafIndex']) expect(commitment).to.equal(validDoc['commitment']) } } }).timeout(0) }) }) describe('Forked (Ganache)', async () => { describe('class Classic', async () => { // Init sync objects const core = new Core() const needsMoney = ganacheProvider.getSigner() const withdrawer = ganacheProvider.getSigner(2) const daiWhaleSigner = ganacheProvider.getSigner(daiWhale) const debugListener = (message: string) => console.debug(message) let snapshotId: any let needsMoneyAddress: string let withdrawerAddress: string let dai: ERC20 let smallestEth: TornadoInstance let dai100K: TornadoInstance before(async function () { this.timeout(0) // We need to connect core first await core.connect(ganacheProvider) // Get snapshot just in case snapshotId = await ganacheProvider.send('evm_snapshot', []) // Prep whale eth balance await ganacheProvider.send('evm_setAccountBalance', [daiWhale, parseUnits('10').toHexString()]) // Addresses needsMoneyAddress = await needsMoney.getAddress() withdrawerAddress = await withdrawer.getAddress() daiAddress = await Onchain.getTokenAddress('1', 'dai') // Contracts dai = chain.getTokenContract(daiAddress) smallestEth = core.getInstance('eth', 0.1) dai100K = core.getInstance('dai', 100000) // Set debug if (debug) core.on('debug', debugListener) }) after(async function () { this.timeout(0) await ganacheProvider.send('evm_revert', [snapshotId]) core.off('debug', debugListener) }) beforeEach(() => { dai = dai.connect(daiWhaleSigner) }) it('createDepositTransaction: build a single eth deposit tx and succeed', async () => { const initBal = await needsMoney.getBalance() // Build tx and load cache for this test const tx = core.createDepositTransaction(smallestEth) // Listen to deposit events core.listenForDeposits(smallestEth) // Get the promise we need const promise = once(core, 'deposit') // Deposit and await cache updated const response = await needsMoney.sendTransaction(tx.request) await response.wait() const endBal = await needsMoney.getBalance() // Await deposit addition to cache await promise // Remove listeners core.clearListeners(smallestEth) // Backup await core.backupNote(smallestEth, tx) // Check deposit predicates expect(initBal).to.equal(parseUnits('1000')) expect(endBal).to.be.lte(parseUnits('999.9')) }).timeout(0) it('createDepositProof: it should be able to build an eth proof', async () => { // Get all of the notes const notes = await core.loadNotes() // Build proof let proof: any proof = await core.createDepositProof( smallestEth, { address: withdrawerAddress }, needsMoneyAddress, notes[0], { // On by default but stating for visibility checkNotesSpent: true, checkKnownRoot: true } ) // Substract the calculated fee from the received amount const ethDelta = parseUnits('0.1').sub(proof[5]) // Withdrawal time, let's see if it works // The balance diff will be exact because withdrawer is paying for gas as relayer await expect(() => smallestEth .connect(withdrawer) .withdraw(proof[0], proof[1], proof[2], proof[3], proof[4], proof[5], proof[6]) ).to.changeEtherBalance(needsMoney, ethDelta) }).timeout(0) it('createDepositTransaction: build a single token deposit tx and succeed', async () => { // Prep deposit amount, proxy for approval, cache, bal for comp const depositAmount = parseUnits('100000') const daiBalBef = await dai.balanceOf(dai100K.address) const proxy = core.getRouter() // We listen for deposits core.listenForDeposits(dai100K) // We will wait for the event const promise = once(core, 'deposit') // Prep for deposit await dai.transfer(needsMoneyAddress, depositAmount) dai = dai.connect(needsMoney) const tx = core.createDepositTransaction(dai100K) // Approve dai for the proxy first (transferFrom) await dai.approve(proxy.address, depositAmount) // Deposit const response = await needsMoney.sendTransaction(tx.request) await response.wait() // Prep for check const daiBalPost = await dai.balanceOf(dai100K.address) // Passing resolve as callback into put didn't work. await promise // Have to clear the listeners core.clearListeners(dai100K) // Backup since we need it for later await core.backupNote(dai100K, tx) // Checks expect(daiBalBef).to.equal(daiBalPost.sub(depositAmount)) expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0) }).timeout(0) it('createDepositProof: it should be able to build a token proof', async () => { if (!process.env.TEST_RELAYER_DOMAIN) throw ErrorUtils.getError('core.test.ts: Need a relayer name') // Get all of the notes const notes = await core.loadNotes() // We need to select last const note = notes[notes.length - 1] let properties: RelayerProperties = { address: withdrawerAddress, version: '2', serviceFeePercent: 0.04, miningFeePercent: 0.15, status: 'whatever', chainId: 1, prices: new Map() } properties.prices.set('dai', BigNumber.from(10).pow(18).div(1800)) // Build proof with relayer properties this time const proof = await core.createDepositProof(dai100K, properties, needsMoneyAddress, note, { // On by default but stating for visibility checkNotesSpent: true, checkKnownRoot: true }) // Calc balance diff again... it will be expressed in dai const daiDelta = parseUnits('100000').sub(proof[5]) await expect( await dai100K .connect(withdrawer) .withdraw(proof[0], proof[1], proof[2], proof[3], proof[4], proof[5], proof[6]) ).to.changeTokenBalance(dai, needsMoney, daiDelta) }).timeout(0) it('createDepositTransactions: multiple eth deposits', async () => { const instances = core.getInstances( [0.1, 1, 10, 100].map((el) => { return { token: 'eth', denomination: el } }) ) // That easy instances.forEach((instance) => core.listenForDeposits(instance)) const depositsPer = [1, 1, 2, 1] const txs = core.createDepositTransactions(instances, { depositsPerInstance: depositsPer }) for (let i = 0, len = txs.length; i < len; i++) { const promise = once(core, 'deposit') const response = await needsMoney.sendTransaction(txs[i].request) await response.wait() await promise } // That easy instances.forEach((instance) => core.clearListeners(instance)) // And backup the notes await Promise.all( instances.map((instance, index) => core.backupNotes(instance, txs.splice(0, depositsPer[index]))) ) //for (let i = 0, len = instances.length; i < len; i++) { // await core.backupNotes(instances[i], txs.splice(0, depositsPer[i])) //} expect(await needsMoney.getBalance()).to.be.lte(parseUnits('888.8')) }).timeout(0) it('createDepositProofs: should be able to withdraw', async () => { // ETH instances const instances = core.getInstances( [0.1, 1, 10, 100].map((el) => { return { token: 'eth', denomination: el } }) ) // Number deposits per instance const depositsPer = [1, 1, 2, 1] // Get all of the notes let notes = await core.loadNotes() // Handle all withdrawals for (let i = 0, len = instances.length; i < len; i++) { const proofs = await core.createDepositProofs( instances[i], { address: withdrawerAddress }, new Array(depositsPer[i]).fill(needsMoneyAddress), notes.splice(0, depositsPer[i]), { // On by default but stating for visibility checkNotesSpent: true, checkKnownRoot: true } ) for (let p = 0, plen = proofs.length; p < plen; p++) { // Get proof const proof = proofs[p] // Substract the calculated fee from the received amount const ethDelta = parseUnits('0.1') .mul(10 ** i) .sub(proof[5]) // Withdrawal time, let's see if it works // The balance diff will be exact because withdrawer is paying for gas as relayer await expect(() => instances[i] .connect(withdrawer) .withdraw(proof[0], proof[1], proof[2], proof[3], proof[4], proof[5], proof[6]) ).to.changeEtherBalance(needsMoney, ethDelta) } } }).timeout(0) it.only('createDepositTransactions: multiple dai deposits', async () => { // Prepare contracts const denoms = [100, 1000, 10000, 100000] const proxy = core.getRouter() const instances = core.getInstances( denoms.map((el) => { return { token: 'dai', denomination: el } }) ) // Prep the money const depositsPer = [1, 2, 1, 2] await dai.transfer(needsMoneyAddress, parseUnits('212100')) dai = dai.connect(needsMoney) await dai.approve(proxy.address, parseUnits('212100')) // Record the money const daiBalancesBef = await Promise.all(instances.map((instance) => dai.balanceOf(instance.address))) // Begin to listen instances.forEach((instance) => core.listenForDeposits(instance)) // Build txs const txs = core.createDepositTransactions(instances, { depositsPerInstance: depositsPer }) // Send transactions for (let i = 0, len = txs.length; i < len; i++) { const promise = once(core, 'deposit') const resp = await needsMoney.sendTransaction(txs[i].request) await resp.wait() await promise } // Clear listeners instances.forEach((instance) => core.clearListeners(instance)) // Backup notes await Promise.all( instances.map((instance, index) => core.backupNotes(instance, txs.splice(0, depositsPer[index]))) ) // Get new balances const daiBalancesPost = await Promise.all( instances.map((instance) => dai.balanceOf(instance.address)) ) // Check and done for (let i = 0; i < 4; i++) { expect(daiBalancesBef[i]).to.equal( daiBalancesPost[i].sub(parseUnits('' + denoms[i] * depositsPer[i])) ) } expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0) }).timeout(0) it.only('createDepositProofs: multiple dai withdrawals', async () => { // ETH instances const denoms = [100, 1000, 10000, 100000] const instances = core.getInstances( denoms.map((el) => { return { token: 'dai', denomination: el } }) ) // Number deposits per instance const depositsPer = [1, 2, 1, 2] // Get all of the notes let notes = await core.loadNotes() // Fake relayer properties let properties: RelayerProperties = { address: withdrawerAddress, version: '2', serviceFeePercent: 0.04, miningFeePercent: 0.15, status: 'whatever', chainId: 1, prices: new Map() } properties.prices.set('dai', BigNumber.from(10).pow(18).div(1800)) // Handle all withdrawals for (let i = 0, len = instances.length; i < len; i++) { const proofs = await core.createDepositProofs( instances[i], properties, new Array(depositsPer[i]).fill(needsMoneyAddress), notes.splice(0, depositsPer[i]), { // On by default but stating for visibility checkNotesSpent: true, checkKnownRoot: true } ) for (let p = 0, plen = proofs.length; p < plen; p++) { // Get proof const proof = proofs[p] // Substract the calculated fee from the received amount const daiDelta = parseUnits('100') .mul(10 ** i) .sub(proof[5]) // Withdrawal time, let's see if it works // The balance diff will be exact because withdrawer is paying for gas as relayer await expect(() => instances[i] .connect(withdrawer) .withdraw(proof[0], proof[1], proof[2], proof[3], proof[4], proof[5], proof[6]) ).to.changeTokenBalance(dai, needsMoney, daiDelta) } } }).timeout(0) }) }) })