mirror of
https://github.com/tornadocash/tornado-relayer
synced 2024-02-02 15:04:06 +01:00
check tornado withdraw fee
This commit is contained in:
parent
5328178d65
commit
0c3c5d1407
@ -7,7 +7,8 @@ REDIS_URL=redis://127.0.0.1:6379
|
||||
# without 0x prefix
|
||||
PRIVATE_KEY=
|
||||
# 2.5 means 2.5%
|
||||
RELAYER_FEE=2.5
|
||||
REGULAR_TORNADO_WITHDRAW_FEE=2.5
|
||||
MINING_SERVICE_FEE=2.5
|
||||
APP_PORT=8000
|
||||
|
||||
# Resubmitter params:
|
||||
|
17
config.js
17
config.js
@ -1,5 +1,7 @@
|
||||
require('dotenv').config()
|
||||
|
||||
const jobType = require('./src/jobTypes')
|
||||
|
||||
function updateConfig(options) {
|
||||
config = Object.assign(config, options)
|
||||
}
|
||||
@ -151,14 +153,15 @@ let config = {
|
||||
},
|
||||
},
|
||||
},
|
||||
defaultGasPrice: 20,
|
||||
port: process.env.APP_PORT || 8000,
|
||||
relayerServiceFee: Number(process.env.RELAYER_FEE),
|
||||
maxGasPrice: process.env.MAX_GAS_PRICE || 200,
|
||||
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
|
||||
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000,
|
||||
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20,
|
||||
rewardAccount: '0x0000000000000000000000000000000000000000',
|
||||
tornadoServiceFee: Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE),
|
||||
miningServiceFee: Number(process.env.MINING_SERVICE_FEE),
|
||||
rewardAccount: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
|
||||
gasLimits: {
|
||||
[jobType.TORNADO_WITHDRAW]: 350000,
|
||||
[jobType.MINING_REWARD]: 800000,
|
||||
[jobType.MINING_WITHDRAW]: 800000,
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
@ -4,7 +4,9 @@
|
||||
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
|
||||
"scripts": {
|
||||
"server": "node src/server.js",
|
||||
"treeUpdater": "node src/treeWatcher",
|
||||
"worker": "node src/worker",
|
||||
"treeWatcher": "node src/treeWatcher",
|
||||
"priceWatcher": "node src/priceWatcher",
|
||||
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||
"prettier:check": "npx prettier --check . --config .prettierrc",
|
||||
"prettier:fix": "npx prettier --write . --config .prettierrc",
|
||||
|
@ -4,7 +4,7 @@ const {
|
||||
getMiningWithdrawInputError,
|
||||
} = require('./validator')
|
||||
const { postJob } = require('./queue')
|
||||
const { jobType } = require('./utils')
|
||||
const jobType = require('./jobTypes')
|
||||
|
||||
async function tornadoWithdraw(req, res) {
|
||||
const inputError = getTornadoWithdrawInputError(req.body)
|
||||
|
5
src/jobTypes.js
Normal file
5
src/jobTypes.js
Normal file
@ -0,0 +1,5 @@
|
||||
module.exports = Object.freeze({
|
||||
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
|
||||
MINING_REWARD: 'MINING_REWARD',
|
||||
MINING_WITHDRAW: 'MINING_WITHDRAW',
|
||||
})
|
22
src/priceWatcher.js
Normal file
22
src/priceWatcher.js
Normal file
@ -0,0 +1,22 @@
|
||||
const Redis = require('ioredis')
|
||||
const { redisUrl, oracleAddress, oracleRpcUrl } = require('../config')
|
||||
const { getArgsForOracle, setSafeInterval } = require('./utils')
|
||||
const redis = new Redis(redisUrl)
|
||||
const Web3 = require('web3')
|
||||
const web3 = new Web3(oracleRpcUrl)
|
||||
|
||||
const priceOracleABI = require('../abis/PriceOracle.abi.json')
|
||||
const oracle = new web3.eth.Contract(priceOracleABI, oracleAddress)
|
||||
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle()
|
||||
|
||||
async function main() {
|
||||
const prices = await oracle.methods.getPricesInETH(tokenAddresses, oneUintAmount).call()
|
||||
const ethPrices = prices.reduce((acc, price, i) => {
|
||||
acc[currencyLookup[tokenAddresses[i]]] = price
|
||||
return acc
|
||||
}, {})
|
||||
await redis.hmset('prices', ethPrices)
|
||||
console.log('Wrote following prices to redis', ethPrices)
|
||||
}
|
||||
|
||||
setSafeInterval(main, 30 * 1000)
|
@ -3,7 +3,6 @@ const status = require('./status')
|
||||
const controller = require('./controller')
|
||||
const { port } = require('../config')
|
||||
const { version } = require('../package.json')
|
||||
const worker = require('./worker')
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
@ -33,6 +32,5 @@ app.post('/relay', controller.tornadoWithdraw)
|
||||
app.post('/v1/miningReward', controller.miningReward)
|
||||
app.post('/v1/miningWithdraw', controller.miningWithdraw)
|
||||
|
||||
worker.start()
|
||||
app.listen(port)
|
||||
console.log(`Relayer ${version} started on port ${port}`)
|
||||
|
75
src/utils.js
75
src/utils.js
@ -1,12 +1,6 @@
|
||||
const { instances, netId } = require('../config')
|
||||
const { poseidon } = require('circomlib')
|
||||
const { toBN, toChecksumAddress } = require('web3-utils')
|
||||
|
||||
const jobType = Object.freeze({
|
||||
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
|
||||
MINING_REWARD: 'MINING_REWARD',
|
||||
MINING_WITHDRAW: 'MINING_WITHDRAW',
|
||||
})
|
||||
const { toBN, toChecksumAddress, BN } = require('web3-utils')
|
||||
|
||||
const sleep = ms => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
@ -49,11 +43,76 @@ function when(source, event) {
|
||||
})
|
||||
}
|
||||
|
||||
function getArgsForOracle() {
|
||||
const tokens = instances.netId1
|
||||
const tokenAddresses = []
|
||||
const oneUintAmount = []
|
||||
const currencyLookup = {}
|
||||
Object.entries(tokens).map(([currency, data]) => {
|
||||
if (currency !== 'eth') {
|
||||
tokenAddresses.push(data.tokenAddress)
|
||||
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
|
||||
currencyLookup[data.tokenAddress] = currency
|
||||
}
|
||||
})
|
||||
return { tokenAddresses, oneUintAmount, currencyLookup }
|
||||
}
|
||||
|
||||
function fromDecimals(value, decimals) {
|
||||
value = value.toString()
|
||||
let ether = value.toString()
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
const negative = ether.substring(0, 1) === '-'
|
||||
if (negative) {
|
||||
ether = ether.substring(1)
|
||||
}
|
||||
|
||||
if (ether === '.') {
|
||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value')
|
||||
}
|
||||
|
||||
// Split it into a whole and fractional part
|
||||
const comps = ether.split('.')
|
||||
if (comps.length > 2) {
|
||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points')
|
||||
}
|
||||
|
||||
let whole = comps[0]
|
||||
let fraction = comps[1]
|
||||
|
||||
if (!whole) {
|
||||
whole = '0'
|
||||
}
|
||||
if (!fraction) {
|
||||
fraction = '0'
|
||||
}
|
||||
if (fraction.length > baseLength) {
|
||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places')
|
||||
}
|
||||
|
||||
while (fraction.length < baseLength) {
|
||||
fraction += '0'
|
||||
}
|
||||
|
||||
whole = new BN(whole)
|
||||
fraction = new BN(fraction)
|
||||
let wei = whole.mul(base).add(fraction)
|
||||
|
||||
if (negative) {
|
||||
wei = wei.mul(negative)
|
||||
}
|
||||
|
||||
return new BN(wei.toString(10), 10)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getInstance,
|
||||
setSafeInterval,
|
||||
poseidonHash2,
|
||||
sleep,
|
||||
when,
|
||||
jobType,
|
||||
getArgsForOracle,
|
||||
fromDecimals,
|
||||
}
|
||||
|
140
src/worker.js
140
src/worker.js
@ -1,6 +1,6 @@
|
||||
const fs = require('fs')
|
||||
const Web3 = require('web3')
|
||||
const { toBN } = require('web3-utils')
|
||||
const { toBN, toWei, fromWei } = require('web3-utils')
|
||||
const MerkleTree = require('fixed-merkle-tree')
|
||||
const Redis = require('ioredis')
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
@ -9,8 +9,21 @@ const tornadoABI = require('../abis/tornadoABI.json')
|
||||
const miningABI = require('../abis/mining.abi.json')
|
||||
const swapABI = require('../abis/swap.abi.json')
|
||||
const { queue } = require('./queue')
|
||||
const { poseidonHash2, jobType } = require('./utils')
|
||||
const { rpcUrl, redisUrl, privateKey, updateConfig, swapAddress, minerAddress } = require('../config')
|
||||
const { poseidonHash2, getInstance, fromDecimals } = require('./utils')
|
||||
const jobType = require('./jobTypes')
|
||||
const {
|
||||
netId,
|
||||
rpcUrl,
|
||||
redisUrl,
|
||||
privateKey,
|
||||
updateConfig,
|
||||
swapAddress,
|
||||
minerAddress,
|
||||
gasLimits,
|
||||
instances,
|
||||
tornadoServiceFee,
|
||||
miningServiceFee,
|
||||
} = require('../config')
|
||||
const { TxManager } = require('tx-manager')
|
||||
const { Controller } = require('tornado-cash-anonymity-mining')
|
||||
|
||||
@ -74,17 +87,48 @@ async function start() {
|
||||
console.log('Worker started')
|
||||
}
|
||||
|
||||
function checkFee({ data, type }) {
|
||||
if (type === jobType.TORNADO_WITHDRAW) {
|
||||
function checkFee({ data }) {
|
||||
if (data.type === jobType.TORNADO_WITHDRAW) {
|
||||
return checkTornadoFee(data)
|
||||
}
|
||||
return checkMiningFee(data)
|
||||
}
|
||||
|
||||
async function checkTornadoFee({ args, contract }) {
|
||||
console.log('args, contract', args, contract)
|
||||
const { currency, amount } = getInstance(contract)
|
||||
const { decimals } = instances[`netId${netId}`][currency]
|
||||
const [fee, refund] = [args[4], args[5]].map(toBN)
|
||||
const { fast } = await gasPriceOracle.gasPrices()
|
||||
console.log('fast', fast)
|
||||
|
||||
const ethPrice = await redis.hget('prices', currency)
|
||||
const expense = toBN(toWei(fast.toString(), 'gwei')).mul(toBN(gasLimits.TORNADO_WITHDRAW))
|
||||
const feePercent = toBN(fromDecimals(amount, decimals))
|
||||
.mul(toBN(tornadoServiceFee * 1e10))
|
||||
.div(toBN(1e10 * 100))
|
||||
let desiredFee
|
||||
switch (currency) {
|
||||
case 'eth': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
desiredFee = expense
|
||||
.add(refund)
|
||||
.mul(toBN(10 ** decimals))
|
||||
.div(toBN(ethPrice))
|
||||
desiredFee = desiredFee.add(feePercent)
|
||||
break
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
'sent fee, desired fee, feePercent',
|
||||
fromWei(fee.toString()),
|
||||
fromWei(desiredFee.toString()),
|
||||
fromWei(feePercent.toString()),
|
||||
)
|
||||
if (fee.lt(desiredFee)) {
|
||||
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
|
||||
}
|
||||
}
|
||||
|
||||
async function checkMiningFee({ args }) {
|
||||
@ -95,61 +139,59 @@ async function checkMiningFee({ args }) {
|
||||
// todo: use desired torn/eth rate and compute the same way as in tornado
|
||||
}
|
||||
|
||||
// may be this looks better
|
||||
// const isTornadoWithdraw = type === jobType.TORNADO_WITHDRAW
|
||||
// const ABI = isTornadoWithdraw ? tornadoABI : miningABI
|
||||
// const contractAddress = isTornadoWithdraw ? data.contract : minerAddress
|
||||
// const value = isTornadoWithdraw ? data.args[5] : 0 // refund
|
||||
function getTxObject({ data, type }) {
|
||||
let ABI,
|
||||
contractAddress,
|
||||
value =
|
||||
type === jobType.TORNADO_WITHDRAW
|
||||
? [tornadoABI, data.contract, data.args[5]]
|
||||
: [miningABI, minerAddress, 0]
|
||||
const method = type !== jobType.MINING_REWARD ? 'withdraw' : 'reward'
|
||||
function getTxObject({ data }) {
|
||||
let [ABI, contractAddress, value] =
|
||||
data.type === jobType.TORNADO_WITHDRAW
|
||||
? [tornadoABI, data.contract, data.args[5]]
|
||||
: [miningABI, minerAddress, 0]
|
||||
const method = data.type !== jobType.MINING_REWARD ? 'withdraw' : 'reward'
|
||||
|
||||
const contract = new web3.eth.Contract(ABI, contractAddress)
|
||||
const calldata = contract.methods[method](data.proof, ...data.args).encodeABI()
|
||||
|
||||
return {
|
||||
value,
|
||||
to: contract,
|
||||
to: contractAddress,
|
||||
data: calldata,
|
||||
}
|
||||
}
|
||||
|
||||
async function process(job) {
|
||||
if (!jobType[job.data.type]) {
|
||||
throw new Error(`Unknown job type: ${job.data.type}`)
|
||||
}
|
||||
await updateStatus(status.ACCEPTED)
|
||||
currentJob = job
|
||||
console.log(`Start processing a new ${job.data.type} job #${job.id}`)
|
||||
await checkFee(job)
|
||||
if (job.data.type !== jobType.TORNADO_WITHDRAW) {
|
||||
// precheck if root is up to date
|
||||
}
|
||||
|
||||
currentTx = await txManager.createTx(getTxObject(job))
|
||||
|
||||
try {
|
||||
await currentTx
|
||||
.send()
|
||||
.on('transactionHash', txHash => {
|
||||
updateTxHash(txHash)
|
||||
updateStatus(status.SENT)
|
||||
})
|
||||
.on('mined', receipt => {
|
||||
console.log('Mined in block', receipt.blockNumber)
|
||||
updateStatus(status.MINED)
|
||||
})
|
||||
.on('confirmations', updateConfirmations)
|
||||
if (!jobType[job.data.type]) {
|
||||
throw new Error(`Unknown job type: ${job.data.type}`)
|
||||
}
|
||||
currentJob = job
|
||||
await updateStatus(status.ACCEPTED)
|
||||
console.log(`Start processing a new ${job.data.type} job #${job.id}`)
|
||||
await checkFee(job)
|
||||
if (job.data.type !== jobType.TORNADO_WITHDRAW) {
|
||||
// precheck if root is up to date
|
||||
}
|
||||
|
||||
await updateStatus(status.CONFIRMED)
|
||||
currentTx = await txManager.createTx(getTxObject(job))
|
||||
|
||||
try {
|
||||
await currentTx
|
||||
.send()
|
||||
.on('transactionHash', txHash => {
|
||||
updateTxHash(txHash)
|
||||
updateStatus(status.SENT)
|
||||
})
|
||||
.on('mined', receipt => {
|
||||
console.log('Mined in block', receipt.blockNumber)
|
||||
updateStatus(status.MINED)
|
||||
})
|
||||
.on('confirmations', updateConfirmations)
|
||||
|
||||
await updateStatus(status.CONFIRMED)
|
||||
} catch (e) {
|
||||
console.error('Revert', e)
|
||||
throw new Error(`Revert by smart contract ${e.message}`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Revert', e)
|
||||
throw new Error(`Revert by smart contract ${e.message}`)
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,4 +213,6 @@ async function updateStatus(status) {
|
||||
await currentJob.update(currentJob.data)
|
||||
}
|
||||
|
||||
start()
|
||||
|
||||
module.exports = { start, process }
|
||||
|
Loading…
Reference in New Issue
Block a user