407 lines
14 KiB
TypeScript
407 lines
14 KiB
TypeScript
|
import chai from 'chai'
|
||
|
import * as ganache from 'ganache'
|
||
|
|
||
|
// External
|
||
|
import { solidity } from 'ethereum-waffle'
|
||
|
import { providers } from 'ethers'
|
||
|
import { parseUnits } from 'ethers/lib/utils'
|
||
|
// @ts-expect-error
|
||
|
import { parseIndexableString } from 'pouchdb-collate'
|
||
|
|
||
|
// Local
|
||
|
import { RelayerProperties } from 'types/sdk/data'
|
||
|
import { ERC20, TornadoInstance } from 'types/deth'
|
||
|
import { Docs, Files, Onchain, Cache } from 'lib/data'
|
||
|
import { Chain, Contracts } from 'lib/chain'
|
||
|
import { Primitives } from 'lib/crypto'
|
||
|
import { ErrorUtils } from 'lib/utils'
|
||
|
import { TorProvider, Relayer, RegularHttpClient } from 'lib/web'
|
||
|
import { Core } from 'lib/core'
|
||
|
|
||
|
// Data
|
||
|
import compareDeposits from './resources/deposits_eth_0.1.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)
|
||
|
|
||
|
async function initializeRelayer(): Promise<Relayer> {
|
||
|
const httpClient = new RegularHttpClient()
|
||
|
|
||
|
const relayer = new Relayer({
|
||
|
url: 'https://thornadope.xyz',
|
||
|
httpClient: httpClient
|
||
|
})
|
||
|
|
||
|
await relayer.fetchProperties()
|
||
|
|
||
|
return relayer
|
||
|
}
|
||
|
|
||
|
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', () => {
|
||
|
const core = new Core(mainnetProvider)
|
||
|
|
||
|
let smallestEth: TornadoInstance
|
||
|
let dai100K: 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)
|
||
|
smallestEth = await core.getInstance('eth', 0.1)
|
||
|
dai100K = await core.getInstance('dai', 100000)
|
||
|
if (debug) core.on('debug', logListener)
|
||
|
})
|
||
|
after(async function () {
|
||
|
this.timeout()
|
||
|
if (debug) core.off('debug', logListener)
|
||
|
})
|
||
|
|
||
|
const smallEthDenomName = '1ETH0.1'
|
||
|
|
||
|
it(`sync: Should be able to fetch deposit events for ${smallEthDenomName}`, async function () {
|
||
|
// This is going to try syncing the entire range
|
||
|
await core.sync(smallestEth, {
|
||
|
blocks: {
|
||
|
deltaDivisor: 50
|
||
|
},
|
||
|
cache: {
|
||
|
sync: {
|
||
|
concurrencyLimit: 20
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
const cache = core.caches.get('Deposits' + smallEthDenomName)
|
||
|
const rows = (await cache!.db.allDocs()).rows
|
||
|
const valid = Object.values(compareDeposits)
|
||
|
|
||
|
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)
|
||
|
|
||
|
const bigDaiDenomName = '1DAI100000'
|
||
|
|
||
|
it(`sync: Should be able to fetch deposit events for ${bigDaiDenomName}`, async function () {
|
||
|
// This is going to try syncing the entire range
|
||
|
await core.sync(dai100K, {
|
||
|
blocks: {
|
||
|
deltaDivisor: 50
|
||
|
},
|
||
|
cache: {
|
||
|
sync: {
|
||
|
concurrencyLimit: 20
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}).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.only('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)))
|
||
|
}
|
||
|
)
|
||
|
})
|
||
|
|
||
|
// 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
|
||
|
|
||
|
// Check deposit predicates
|
||
|
expect(initBal).to.equal(parseUnits('1000'))
|
||
|
expect(endBal).to.be.lte(parseUnits('999.9'))
|
||
|
}).timeout(0)
|
||
|
|
||
|
it.only('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
|
||
|
|
||
|
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(
|
||
|
await 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.only('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)))
|
||
|
}
|
||
|
)
|
||
|
})
|
||
|
|
||
|
// 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
|
||
|
|
||
|
// Checks
|
||
|
expect(daiBalBef).to.equal(daiBalPost.sub(depositAmount))
|
||
|
expect(await dai.balanceOf(needsMoneyAddress)).to.equal(0)
|
||
|
}).timeout(0)
|
||
|
|
||
|
it.only('buildDepositProof: it should be able to build a token proof', async () => {
|
||
|
// 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]
|
||
|
|
||
|
// Init properties via some relayer to make our life easier
|
||
|
const relayer = await initializeRelayer()
|
||
|
let properties = relayer.properties
|
||
|
|
||
|
// 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 smallestEth
|
||
|
.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('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, 2, 3, 4]
|
||
|
})
|
||
|
|
||
|
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('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)
|
||
|
})
|
||
|
})
|
||
|
})
|