Merge pull request #14 from tornadocash/sender

Sender
This commit is contained in:
Roman Storm 2020-05-08 12:08:01 -07:00 committed by GitHub
commit b88edf9d0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 119 additions and 48 deletions

9
.editorconfig Normal file

@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

@ -8,5 +8,14 @@ REDIS_URL=redis://127.0.0.1:6379
PRIVATE_KEY= PRIVATE_KEY=
# 2.5 means 2.5% # 2.5 means 2.5%
RELAYER_FEE=2.5 RELAYER_FEE=2.5
APP_PORT=8000
APP_PORT=8000 # Resubmitter params:
# how often the watcher will check the first pending tx (in seconds)
NONCE_WATCHER_INTERVAL=30
# how long a tx can be in pending pool (in seconds)
ALLOWABLE_PENDING_TX_TIMEOUT=180
# in GWEI
MAX_GAS_PRICE=100
# how much to increase the gas price for a stuck tx
GAS_PRICE_BUMP_PERCENTAGE=20

@ -147,5 +147,9 @@ module.exports = {
defaultGasPrice: 20, defaultGasPrice: 20,
gasOracleUrls: ['https://ethgasstation.info/json/ethgasAPI.json', 'https://gas-oracle.zoltu.io/'], gasOracleUrls: ['https://ethgasstation.info/json/ethgasAPI.json', 'https://gas-oracle.zoltu.io/'],
port: process.env.APP_PORT, port: process.env.APP_PORT,
relayerServiceFee: Number(process.env.RELAYER_FEE) relayerServiceFee: Number(process.env.RELAYER_FEE),
} maxGasPrice: process.env.MAX_GAS_PRICE,
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL) * 1000,
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT) * 1000,
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE
}

@ -55,7 +55,7 @@ class Fetcher {
if (Number(json.fast) === 0) { if (Number(json.fast) === 0) {
throw new Error('Fetch gasPrice failed') throw new Error('Fetch gasPrice failed')
} }
if (json.fast) { if (json.fast) {
this.gasPrices.fast = Number(json.fast) / delimiter this.gasPrices.fast = Number(json.fast) / delimiter
} }
@ -63,7 +63,7 @@ class Fetcher {
if (json.percentile_97) { if (json.percentile_97) {
this.gasPrices.fast = parseInt(json.percentile_90) + 1 / delimiter this.gasPrices.fast = parseInt(json.percentile_90) + 1 / delimiter
} }
console.log('gas price fetch', this.gasPrices) // console.log('gas price fetch', this.gasPrices)
} else { } else {
throw Error('Fetch gasPrice failed') throw Error('Fetch gasPrice failed')
} }

@ -1,8 +1,11 @@
const Fetcher = require('./Fetcher') const Fetcher = require('./Fetcher')
const Sender = require('./Sender')
const web3 = require('./setupWeb3') const web3 = require('./setupWeb3')
const fetcher = new Fetcher(web3) const fetcher = new Fetcher(web3)
const sender = new Sender(web3)
module.exports = { module.exports = {
fetcher, fetcher,
web3 web3,
} sender
}

