diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..53b061a --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/.env.example b/.env.example index 1190242..4e12783 100644 --- a/.env.example +++ b/.env.example @@ -8,5 +8,14 @@ REDIS_URL=redis://127.0.0.1:6379 PRIVATE_KEY= # 2.5 means 2.5% RELAYER_FEE=2.5 +APP_PORT=8000 -APP_PORT=8000 \ No newline at end of file +# 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 diff --git a/config.js b/config.js index 6f06bc6..c35c5ab 100644 --- a/config.js +++ b/config.js @@ -147,5 +147,9 @@ module.exports = { defaultGasPrice: 20, gasOracleUrls: ['https://ethgasstation.info/json/ethgasAPI.json', 'https://gas-oracle.zoltu.io/'], port: process.env.APP_PORT, - relayerServiceFee: Number(process.env.RELAYER_FEE) -} \ No newline at end of file + 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 +} diff --git a/src/Fetcher.js b/src/Fetcher.js index 6ef11f6..2b9035c 100644 --- a/src/Fetcher.js +++ b/src/Fetcher.js @@ -55,7 +55,7 @@ class Fetcher { if (Number(json.fast) === 0) { throw new Error('Fetch gasPrice failed') } - + if (json.fast) { this.gasPrices.fast = Number(json.fast) / delimiter } @@ -63,7 +63,7 @@ class Fetcher { if (json.percentile_97) { this.gasPrices.fast = parseInt(json.percentile_90) + 1 / delimiter } - console.log('gas price fetch', this.gasPrices) + // console.log('gas price fetch', this.gasPrices) } else { throw Error('Fetch gasPrice failed') } diff --git a/src/instances.js b/src/instances.js index caf6942..975ec60 100644 --- a/src/instances.js +++ b/src/instances.js @@ -1,8 +1,11 @@ const Fetcher = require('./Fetcher') +const Sender = require('./Sender') const web3 = require('./setupWeb3') const fetcher = new Fetcher(web3) +const sender = new Sender(web3) module.exports = { fetcher, - web3 -} \ No newline at end of file + web3, + sender +} diff --git a/src/relayController.js b/src/relayController.js index c4387f5..ee8830c 100644 --- a/src/relayController.js +++ b/src/relayController.js @@ -1,13 +1,13 @@ const Queue = require('bull') const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils') const mixerABI = require('../abis/mixerABI.json') -const { +const { isValidProof, isValidArgs, isKnownContract, isEnoughFee } = require('./utils') const config = require('../config') const { redisClient, redisOpts } = require('./redis') -const { web3, fetcher } = require('./instances') +const { web3, fetcher, sender } = require('./instances') const withdrawQueue = new Queue('withdraw', redisOpts) const reponseCbs = {} @@ -20,7 +20,7 @@ withdrawQueue.on('completed', respLambda) async function relayController(req, resp) { let requestJob - + const { proof, args, contract } = req.body let { valid , reason } = isValidProof(proof) if (!valid) { @@ -59,7 +59,7 @@ async function relayController(req, resp) { 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() }, { removeOnComplete: true }) reponseCbs[requestJob.id] = resp @@ -97,7 +97,7 @@ withdrawQueue.process(async function(job, done){ let gas = await mixer.methods.withdraw(proof, ...args).estimateGas({ from: web3.eth.defaultAccount, - value: refund + value: refund }) gas += 50000 @@ -120,14 +120,18 @@ withdrawQueue.process(async function(job, done){ value: numberToHex(refund), gas: numberToHex(gas), gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')), + // you can use this gasPrice to test watcher + // gasPrice: numberToHex(100000000), to: mixer._address, netId: config.netId, data, nonce } + tx.date = Date.now() + await redisClient.set('tx:' + nonce, JSON.stringify(tx) ) nonce += 1 await redisClient.set('nonce', nonce) - sendTx(tx, done) + sender.sendTx(tx, done) } catch (e) { console.error(e, 'estimate gas failed') done(null, { @@ -137,38 +141,4 @@ withdrawQueue.process(async function(job, done){ } }) -async function sendTx(tx, done, retryAttempt = 1) { - - 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 \ No newline at end of file +module.exports = relayController diff --git a/src/sender.js b/src/sender.js new file mode 100644 index 0000000..6dd44e5 --- /dev/null +++ b/src/sender.js @@ -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