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
|
# without 0x prefix
|
||||||
PRIVATE_KEY=
|
PRIVATE_KEY=
|
||||||
# 2.5 means 2.5%
|
# 2.5 means 2.5%
|
||||||
RELAYER_FEE=2.5
|
REGULAR_TORNADO_WITHDRAW_FEE=2.5
|
||||||
|
MINING_SERVICE_FEE=2.5
|
||||||
APP_PORT=8000
|
APP_PORT=8000
|
||||||
|
|
||||||
# Resubmitter params:
|
# Resubmitter params:
|
||||||
|
17
config.js
17
config.js
@ -1,5 +1,7 @@
|
|||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
|
|
||||||
|
const jobType = require('./src/jobTypes')
|
||||||
|
|
||||||
function updateConfig(options) {
|
function updateConfig(options) {
|
||||||
config = Object.assign(config, options)
|
config = Object.assign(config, options)
|
||||||
}
|
}
|
||||||
@ -151,14 +153,15 @@ let config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultGasPrice: 20,
|
|
||||||
port: process.env.APP_PORT || 8000,
|
port: process.env.APP_PORT || 8000,
|
||||||
relayerServiceFee: Number(process.env.RELAYER_FEE),
|
tornadoServiceFee: Number(process.env.REGULAR_TORNADO_WITHDRAW_FEE),
|
||||||
maxGasPrice: process.env.MAX_GAS_PRICE || 200,
|
miningServiceFee: Number(process.env.MINING_SERVICE_FEE),
|
||||||
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
|
rewardAccount: '0x03Ebd0748Aa4D1457cF479cce56309641e0a98F5',
|
||||||
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000,
|
gasLimits: {
|
||||||
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20,
|
[jobType.TORNADO_WITHDRAW]: 350000,
|
||||||
rewardAccount: '0x0000000000000000000000000000000000000000',
|
[jobType.MINING_REWARD]: 800000,
|
||||||
|
[jobType.MINING_WITHDRAW]: 800000,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config
|
module.exports = config
|
||||||
|
@ -4,7 +4,9 @@
|
|||||||
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
|
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"server": "node src/server.js",
|
"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 .",
|
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||||
"prettier:check": "npx prettier --check . --config .prettierrc",
|
"prettier:check": "npx prettier --check . --config .prettierrc",
|
||||||
"prettier:fix": "npx prettier --write . --config .prettierrc",
|
"prettier:fix": "npx prettier --write . --config .prettierrc",
|
||||||
|
@ -4,7 +4,7 @@ const {
|
|||||||
getMiningWithdrawInputError,
|
getMiningWithdrawInputError,
|
||||||
} = require('./validator')
|
} = require('./validator')
|
||||||
const { postJob } = require('./queue')
|
const { postJob } = require('./queue')
|
||||||
const { jobType } = require('./utils')
|
const jobType = require('./jobTypes')
|
||||||
|
|
||||||
async function tornadoWithdraw(req, res) {
|
async function tornadoWithdraw(req, res) {
|
||||||
const inputError = getTornadoWithdrawInputError(req.body)
|
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 controller = require('./controller')
|
||||||
const { port } = require('../config')
|
const { port } = require('../config')
|
||||||
const { version } = require('../package.json')
|
const { version } = require('../package.json')
|
||||||
const worker = require('./worker')
|
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
@ -33,6 +32,5 @@ app.post('/relay', controller.tornadoWithdraw)
|
|||||||
app.post('/v1/miningReward', controller.miningReward)
|
app.post('/v1/miningReward', controller.miningReward)
|
||||||
app.post('/v1/miningWithdraw', controller.miningWithdraw)
|
app.post('/v1/miningWithdraw', controller.miningWithdraw)
|
||||||
|
|
||||||
worker.start()
|
|
||||||
app.listen(port)
|
app.listen(port)
|
||||||
console.log(`Relayer ${version} started on port ${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 { instances, netId } = require('../config')
|
||||||
const { poseidon } = require('circomlib')
|
const { poseidon } = require('circomlib')
|
||||||
const { toBN, toChecksumAddress } = require('web3-utils')
|
const { toBN, toChecksumAddress, BN } = require('web3-utils')
|
||||||
|
|
||||||
const jobType = Object.freeze({
|
|
||||||
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
|
|
||||||
MINING_REWARD: 'MINING_REWARD',
|
|
||||||
MINING_WITHDRAW: 'MINING_WITHDRAW',
|
|
||||||
})
|
|
||||||
|
|
||||||
const sleep = ms => new Promise(res => setTimeout(res, ms))
|
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 = {
|
module.exports = {
|
||||||
getInstance,
|
getInstance,
|
||||||
setSafeInterval,
|
setSafeInterval,
|
||||||
poseidonHash2,
|
poseidonHash2,
|
||||||
sleep,
|
sleep,
|
||||||
when,
|
when,
|
||||||
jobType,
|
getArgsForOracle,
|
||||||
|
fromDecimals,
|
||||||
}
|
}
|
||||||
|
140
src/worker.js
140
src/worker.js
@ -1,6 +1,6 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const Web3 = require('web3')
|
const Web3 = require('web3')
|
||||||
const { toBN } = require('web3-utils')
|
const { toBN, toWei, fromWei } = require('web3-utils')
|
||||||
const MerkleTree = require('fixed-merkle-tree')
|
const MerkleTree = require('fixed-merkle-tree')
|
||||||
const Redis = require('ioredis')
|
const Redis = require('ioredis')
|
||||||
const { GasPriceOracle } = require('gas-price-oracle')
|
const { GasPriceOracle } = require('gas-price-oracle')
|
||||||
@ -9,8 +9,21 @@ const tornadoABI = require('../abis/tornadoABI.json')
|
|||||||
const miningABI = require('../abis/mining.abi.json')
|
const miningABI = require('../abis/mining.abi.json')
|
||||||
const swapABI = require('../abis/swap.abi.json')
|
const swapABI = require('../abis/swap.abi.json')
|
||||||
const { queue } = require('./queue')
|
const { queue } = require('./queue')
|
||||||
const { poseidonHash2, jobType } = require('./utils')
|
const { poseidonHash2, getInstance, fromDecimals } = require('./utils')
|
||||||
const { rpcUrl, redisUrl, privateKey, updateConfig, swapAddress, minerAddress } = require('../config')
|
const jobType = require('./jobTypes')
|
||||||
|
const {
|
||||||
|
netId,
|
||||||
|
rpcUrl,
|
||||||
|
redisUrl,
|
||||||
|
privateKey,
|
||||||
|
updateConfig,
|
||||||
|
swapAddress,
|
||||||
|
minerAddress,
|
||||||
|
gasLimits,
|
||||||
|
instances,
|
||||||
|
tornadoServiceFee,
|
||||||
|
miningServiceFee,
|
||||||
|
} = require('../config')
|
||||||
const { TxManager } = require('tx-manager')
|
const { TxManager } = require('tx-manager')
|
||||||
const { Controller } = require('tornado-cash-anonymity-mining')
|
const { Controller } = require('tornado-cash-anonymity-mining')
|
||||||
|
|
||||||
@ -74,17 +87,48 @@ async function start() {
|
|||||||
console.log('Worker started')
|
console.log('Worker started')
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkFee({ data, type }) {
|
function checkFee({ data }) {
|
||||||
if (type === jobType.TORNADO_WITHDRAW) {
|
if (data.type === jobType.TORNADO_WITHDRAW) {
|
||||||
return checkTornadoFee(data)
|
return checkTornadoFee(data)
|
||||||
}
|
}
|
||||||
return checkMiningFee(data)
|
return checkMiningFee(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkTornadoFee({ args, contract }) {
|
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()
|
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 }) {
|
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
|
// todo: use desired torn/eth rate and compute the same way as in tornado
|
||||||
}
|
}
|
||||||
|
|
||||||
// may be this looks better
|
function getTxObject({ data }) {
|
||||||
// const isTornadoWithdraw = type === jobType.TORNADO_WITHDRAW
|
let [ABI, contractAddress, value] =
|
||||||
// const ABI = isTornadoWithdraw ? tornadoABI : miningABI
|
data.type === jobType.TORNADO_WITHDRAW
|
||||||
// const contractAddress = isTornadoWithdraw ? data.contract : minerAddress
|
? [tornadoABI, data.contract, data.args[5]]
|
||||||
// const value = isTornadoWithdraw ? data.args[5] : 0 // refund
|
: [miningABI, minerAddress, 0]
|
||||||
function getTxObject({ data, type }) {
|
const method = data.type !== jobType.MINING_REWARD ? 'withdraw' : 'reward'
|
||||||
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'
|
|
||||||
|
|
||||||
const contract = new web3.eth.Contract(ABI, contractAddress)
|
const contract = new web3.eth.Contract(ABI, contractAddress)
|
||||||
const calldata = contract.methods[method](data.proof, ...data.args).encodeABI()
|
const calldata = contract.methods[method](data.proof, ...data.args).encodeABI()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
value,
|
value,
|
||||||
to: contract,
|
to: contractAddress,
|
||||||
data: calldata,
|
data: calldata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function process(job) {
|
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 {
|
try {
|
||||||
await currentTx
|
if (!jobType[job.data.type]) {
|
||||||
.send()
|
throw new Error(`Unknown job type: ${job.data.type}`)
|
||||||
.on('transactionHash', txHash => {
|
}
|
||||||
updateTxHash(txHash)
|
currentJob = job
|
||||||
updateStatus(status.SENT)
|
await updateStatus(status.ACCEPTED)
|
||||||
})
|
console.log(`Start processing a new ${job.data.type} job #${job.id}`)
|
||||||
.on('mined', receipt => {
|
await checkFee(job)
|
||||||
console.log('Mined in block', receipt.blockNumber)
|
if (job.data.type !== jobType.TORNADO_WITHDRAW) {
|
||||||
updateStatus(status.MINED)
|
// precheck if root is up to date
|
||||||
})
|
}
|
||||||
.on('confirmations', updateConfirmations)
|
|
||||||
|
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error('Revert', e)
|
console.error(e)
|
||||||
throw new Error(`Revert by smart contract ${e.message}`)
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,4 +213,6 @@ async function updateStatus(status) {
|
|||||||
await currentJob.update(currentJob.data)
|
await currentJob.update(currentJob.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start()
|
||||||
|
|
||||||
module.exports = { start, process }
|
module.exports = { start, process }
|
||||||
|
Loading…
Reference in New Issue
Block a user