import chai from 'chai' import * as ganache from 'ganache' // External 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 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) it('Should print cache path to console', async () => { console.log(await Files.getCachePath('anything')) }) 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 = await Contracts.getInstance(String(1), 'eth', String(1), mainnetProvider) expect(instance.address).to.equal('0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936') await expect(instance.getLastRoot()).to.not.be.reverted }).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') const depositReferences: { [key: string]: typeof eth01DepositsReference } = { '1ETH0.1': eth01DepositsReference, '1ETH1': eth1DepositsReference, '1ETH10': eth10DepositsReference, '1ETH100': eth100DepositsReference, '1DAI100000': dai100KDepositsReference } const core = new Core(mainnetProvider) 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) const regexp = /([0-9]+)([A-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() 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], { blockDivisor: 50, concurrencyLimit: 20, 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) 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(ganacheProvider) const needsMoney = ganacheProvider.getSigner() const daiWhaleSigner = ganacheProvider.getSigner(daiWhale) const debugListener = (message: string) => console.debug(message) let snapshotId: any let needsMoneyAddress: string let dai: ERC20 let smallestEth: TornadoInstance let dai100K: TornadoInstance before(async function () { this.timeout(0) // Get snapshot just in case snapshotId = await ganacheProvider.send('evm_snapshot', []) // Prep whale eth balance await ganacheProvider.send('evm_setAccountBalance', [daiWhale, parseUnits('10').toHexString()]) // Init async objects needsMoneyAddress = await needsMoney.getAddress() daiAddress = await Onchain.getTokenAddress('1', 'dai') dai = chain.getTokenContract(daiAddress).connect(daiWhaleSigner) smallestEth = await core.getInstance('eth', 0.1) dai100K = await 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) }) afterEach(() => { dai = dai.connect(daiWhaleSigner) }) it('buildDepositTransaction: build a single eth deposit tx and succeed', async () => { const initBal = await needsMoney.getBalance() // Build tx and load cache for this test const tx = await core.buildDepositTransaction(smallestEth) const cache = core.loadDepositCache('Deposits1ETH0.1') // Prep promise to only try withdrawing after cache has been updated const putPromise = new Promise((resolve) => { smallestEth.on( smallestEth.filters.Deposit(null, null, null), function (commitment, leafIndex, timestamp, event) { resolve(cache.db.put(cache.buildDoc(event))) } ) }) const listener = smallestEth.listeners(smallestEth.filters.Deposit(null, null, null))[0] // Deposit and await cache updated const response = await needsMoney.sendTransaction(tx.request) await response.wait() const endBal = await needsMoney.getBalance() // Passing resolve as callback into put didn't work await await putPromise // Turn off listener (NEEDED OR WE'RE NOT RESOLVING) smallestEth.off(smallestEth.filters.Deposit(null, null, null), listener) // Check deposit predicates expect(initBal).to.equal(parseUnits('1000')) expect(endBal).to.be.lte(parseUnits('999.9')) }).timeout(0) it('buildDepositProof: it should be able to build an eth proof', async () => { // Get withdrawer, load cache, prep note for this test const withdrawer = ganacheProvider.getSigner(2) const cache = core.loadDepositCache('Deposits1ETH0.1') // We need this to clean the cache, we want to have clean state const doc = (await cache.db.allDocs({ include_docs: true, descending: true, limit: 1 })).rows[0].doc // We are not transforming because we want to test this out const notes = await core.loadNotes() // Build proof let proof: any try { proof = await core.buildDepositProof( smallestEth, { address: await withdrawer.getAddress() }, await needsMoney.getAddress(), notes[0], { // On by default but stating for visibility checkNotesSpent: true, checkKnownRoot: true } ) } finally { await cache.db.remove(doc?._id!, doc?._rev!) } // 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('buildDepositTransaction: build a single token deposit tx and succeed', async () => { // Prep deposit amount, proxy for approval, cache, bal for comp const depositAmount = parseUnits('100000') const proxy = await core.getProxy() const cache = core.loadDepositCache('Deposits1DAI100000') const daiBalBef = await dai.balanceOf(dai100K.address) // Prep promise to only try withdrawing after cache has been updated const putPromise = new Promise((resolve) => { dai100K.on( dai100K.filters.Deposit(null, null, null), function (commitment, leafIndex, timestamp, event) { resolve(cache.db.put(cache.buildDoc(event))) } ) }) const listener = dai100K.listeners()[0] // Prep for deposit await dai.transfer(needsMoneyAddress, depositAmount) dai = dai.connect(needsMoney) const tx = await core.buildDepositTransaction(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 await putPromise // Off (otherwise no resolve) dai100K.off(dai100K.filters.Deposit(null, null, null), listener) // Checks expect(daiBalBef).to.equal(daiBalPost.sub(depositAmount)) expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0) }).timeout(0) it('buildDepositProof: 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 withdrawer, load cache, prep note for this test const withdrawer = ganacheProvider.getSigner(2) const cache = core.loadDepositCache('Deposits1DAI100000') // We need this to clean the cache, we want to have clean state const doc = (await cache.db.allDocs({ include_docs: true, descending: true, limit: 1 })).rows[0].doc // We are not transforming because we want to test this out const notes = await core.loadNotes() // We need to select last const note = notes[notes.length - 1] let properties: RelayerProperties = { address: await withdrawer.getAddress(), 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)) // Just set another address properties.address = await withdrawer.getAddress() // Build proof with relayer properties this time let proof try { proof = await core.buildDepositProof(dai100K, properties, await needsMoney.getAddress(), note, { // On by default but stating for visibility checkNotesSpent: true, checkKnownRoot: true }) } finally { await cache.db.remove(doc?._id!, doc?._rev!) } // 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.only('buildDepositTransactions: multiple eth deposits', async () => { const instances = await core.getInstances( [0.1, 1, 10, 100].map((el) => { return { token: 'eth', denomination: el } }) ) const txs = await core.buildDepositTransactions(instances, { depositsPerInstance: [1, 1, 2, 1] }) for (let i = 0, len = txs.length; i < len; i++) { console.log('SENDING => ', i) const response = await needsMoney.sendTransaction(txs[i].request) console.log('TX SENT => ', i) await response.wait() console.log('WAITING => ', i) } expect(await needsMoney.getBalance()).to.be.lte(parseUnits('888.8')) }).timeout(0) it('buildDepositTransactions: multiple token deposits', async () => { const instances = await core.getInstances( [100, 1000, 10000, 100000].map((el) => { return { token: 'dai', denomination: el } }) ) const proxy = await core.getProxy() const depositAmount = parseUnits('432100') await dai.transfer(needsMoneyAddress, parseUnits('432100')) dai = dai.connect(needsMoney) const txs = await core.buildDepositTransactions(instances, { depositsPerInstance: [1, 2, 3, 4] }) await dai.approve(proxy.address, depositAmount) for (let i = 0, len = txs.length; i < len; i++) { await expect(() => needsMoney.sendTransaction(txs[i].request)).to.not.be.reverted } expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0) }).timeout(0) it('createInvoice: should be able to create an invoice', async () => { const instance = await core.getInstance('dai', '1000') const invoice = await core.createInvoice(instance) console.log(invoice) }).timeout(0) }) }) })