Simplify fee calculation & Add network from networkConfig #35

Open
tornadocontrib wants to merge 6 commits from tornadocontrib/classic-ui:light-4 into development
12 changed files with 383 additions and 183 deletions

@ -43,6 +43,8 @@ export const ACTION_GAS = Object.freeze({
[ACTION.ARB_WITHDRAW]: 1900000 [ACTION.ARB_WITHDRAW]: 1900000
}) })
export const WITHDRAW_GAS_LIMIT = 600_000
export const GAS_PRICES = ['low', 'standard', 'fast'] export const GAS_PRICES = ['low', 'standard', 'fast']
export const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' } export const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' }

@ -42,6 +42,7 @@ export default {
routerContract: '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b', routerContract: '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
registryContract: '0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2', registryContract: '0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2',
echoContractAccount: '0x9B27DD5Bb15d42DC224FCD0B7caEbBe16161Df42', echoContractAccount: '0x9B27DD5Bb15d42DC224FCD0B7caEbBe16161Df42',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
aggregatorContract: '0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49', aggregatorContract: '0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49',
tokens: { tokens: {
eth: { eth: {
@ -62,9 +63,10 @@ export default {
'100000': '0x23773E65ed146A459791799d01336DB287f25334' '100000': '0x23773E65ed146A459791799d01336DB287f25334'
}, },
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
tokenGasLimit: 70_000,
symbol: 'DAI', symbol: 'DAI',
decimals: 18, decimals: 18,
gasLimit: '55000' gasLimit: 700_000
}, },
cdai: { cdai: {
instanceAddress: { instanceAddress: {
@ -74,9 +76,10 @@ export default {
'5000000': '0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af' '5000000': '0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af'
}, },
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643', tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
tokenGasLimit: 200_000,
symbol: 'cDAI', symbol: 'cDAI',
decimals: 8, decimals: 8,
gasLimit: '425000' gasLimit: 700_000
}, },
/** /**
* Instances frozen due to sanctions * Instances frozen due to sanctions
@ -86,9 +89,10 @@ export default {
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D' '1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D'
}, },
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
tokenGasLimit: 70_000,
symbol: 'USDC', symbol: 'USDC',
decimals: 6, decimals: 6,
gasLimit: '80000' gasLimit: 700_000,
}, },
usdt: { usdt: {
instanceAddress: { instanceAddress: {
@ -96,9 +100,10 @@ export default {
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f' '1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f'
}, },
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7', tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
tokenGasLimit: 70_000,
symbol: 'USDT', symbol: 'USDT',
decimals: 6, decimals: 6,
gasLimit: '100000' gasLimit: 700_000,
}, },
**/ **/
wbtc: { wbtc: {
@ -108,9 +113,10 @@ export default {
'10': '0xbB93e510BbCD0B7beb5A853875f9eC60275CF498' '10': '0xbB93e510BbCD0B7beb5A853875f9eC60275CF498'
}, },
tokenAddress: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', tokenAddress: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
tokenGasLimit: 70_000,
symbol: 'WBTC', symbol: 'WBTC',
decimals: 8, decimals: 8,
gasLimit: '85000' gasLimit: 700_000
} }
}, },
ensSubdomainKey: 'mainnet-tornado', ensSubdomainKey: 'mainnet-tornado',
@ -147,6 +153,7 @@ export default {
deployedBlock: 8158799, deployedBlock: 8158799,
multicall: '0x41263cba59eb80dc200f3e2544eda4ed6a90e76c', multicall: '0x41263cba59eb80dc200f3e2544eda4ed6a90e76c',
echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4', echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
rpcUrls: { rpcUrls: {
tornadoRPC: { tornadoRPC: {
name: 'Tornado RPC', name: 'Tornado RPC',
@ -202,6 +209,9 @@ export default {
deployedBlock: 16257962, deployedBlock: 16257962,
multicall: '0x11ce4B23bD875D7F5C6a31084f55fDe1e9A87507', multicall: '0x11ce4B23bD875D7F5C6a31084f55fDe1e9A87507',
echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4', echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
gasPriceOracleContract: '0xF81A8D8D3581985D3969fe53bFA67074aDFa8F3C',
gasStationApi: 'https://polygon-oracle.tornadocash-rpc.com',
rpcUrls: { rpcUrls: {
chainnodes: { chainnodes: {
name: 'Chainnodes RPC', name: 'Chainnodes RPC',
@ -253,7 +263,8 @@ export default {
deployedBlock: 2243689, deployedBlock: 2243689,
multicall: '0x35A6Cdb2C9AD4a45112df4a04147EB07dFA01aB7', multicall: '0x35A6Cdb2C9AD4a45112df4a04147EB07dFA01aB7',
echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4', echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
ovmGasPriceOracleContract: '0x420000000000000000000000000000000000000F', offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
optimismL1FeeOracleAddress: '0x420000000000000000000000000000000000000F',
rpcUrls: { rpcUrls: {
tornadoRPC: { tornadoRPC: {
name: 'Tornado RPC', name: 'Tornado RPC',
@ -309,6 +320,7 @@ export default {
deployedBlock: 3430648, deployedBlock: 3430648,
multicall: '0x842eC2c7D803033Edf55E478F461FC547Bc54EB2', multicall: '0x842eC2c7D803033Edf55E478F461FC547Bc54EB2',
echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4', echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
rpcUrls: { rpcUrls: {
tornadoRPC: { tornadoRPC: {
name: 'Tornado RPC', name: 'Tornado RPC',
@ -368,6 +380,7 @@ export default {
deployedBlock: 17754561, deployedBlock: 17754561,
multicall: '0xb5b692a88bdfc81ca69dcb1d924f59f0413a602a', multicall: '0xb5b692a88bdfc81ca69dcb1d924f59f0413a602a',
echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4', echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
rpcUrls: { rpcUrls: {
tornadoRPC: { tornadoRPC: {
name: 'Tornado RPC', name: 'Tornado RPC',
@ -423,6 +436,7 @@ export default {
deployedBlock: 4429818, deployedBlock: 4429818,
multicall: '0xe86e3989c74293Acc962156cd3F525c07b6a1B6e', multicall: '0xe86e3989c74293Acc962156cd3F525c07b6a1B6e',
echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4', echoContractAccount: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
offchainOracleContract: '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8',
rpcUrls: { rpcUrls: {
publicRpc: { publicRpc: {
name: 'Avalanche RPC', name: 'Avalanche RPC',
@ -511,9 +525,10 @@ export default {
'100000': '0x73B4BD04bF83206B6e979BE2507098F92EDf4F90' '100000': '0x73B4BD04bF83206B6e979BE2507098F92EDf4F90'
}, },
tokenAddress: '0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357', tokenAddress: '0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357',
tokenGasLimit: 70_000,
symbol: 'DAI', symbol: 'DAI',
decimals: 18, decimals: 18,
gasLimit: '55000' gasLimit: 700_000
} }
}, },
ensSubdomainKey: 'sepolia-tornado', ensSubdomainKey: 'sepolia-tornado',

@ -178,6 +178,24 @@ export default {
if (ctx.isClient) { if (ctx.isClient) {
config.devtool = hasSourceMaps config.devtool = hasSourceMaps
} }
config.module.rules.push({
test: /\.(js|cjs|mjs|jsx)$/,
include: /node_modules/,
type: 'javascript/auto',
use: {
loader: 'babel-loader',
options: {
compact: false,
presets: [['@babel/preset-env', { targets: 'defaults' }]],
plugins: [
'@babel/plugin-transform-private-methods',
'@babel/plugin-transform-class-properties',
'@babel/plugin-transform-optional-chaining',
'@babel/plugin-transform-nullish-coalescing-operator'
]
}
}
})
config.module.rules.push({ config.module.rules.push({
test: /\.bin$/, test: /\.bin$/,
use: 'arraybuffer-loader' use: 'arraybuffer-loader'

@ -30,7 +30,7 @@
"@nuxtjs/moment": "^1.6.0", "@nuxtjs/moment": "^1.6.0",
"@tornado/fixed-merkle-tree": "0.7", "@tornado/fixed-merkle-tree": "0.7",
"@tornado/snarkjs": "0.1.20", "@tornado/snarkjs": "0.1.20",
"@tornado/tornado-oracles": "^2.1.0", "@tornado/tornado-oracles": "git+https://git.tornado.ws/tornadocontrib/tornado-oracles.git#0e9fe6970d54995f00cf3dbc1cfc73d9f7365a62",
"@tornado/websnark": "0.0.4", "@tornado/websnark": "0.0.4",
"@walletconnect/web3-provider": "1.7.8", "@walletconnect/web3-provider": "1.7.8",
"ajv": "^6.10.2", "ajv": "^6.10.2",
@ -44,6 +44,7 @@
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"eth-ens-namehash": "^2.0.8", "eth-ens-namehash": "^2.0.8",
"eth-sig-util": "^2.5.3", "eth-sig-util": "^2.5.3",
"ethers": "^6.12.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"form-data": "^3.0.0", "form-data": "^3.0.0",
"graphql": "^15.5.1", "graphql": "^15.5.1",
@ -64,6 +65,7 @@
"web3": "1.5.2" "web3": "1.5.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "7.24.4",
"@nuxtjs/eslint-config": "^1.1.2", "@nuxtjs/eslint-config": "^1.1.2",
"@nuxtjs/eslint-module": "^1.1.0", "@nuxtjs/eslint-module": "^1.1.0",
"@vue/test-utils": "^1.0.0-beta.27", "@vue/test-utils": "^1.0.0-beta.27",

@ -1,12 +1,13 @@
// from https://github.com/ChainSafe/web3.js/issues/2683#issuecomment-547348416 // from https://github.com/ChainSafe/web3.js/issues/2683#issuecomment-547348416
import namehash from 'eth-ens-namehash' import namehash from 'eth-ens-namehash'
import { BigNumber, utils } from 'ethers'
import ABI from 'web3-eth-ens/lib/resources/ABI/Resolver' import ABI from 'web3-eth-ens/lib/resources/ABI/Resolver'
import uniq from 'lodash/uniq' import uniq from 'lodash/uniq'
import chunk from 'lodash/chunk' import chunk from 'lodash/chunk'
import { CHUNK_COUNT_PER_BATCH_REQUEST } from '@/constants' import { CHUNK_COUNT_PER_BATCH_REQUEST } from '@/constants'
const { isAddress } = require('web3-utils')
export const createBatchRequestCallback = (resolve, reject) => (error, data) => { export const createBatchRequestCallback = (resolve, reject) => (error, data) => {
if (error) { if (error) {
reject(error) reject(error)
@ -65,8 +66,7 @@ const createFetchEnsNames = (web3, batch, results) => async (data) => {
batch.add(requestData) batch.add(requestData)
}) })
const isZeroAddress = const isZeroAddress = ensName.trim().length && isAddress(ensName) && BigInt(ensName) === BigInt(0)
ensName.trim().length && utils.isAddress(ensName) && BigNumber.from(ensName).isZero()
if (isZeroAddress) return results if (isZeroAddress) return results

@ -270,12 +270,22 @@ class RelayerRegister {
} }
getValidRelayers = async (relayers, ensSubdomainKey) => { getValidRelayers = async (relayers, ensSubdomainKey) => {
const relayerNameHashes = relayers.map((r) => namehash.hash(r.ensName)) const relayersSet = new Set()
const uniqueRelayers = relayers.filter(({ ensName }) => {
if (!relayersSet.has(ensName)) {
relayersSet.add(ensName)
return true
}
return false
})
const relayerNameHashes = uniqueRelayers.map((r) => namehash.hash(r.ensName))
const relayersData = await this.aggregator.methods.relayersData(relayerNameHashes, subdomains).call() const relayersData = await this.aggregator.methods.relayersData(relayerNameHashes, subdomains).call()
const validRelayers = relayersData.reduce( const validRelayers = relayersData.reduce(
(acc, curr, index) => this.filterRelayer(acc, curr, ensSubdomainKey, relayers[index]), (acc, curr, index) => this.filterRelayer(acc, curr, ensSubdomainKey, uniqueRelayers[index]),
[] []
) )

@ -1,6 +1,7 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
/* eslint-disable no-console, import/order */ /* eslint-disable no-console, import/order */
import Web3 from 'web3' import Web3 from 'web3'
import { ZeroAddress } from 'ethers'
import networkConfig from '@/networkConfig' import networkConfig from '@/networkConfig'
import { cachedEventsLength, eventsType, httpConfig } from '@/constants' import { cachedEventsLength, eventsType, httpConfig } from '@/constants'
@ -106,26 +107,29 @@ const mutations = {
} }
const getters = { const getters = {
getWeb3: (state, getters, rootState, rootGetters) => {
const netId = rootGetters['metamask/netId']
const { url } = rootState.settings[`netId${netId}`].rpc
const httpProvider = new Web3.providers.HttpProvider(url, httpConfig)
return new Web3(httpProvider)
},
eventsInterface: (state, getters, rootState, rootGetters) => { eventsInterface: (state, getters, rootState, rootGetters) => {
const netId = rootGetters['metamask/netId'] const netId = rootGetters['metamask/netId']
const { url } = rootState.settings[`netId${netId}`].rpc const { url } = rootState.settings[`netId${netId}`].rpc
return new EventsFactory(url) return new EventsFactory(url)
}, },
instanceContract: (state, getters, rootState) => ({ currency, amount, netId }) => { instanceContract: (state, getters) => ({ currency, amount, netId }) => {
const config = networkConfig[`netId${netId}`] const config = networkConfig[`netId${netId}`]
const { url } = rootState.settings[`netId${netId}`].rpc
const address = config.tokens[currency].instanceAddress[amount] const address = config.tokens[currency].instanceAddress[amount]
const httpProvider = new Web3.providers.HttpProvider(url, httpConfig) const web3 = getters.getWeb3
const web3 = new Web3(httpProvider)
return new web3.eth.Contract(InstanceABI, address) return new web3.eth.Contract(InstanceABI, address)
}, },
multicallContract: (state, getters, rootState) => ({ netId }) => { multicallContract: (state, getters) => ({ netId }) => {
const config = networkConfig[`netId${netId}`] const config = networkConfig[`netId${netId}`]
const { url } = rootState.settings[`netId${netId}`].rpc const web3 = getters.getWeb3
const web3 = new Web3(url)
return new web3.eth.Contract(MulticallABI, config.multicall) return new web3.eth.Contract(MulticallABI, config.multicall)
}, },
tornadoProxyContract: (state, getters, rootState) => ({ netId }) => { tornadoProxyContract: (state, getters) => ({ netId }) => {
const { const {
'tornado-proxy.contract.tornadocash.eth': tornadoProxy, 'tornado-proxy.contract.tornadocash.eth': tornadoProxy,
'tornado-router.contract.tornadocash.eth': tornadoRouter, 'tornado-router.contract.tornadocash.eth': tornadoRouter,
@ -133,8 +137,7 @@ const getters = {
} = networkConfig[`netId${netId}`] } = networkConfig[`netId${netId}`]
const proxyContract = tornadoRouter || tornadoProxy || tornadoProxyLight const proxyContract = tornadoRouter || tornadoProxy || tornadoProxyLight
const { url } = rootState.settings[`netId${netId}`].rpc const web3 = getters.getWeb3
const web3 = new Web3(url)
return new web3.eth.Contract(TornadoProxyABI, proxyContract) return new web3.eth.Contract(TornadoProxyABI, proxyContract)
}, },
currentContract: (state, getters) => (params) => { currentContract: (state, getters) => (params) => {
@ -150,11 +153,16 @@ const getters = {
const tornadoProxy = getters.tornadoProxyContract({ netId }) const tornadoProxy = getters.tornadoProxyContract({ netId })
const tornadoInstance = getters.instanceContract({ currency, amount, netId }) const tornadoInstance = getters.instanceContract({ currency, amount, netId })
const { withdrawType } = state
const relayer = rootState.relayer.selectedRelayer.address
const { ethAccount } = rootState.metamask
const calldata = tornadoProxy.methods const calldata = tornadoProxy.methods
.withdraw(tornadoInstance._address, proof, ...withdrawCallArgs) .withdraw(tornadoInstance._address, proof, ...withdrawCallArgs)
.encodeABI() .encodeABI()
return { return {
from: withdrawType === 'relayer' ? relayer : ethAccount,
to: tornadoProxy._address, to: tornadoProxy._address,
data: calldata, data: calldata,
value: withdrawCallArgs[5] || 0 value: withdrawCallArgs[5] || 0
@ -510,6 +518,7 @@ const actions = {
}, },
async sendDeposit({ state, rootState, getters, rootGetters, dispatch, commit }, { isEncrypted }) { async sendDeposit({ state, rootState, getters, rootGetters, dispatch, commit }, { isEncrypted }) {
try { try {
const web3 = getters.getWeb3
const { commitment, note, prefix } = state const { commitment, note, prefix } = state
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let [, currency, amount, netId] = prefix.split('-') let [, currency, amount, netId] = prefix.split('-')
@ -548,7 +557,7 @@ const actions = {
value: numberToHex(value), value: numberToHex(value),
data data
} }
const gasLimit = await rootGetters['fees/oracle'].getGasLimit(incompletedTx, 'other', 10) const gasLimit = Math.floor((await web3.eth.estimateGas(incompletedTx)) * 1.2)
const callParams = { const callParams = {
method: 'eth_sendTransaction', method: 'eth_sendTransaction',
@ -665,36 +674,69 @@ const actions = {
{ rootGetters, rootState, state, getters, dispatch }, { rootGetters, rootState, state, getters, dispatch },
{ root, note, tree, recipient, leafIndex } { root, note, tree, recipient, leafIndex }
) { ) {
const { circuit, provingKey } = await getTornadoKeys()
if (!groth16) {
groth16 = await buildGroth16()
}
const web3 = getters.getWeb3
const { nullifierHash, secret, nullifier, amount, currency } = note
const { denomination, isNativeCurrency } = rootGetters['fees/selectedInstance']
const withdrawType = state.withdrawType
const { pathElements, pathIndices } = tree.path(leafIndex) const { pathElements, pathIndices } = tree.path(leafIndex)
console.log('pathElements, pathIndices', pathElements, pathIndices) console.log('pathElements, pathIndices', pathElements, pathIndices)
const nativeCurrency = rootGetters['metamask/nativeCurrency'] const defaultGasLimit = rootGetters['fees/gasLimit']()
const withdrawType = state.withdrawType let gasLimit = defaultGasLimit
let relayer = BigInt(0) async function getProof() {
let fee = BigInt(0) let relayer = ZeroAddress
let refund = BigInt(0) let fee = BigInt(0)
let refund = BigInt(0)
if (withdrawType === 'relayer') {
// Recalculate proof with actual fee and refund
await dispatch(
'fees/calculateWithdrawalFeeViaRelayer',
{
tx: {
gasLimit
}
},
{ root: true }
)
relayer = rootState.relayer.selectedRelayer.address
fee = BigInt(rootState.fees.withdrawalFeeViaRelayer)
if (fee > denomination) {
throw new Error(
'Relayer fee exceeds the withdraw amount, disable relayer withdrawal or use other amount'
)
}
}
if (!isNativeCurrency) {
refund = rootGetters['fees/ethRefund']
}
async function calculateSnarkProof() {
const input = { const input = {
// public // public
fee, fee,
root, root,
refund, refund,
relayer, relayer: BigInt(relayer),
recipient: BigInt(recipient), recipient: BigInt(recipient),
nullifierHash: note.nullifierHash, nullifierHash,
// private // private
pathIndices, pathIndices,
pathElements, pathElements,
secret: note.secret, secret,
nullifier: note.nullifier nullifier
}
const { circuit, provingKey } = await getTornadoKeys()
if (!groth16) {
groth16 = await buildGroth16()
} }
console.log('Start generating SNARK proof', input) console.log('Start generating SNARK proof', input)
@ -711,31 +753,41 @@ const actions = {
toFixedHex(input.refund) toFixedHex(input.refund)
] ]
console.timeEnd('SNARK proof time') console.timeEnd('SNARK proof time')
return { args, proof }
return {
relayer,
fee,
proof,
args
}
} }
// Don't need to calculate or estimate relayer fee, so, return proof immediately // eslint-disable-next-line prefer-const
if (withdrawType !== 'relayer') return calculateSnarkProof() let { proof, args } = await getProof()
relayer = BigInt(rootState.relayer.selectedRelayer.address)
fee = BigInt(rootState.fees.withdrawalFeeViaRelayer)
const naiveProof = await calculateSnarkProof()
if (Number(note.netId) === 1) return naiveProof // Don't need to smart-estimate fee if we use V4 withdrawal
const { proof: dummyProof, args: dummyArgs } = naiveProof
const withdrawalTx = getters.relayerWithdrawalTxData({ const withdrawalTx = getters.relayerWithdrawalTxData({
proof: dummyProof, proof,
withdrawCallArgs: dummyArgs, withdrawCallArgs: args,
amount: note.amount, amount,
currency: note.currency currency
}) })
if (note.currency !== nativeCurrency) refund = BigInt(state.ethToReceive.toString())
await dispatch('fees/calculateWithdrawalFeeViaRelayer', { tx: withdrawalTx }, { root: true }) gasLimit = BigInt(await web3.eth.estimateGas(withdrawalTx))
fee = BigInt(rootState.fees.withdrawalFeeViaRelayer)
// Recalculate proof with actual fee and refund // If required gasLimit is above default, calculate the snark proof again
return calculateSnarkProof() if (gasLimit > defaultGasLimit) {
;({ proof, args } = await getProof())
const withdrawalTx = getters.relayerWithdrawalTxData({
proof,
withdrawCallArgs: args,
amount,
currency
})
gasLimit = BigInt(await web3.eth.estimateGas(withdrawalTx))
}
return { proof, args }
}, },
async prepareWithdraw({ dispatch, commit }, { note, recipient }) { async prepareWithdraw({ dispatch, commit }, { note, recipient }) {
commit('REMOVE_PROOF', { note }) commit('REMOVE_PROOF', { note })
@ -765,6 +817,7 @@ const actions = {
}, },
async withdraw({ state, rootState, rootGetters, dispatch, getters }, { note }) { async withdraw({ state, rootState, rootGetters, dispatch, getters }, { note }) {
try { try {
const web3 = getters.getWeb3
const [, currency, amount, netId] = note.split('-') const [, currency, amount, netId] = note.split('-')
const config = networkConfig[`netId${netId}`] const config = networkConfig[`netId${netId}`]
const { proof, args } = state.notes[note] const { proof, args } = state.notes[note]
@ -782,7 +835,7 @@ const actions = {
to: contractInstance._address, to: contractInstance._address,
from: ethAccount from: ethAccount
} }
const gasLimit = await rootGetters['fees/oracle'].getGasLimit(incompletedTx, 'other', 20) const gasLimit = Math.floor((await web3.eth.estimateGas(incompletedTx)) * 1.1)
const callParams = { const callParams = {
method: 'eth_sendTransaction', method: 'eth_sendTransaction',
@ -923,8 +976,8 @@ const actions = {
console.error(`Method loadWithdrawalData has error: ${e}`) console.error(`Method loadWithdrawalData has error: ${e}`)
} }
}, },
async setDefaultEthToReceive({ commit, rootGetters }, { currency }) { setDefaultEthToReceive({ commit, rootGetters }, { gasPrice, refundGasLimit }) {
const ethToReceive = await rootGetters['fees/oracle'].calculateRefundInETH(currency.toLowerCase()) const ethToReceive = rootGetters['fees/oracle'].defaultEthRefund(gasPrice, refundGasLimit).toString()
commit('SAVE_ETH_TO_RECEIVE', { ethToReceive }) commit('SAVE_ETH_TO_RECEIVE', { ethToReceive })
commit('SAVE_DEFAULT_ETH_TO_RECEIVE', { ethToReceive }) commit('SAVE_DEFAULT_ETH_TO_RECEIVE', { ethToReceive })
}, },

@ -1,35 +1,122 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { toWei, fromWei, toBN } from 'web3-utils' import { toWei, toBN } from 'web3-utils'
import { TornadoFeeOracleV4, TornadoFeeOracleV5 } from '@tornado/tornado-oracles' import { formatUnits, parseUnits } from 'ethers'
import { ChainId, TornadoFeeOracle, getProvider } from '@tornado/tornado-oracles'
import { WITHDRAW_GAS_LIMIT } from '@/constants/variables'
export const state = () => { export const state = () => {
return { return {
gasPriceParams: { gasPrice: toWei(toBN(50), 'gwei') }, gasPriceParams: { gasPrice: toWei(toBN(50), 'gwei') },
l1Fee: toBN(0),
withdrawalNetworkFee: toBN(0), withdrawalNetworkFee: toBN(0),
withdrawalFeeViaRelayer: toBN(0) withdrawalFeeViaRelayer: toBN(0)
} }
} }
export const getters = { export const getters = {
provider: (state, getters, rootState, rootGetters) => {
const netId = Number(rootGetters['metamask/netId'])
const { url: rpcUrl } = rootState.settings[`netId${netId}`].rpc
const config = rootGetters['metamask/networkConfig']
return getProvider(netId, rpcUrl, config)
},
oracle: (state, getters, rootState, rootGetters) => { oracle: (state, getters, rootState, rootGetters) => {
const netId = Number(rootGetters['metamask/netId']) const netId = Number(rootGetters['metamask/netId'])
const { url: rpcUrl } = rootState.settings[`netId${netId}`].rpc const { url: rpcUrl } = rootState.settings[`netId${netId}`].rpc
const { gasPrices } = rootGetters['metamask/networkConfig'] const config = rootGetters['metamask/networkConfig']
// Return old oracle for backwards compatibility, if chain is ETH Mainnet return new TornadoFeeOracle(netId, rpcUrl, config)
return netId === 1
? new TornadoFeeOracleV4(netId, rpcUrl, gasPrices)
: new TornadoFeeOracleV5(netId, rpcUrl, gasPrices)
}, },
getGasPriceParams: (state) => { getGasPriceParams: (state) => {
return state.gasPriceParams return state.gasPriceParams
}, },
gasPrice: (state, getters) => { getMetamaskGasPriceParams: (state) => {
const { gasPrice, maxFeePerGas } = getters.getGasPriceParams const feeData = state.gasPriceParams
return maxFeePerGas || gasPrice
const gasParams = feeData.maxFeePerGas
? {
maxFeePerGas: '0x' + feeData.maxFeePerGas.toString(16),
maxPriorityFeePerGas: '0x' + feeData.maxPriorityFeePerGas.toString(16)
}
: {
gasPrice: '0x' + feeData.gasPrice.toString(16)
}
return gasParams
},
l1Fee: (state) => {
return state.l1Fee
},
selectedInstance: (state, getters, rootState, rootGetters) => {
const nativeCurrency = rootGetters['metamask/nativeCurrency']
const { tokens } = rootGetters['metamask/networkConfig']
const { currency, amount } = rootState.application.selectedStatistic
const {
instanceAddress: instanceAddresses,
decimals,
gasLimit: instanceGasLimit,
tokenGasLimit
} = tokens[currency]
const { [amount]: instanceAddress } = instanceAddresses
const isNativeCurrency = currency.toLowerCase() === nativeCurrency
const firstAmount = Object.keys(instanceAddresses).sort((a, b) => Number(a) - Number(b))[0]
const isFirstAmount = Number(amount) === Number(firstAmount)
const denomination = parseUnits(String(amount), decimals)
const tokenPriceInWei = rootState.price.prices[currency.toLowerCase()]
return {
instanceAddress,
currency: currency.toLowerCase(),
amount: String(amount),
decimals,
denomination,
isNativeCurrency,
isFirstAmount,
instanceGasLimit,
tokenGasLimit,
tokenPriceInWei
}
},
gasPrice: (state, getters, rootState, rootGetters) => {
const netId = Number(rootGetters['metamask/netId'])
const gasPriceParams = getters.getGasPriceParams
let gasPrice = BigInt(
gasPriceParams.maxFeePerGas
? gasPriceParams.maxFeePerGas.toString()
: gasPriceParams.gasPrice.toString()
)
// to-do: manually bump gas price for BSC, remove this when we are able to check gasPrice from relayer status
if (netId === ChainId.BSC && gasPrice < parseUnits('3.3', 'gwei')) {
gasPrice = parseUnits('3.3', 'gwei')
}
return gasPrice
}, },
gasPriceInGwei: (state, getters) => { gasPriceInGwei: (state, getters) => {
return fromWei(getters.gasPrice, 'gwei') return formatUnits(getters.gasPrice, 'gwei')
},
gasLimit: (state, getters) => ({ gas, gasLimit } = {}) => {
const { instanceGasLimit } = getters.selectedInstance
return BigInt(gas || gasLimit || instanceGasLimit || WITHDRAW_GAS_LIMIT)
},
refundGasLimit: (state, getters) => {
const { isFirstAmount, tokenGasLimit } = getters.selectedInstance
return isFirstAmount && tokenGasLimit ? BigInt(tokenGasLimit) : undefined
},
ethRefund: (state, getters, rootState) => {
return rootState.application.ethToReceive || 0
},
relayerFeePercent: (state, getters, rootState) => {
return rootState.relayer.selectedRelayer.tornadoServiceFee
} }
} }
@ -42,6 +129,9 @@ export const mutations = {
}, },
SAVE_WITHDRAWAL_FEE_VIA_RELAYER(state, fee) { SAVE_WITHDRAWAL_FEE_VIA_RELAYER(state, fee) {
state.withdrawalFeeViaRelayer = fee state.withdrawalFeeViaRelayer = fee
},
SAVE_L1_FEE(state, fee) {
state.l1Fee = fee
} }
} }
@ -50,8 +140,20 @@ export const actions = {
const { pollInterval } = rootGetters['metamask/networkConfig'] const { pollInterval } = rootGetters['metamask/networkConfig']
try { try {
const gasPriceParams = await getters.oracle.getGasPriceParams() const feeData = await getters.provider.getFeeData()
commit('SAVE_GAS_PARAMS', gasPriceParams)
const gasParams = feeData.maxFeePerGas
? {
maxFeePerGas: feeData.maxFeePerGas.toString(),
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas
? feeData.maxPriorityFeePerGas.toString()
: null
}
: {
gasPrice: feeData.gasPrice ? feeData.gasPrice.toString() : parseUnits('50', 'gwei').toString()
}
commit('SAVE_GAS_PARAMS', gasParams)
} catch (e) { } catch (e) {
console.error('fetchGasPrice', e) console.error('fetchGasPrice', e)
} finally { } finally {
@ -62,32 +164,66 @@ export const actions = {
const { gasPrices } = rootGetters['metamask/networkConfig'] const { gasPrices } = rootGetters['metamask/networkConfig']
commit('SAVE_GAS_PARAMS', { gasPrice: toWei(gasPrices?.fast?.toFixed(9) || 0, 'gwei') }) commit('SAVE_GAS_PARAMS', { gasPrice: toWei(gasPrices?.fast?.toFixed(9) || 0, 'gwei') })
}, },
async calculateWithdrawalNetworkFee({ getters, commit }, { tx }) { calculateWithdrawalNetworkFee({ getters, commit }, { tx }) {
const withdrawalGas = await getters.oracle.getGas(tx, 'user_withdrawal') const gasPrice = getters.gasPrice
const gasLimit = BigInt(tx?.gas || tx?.gasLimit || WITHDRAW_GAS_LIMIT)
commit('SAVE_WITHDRAWAL_NETWORK_FEE', toBN(withdrawalGas)) const gasCost = gasPrice * gasLimit
commit('SAVE_WITHDRAWAL_NETWORK_FEE', toBN(gasCost.toString()))
return gasCost
}, },
async calculateWithdrawalFeeViaRelayer({ dispatch, getters, commit, rootGetters, rootState }, { tx }) { async calculateL1Fee({ commit, getters }, { tx }) {
const feePercent = rootState.relayer.selectedRelayer.tornadoServiceFee const oracle = getters.oracle
const { currency, amount } = rootState.application.selectedStatistic
const nativeCurrency = rootGetters['metamask/nativeCurrency']
const { decimals } = rootGetters['metamask/networkConfig'].tokens[currency]
await dispatch('calculateWithdrawalNetworkFee', { tx }) const l1Fee = await oracle.fetchL1OptimismFee(tx)
if (currency !== nativeCurrency)
await dispatch('application/setDefaultEthToReceive', { currency }, { root: true })
const withdrawalFee = await getters.oracle.calculateWithdrawalFeeViaRelayer( commit('SAVE_L1_FEE', toBN(l1Fee.toString()))
'user_withdrawal', },
tx, async calculateWithdrawalFeeViaRelayer({ dispatch, getters, commit }, { tx }) {
feePercent, const { decimals, denomination, isNativeCurrency, tokenPriceInWei } = getters.selectedInstance
currency.toLowerCase(),
amount,
decimals,
rootState.application.ethToReceive || 0,
rootState.price.prices[currency.toLowerCase()]
)
commit('SAVE_WITHDRAWAL_FEE_VIA_RELAYER', toBN(withdrawalFee)) const oracle = getters.oracle
const gasPrice = getters.gasPrice
const gasLimit = getters.gasLimit(tx)
const refundGasLimit = getters.refundGasLimit
const relayerFeePercent = getters.relayerFeePercent
await dispatch('calculateL1Fee', { tx })
dispatch('calculateWithdrawalNetworkFee', { tx })
const l1Fee = getters.l1Fee
if (!isNativeCurrency) {
dispatch('application/setDefaultEthToReceive', { gasPrice, refundGasLimit }, { root: true })
const ethRefund = getters.ethRefund
const relayerFee = oracle.calculateRelayerFee({
gasPrice,
gasLimit,
l1Fee,
denomination,
ethRefund,
tokenPriceInWei,
tokenDecimals: decimals,
relayerFeePercent,
isEth: isNativeCurrency
})
commit('SAVE_WITHDRAWAL_FEE_VIA_RELAYER', toBN(relayerFee.toString()))
return
}
const relayerFee = oracle.calculateRelayerFee({
gasPrice,
gasLimit,
l1Fee,
denomination,
relayerFeePercent
})
commit('SAVE_WITHDRAWAL_FEE_VIA_RELAYER', toBN(relayerFee.toString()))
} }
} }

@ -2,7 +2,7 @@
/* eslint-disable import/order */ /* eslint-disable import/order */
import Web3 from 'web3' import Web3 from 'web3'
import { utils } from 'ethers' import { AbiCoder, concat } from 'ethers'
import { ToastProgrammatic as Toast } from 'buefy' import { ToastProgrammatic as Toast } from 'buefy'
import networkConfig from '@/networkConfig' import networkConfig from '@/networkConfig'
@ -14,6 +14,8 @@ import { httpConfig } from '@/constants'
const { numberToHex, toWei, fromWei, toBN, hexToNumber, hexToNumberString } = require('web3-utils') const { numberToHex, toWei, fromWei, toBN, hexToNumber, hexToNumberString } = require('web3-utils')
const defaultAbiCoder = AbiCoder.defaultAbiCoder()
const state = () => { const state = () => {
return { return {
approvalAmount: 'unlimited', approvalAmount: 'unlimited',
@ -391,8 +393,8 @@ const actions = {
if (contact || message) { if (contact || message) {
const value = JSON.stringify([contact, message]) const value = JSON.stringify([contact, message])
const tail = utils.defaultAbiCoder.encode(['string'], [value]) const tail = defaultAbiCoder.encode(['string'], [value])
dataWithTail = utils.hexConcat([data, tail]) dataWithTail = concat([data, tail])
} }
const gas = await web3.eth.estimateGas({ const gas = await web3.eth.estimateGas({

@ -1,7 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
/* eslint-disable import/order */ /* eslint-disable import/order */
import { utils } from 'ethers' import { AbiCoder, dataLength, dataSlice } from 'ethers'
import uniqBy from 'lodash/uniqBy' import uniqBy from 'lodash/uniqBy'
import chunk from 'lodash/chunk' import chunk from 'lodash/chunk'
@ -10,6 +10,8 @@ import { CHUNK_COUNT_PER_BATCH_REQUEST } from '@/constants'
const { toWei, fromWei, toBN } = require('web3-utils') const { toWei, fromWei, toBN } = require('web3-utils')
const defaultAbiCoder = AbiCoder.defaultAbiCoder()
const CACHE_TX = {} const CACHE_TX = {}
const CACHE_BLOCK = {} const CACHE_BLOCK = {}
@ -18,15 +20,12 @@ const parseComment = (calldata, govInstance) => {
if (!calldata || !govInstance) return empty if (!calldata || !govInstance) return empty
const methodLength = 4 // length of castDelegatedVote method const methodLength = 4 // length of castDelegatedVote method
const result = utils.defaultAbiCoder.decode( const result = defaultAbiCoder.decode(['address[]', 'uint256', 'bool'], dataSlice(calldata, methodLength))
['address[]', 'uint256', 'bool'],
utils.hexDataSlice(calldata, methodLength)
)
const data = govInstance.methods.castDelegatedVote(...result).encodeABI() const data = govInstance.methods.castDelegatedVote(...result).encodeABI()
const dataLength = utils.hexDataLength(data) const length = dataLength(data)
try { try {
const str = utils.defaultAbiCoder.decode(['string'], utils.hexDataSlice(calldata, dataLength)) const str = defaultAbiCoder.decode(['string'], dataSlice(calldata, length))
const [contact, message] = JSON.parse(str) const [contact, message] = JSON.parse(str)
return { contact, message } return { contact, message }
} catch { } catch {

@ -505,82 +505,26 @@ const actions = {
throw new Error(err.message) throw new Error(err.message)
} }
}, },
async addNetwork(_, { netId }) { async addNetwork({ rootGetters }, { netId }) {
const METAMASK_LIST = { const config = networkConfig[`netId${netId}`]
56: { const { url } = rootGetters['settings/getRpc'](netId)
chainId: '0x38',
chainName: 'Binance Smart Chain Mainnet',
rpcUrls: ['https://bscrpc.com'],
nativeCurrency: {
name: 'Binance Chain Native Token',
symbol: 'BNB',
decimals: 18
},
blockExplorerUrls: ['https://bscscan.com']
},
10: {
chainId: '0xa',
chainName: 'Optimism',
rpcUrls: ['https://mainnet.optimism.io/'],
nativeCurrency: {
name: 'Ether',
symbol: 'ETH',
decimals: 18
},
blockExplorerUrls: ['https://optimistic.etherscan.io']
},
100: {
chainId: '0x64',
chainName: 'Gnosis',
rpcUrls: ['https://development.tornadocash.community/rpc/v1'],
nativeCurrency: {
name: 'xDAI',
symbol: 'xDAI',
decimals: 18
},
blockExplorerUrls: ['https://blockscout.com/xdai/mainnet']
},
137: {
chainId: '0x89',
chainName: 'Polygon Mainnet',
rpcUrls: ['https://polygon-rpc.com'],
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18
},
blockExplorerUrls: ['https://polygonscan.com']
},
42161: {
chainId: '0xA4B1',
chainName: 'Arbitrum One',
rpcUrls: ['https://arb1.arbitrum.io/rpc'],
nativeCurrency: {
name: 'Ether',
symbol: 'ETH',
decimals: 18
},
blockExplorerUrls: ['https://arbiscan.io']
},
43114: {
chainId: '0xA86A',
chainName: 'Avalanche C-Chain',
rpcUrls: ['https://api.avax.network/ext/bc/C/rpc'],
nativeCurrency: {
name: 'Avalanche',
symbol: 'AVAX',
decimals: 18
},
blockExplorerUrls: ['https://snowtrace.io']
}
}
if (METAMASK_LIST[netId]) { await this.$provider.sendRequest({
await this.$provider.sendRequest({ method: 'wallet_addEthereumChain',
method: 'wallet_addEthereumChain', params: [
params: [METAMASK_LIST[netId]] {
}) chainId: `0x${Number(netId).toString(16)}`,
} chainName: config.networkName,
rpcUrls: [url],
nativeCurrency: {
name: config.networkName,
symbol: config.currencyName,
decimals: 18
},
blockExplorerUrls: [config.explorerUrl.tx.replace('/tx', '')]
}
]
})
}, },
async checkNetworkVersion() { async checkNetworkVersion() {
try { try {

@ -18,8 +18,25 @@ export const getters = {
priceOracle: (state, getters, rootState, rootGetters) => { priceOracle: (state, getters, rootState, rootGetters) => {
const netId = Number(rootGetters['metamask/netId']) const netId = Number(rootGetters['metamask/netId'])
const { url: rpcUrl } = rootState.settings[`netId${netId}`].rpc const { url: rpcUrl } = rootState.settings[`netId${netId}`].rpc
const config = rootGetters['metamask/networkConfig']
return new TokenPriceOracle(rpcUrl) return new TokenPriceOracle(netId, rpcUrl, config)
},
tokens: (state, getters, rootStater, rootGetters) => {
const config = rootGetters['metamask/networkConfig']
const { 'torn.contract.tornadocash.eth': tornContract, tokens } = config
return [
{
tokenAddress: tornContract,
symbol: 'TORN',
decimals: 18
},
...Object.values(tokens)
.map(({ tokenAddress, symbol, decimals }) =>
tokenAddress ? { tokenAddress, symbol, decimals } : undefined
)
.filter((t) => t)
]
}, },
tokenRate: (state, getters, rootState) => { tokenRate: (state, getters, rootState) => {
return state.prices[rootState.application.selectedStatistic.currency] return state.prices[rootState.application.selectedStatistic.currency]
@ -49,8 +66,10 @@ export const actions = {
return return
} }
const tokens = getters.tokens
try { try {
const prices = await getters.priceOracle.fetchPrices() const prices = await getters.priceOracle.fetchPrices(tokens)
console.log('prices', prices) console.log('prices', prices)
commit('SAVE_TOKEN_PRICES', prices) commit('SAVE_TOKEN_PRICES', prices)