@ -1,13 +1,13 @@
const Queue = require('bull') const Queue = require('bull')
const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils') const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils')
const mixerABI = require('../abis/mixerABI.json') const mixerABI = require('../abis/mixerABI.json')
const { const {
isValidProof, isValidArgs, isKnownContract, isEnoughFee isValidProof, isValidArgs, isKnownContract, isEnoughFee
} = require('./utils') } = require('./utils')
const config = require('../config') const config = require('../config')
const { redisClient, redisOpts } = require('./redis') const { redisClient, redisOpts } = require('./redis')
const { web3, fetcher } = require('./instances') const { web3, fetcher, sender } = require('./instances')
const withdrawQueue = new Queue('withdraw', redisOpts) const withdrawQueue = new Queue('withdraw', redisOpts)
const reponseCbs = {} const reponseCbs = {}
@ -20,7 +20,7 @@ withdrawQueue.on('completed', respLambda)
async function relayController(req, resp) { async function relayController(req, resp) {
let requestJob let requestJob
const { proof, args, contract } = req.body const { proof, args, contract } = req.body
let { valid , reason } = isValidProof(proof) let { valid , reason } = isValidProof(proof)
if (!valid) { if (!valid) {
@ -59,7 +59,7 @@ async function relayController(req, resp) {
return resp.status(400).json({ error: 'Relayer address is invalid' }) return resp.status(400).json({ error: 'Relayer address is invalid' })
} }
requestJob = await withdrawQueue.add({ requestJob = await withdrawQueue.add({
contract, nullifierHash, root, proof, args, currency, amount, fee: fee.toString(), refund: refund.toString() contract, nullifierHash, root, proof, args, currency, amount, fee: fee.toString(), refund: refund.toString()
}, { removeOnComplete: true }) }, { removeOnComplete: true })
reponseCbs[requestJob.id] = resp reponseCbs[requestJob.id] = resp
@ -97,7 +97,7 @@ withdrawQueue.process(async function(job, done){
let gas = await mixer.methods.withdraw(proof, ...args).estimateGas({ let gas = await mixer.methods.withdraw(proof, ...args).estimateGas({
from: web3.eth.defaultAccount, from: web3.eth.defaultAccount,
value: refund value: refund
}) })
gas += 50000 gas += 50000
@ -120,14 +120,18 @@ withdrawQueue.process(async function(job, done){
value: numberToHex(refund), value: numberToHex(refund),
gas: numberToHex(gas), gas: numberToHex(gas),
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')), gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
// you can use this gasPrice to test watcher
// gasPrice: numberToHex(100000000),
to: mixer._address, to: mixer._address,
netId: config.netId, netId: config.netId,
data, data,
nonce nonce
} }
tx.date = Date.now()
await redisClient.set('tx:' + nonce, JSON.stringify(tx) )
nonce += 1 nonce += 1
await redisClient.set('nonce', nonce) await redisClient.set('nonce', nonce)
sendTx(tx, done) sender.sendTx(tx, done)
} catch (e) { } catch (e) {
console.error(e, 'estimate gas failed') console.error(e, 'estimate gas failed')
done(null, { done(null, {
@ -137,38 +141,4 @@ withdrawQueue.process(async function(job, done){
} }
}) })
async function sendTx(tx, done, retryAttempt = 1) { module.exports = relayController
let signedTx = await web3.eth.accounts.signTransaction(tx, config.privateKey)
let result = web3.eth.sendSignedTransaction(signedTx.rawTransaction)
result.once('transactionHash', function(txHash){
done(null, {
status: 200,
msg: { txHash }
})
console.log(`A new successfully sent tx ${txHash}`)
}).on('error', async function(e){
console.log('error', e.message)
if(e.message === 'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.'
|| e.message === 'Returned error: Transaction nonce is too low. Try incrementing the nonce.'
|| e.message === 'Returned error: nonce too low'
|| e.message === 'Returned error: replacement transaction underpriced') {
console.log('nonce too low, retrying')
if(retryAttempt <= 10) {
retryAttempt++
const newNonce = tx.nonce + 1
tx.nonce = newNonce
await redisClient.set('nonce', newNonce)
sendTx(tx, done, retryAttempt)
return
}
}
console.error('on transactionHash error', e.message)
done(null, {
status: 400,
msg: { error: 'Internal Relayer Error. Please use a different relayer service' }
})
})
}
module.exports = relayController

76
src/sender.js Normal file

@ -0,0 +1,76 @@
const { redisClient } = require('./redis')
const config = require('../config')
const { toBN, toHex, toWei, BN, fromWei } = require('web3-utils')
class Sender {
constructor(web3) {
this.web3 = web3
this.watherInterval = config.watherInterval
this.pendingTxTimeout = config.pendingTxTimeout
this.gasBumpPercentage = 100 + Number(config.gasBumpPercentage)
this.watcher()
}
async watcher() {
try {
const networkNonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
let tx = await redisClient.get('tx:' + networkNonce)
if (tx) {
tx = JSON.parse(tx)
if (Date.now() - tx.date > this.pendingTxTimeout) {
const newGasPrice = toBN(tx.gasPrice).mul(toBN(this.gasBumpPercentage)).div(toBN(100))
const maxGasPrice = toBN(toWei(config.maxGasPrice))
tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
tx.date = Date.now()
await redisClient.set('tx:' + tx.nonce, JSON.stringify(tx) )
console.log('resubmitting with gas price', fromWei(tx.gasPrice.toString(), 'gwei'), ' gwei')
this.sendTx(tx, null, 9999)
}
}
} catch(e) {
console.error('watcher error:', e)
} finally {
setTimeout(() => this.watcher(), this.watherInterval)
}
}
async sendTx(tx, done, retryAttempt = 1) {
let signedTx = await this.web3.eth.accounts.signTransaction(tx, config.privateKey)
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
result.once('transactionHash', function(txHash){
console.log(`A new successfully sent tx ${txHash}`)
if (done) {
done(null, {
status: 200,
msg: { txHash }
})
}
}).on('error', async function(e){
console.log(`Error for tx with nonce ${tx.nonce}\n${e.message}`)
if(e.message === 'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.'
|| e.message === 'Returned error: Transaction nonce is too low. Try incrementing the nonce.'
|| e.message === 'Returned error: nonce too low'
|| e.message === 'Returned error: replacement transaction underpriced') {
console.log('nonce too low, retrying')
if(retryAttempt <= 10) {
retryAttempt++
const newNonce = tx.nonce + 1
tx.nonce = newNonce
await redisClient.set('nonce', newNonce)
await redisClient.set('tx:' + newNonce, JSON.stringify(tx))
this.sendTx(tx, done, retryAttempt)
return
}
}
if (done) {
done(null, {
status: 400,
msg: { error: 'Internal Relayer Error. Please use a different relayer service' }
})
}
})
}
}
module.exports = Sender