From a49746180e4ac75cba8e405e17caa5a4dc8d2797 Mon Sep 17 00:00:00 2001 From: Alexey Date: Fri, 8 May 2020 20:29:31 +0300 Subject: [PATCH] watcher --- .editorconfig | 9 ++++++ .env.example | 11 ++++++- config.js | 8 +++-- src/Fetcher.js | 4 +-- src/instances.js | 4 +-- src/relayController.js | 29 ++++++----------- src/sender.js | 72 +++++++++++++++++++++++++++--------------- 7 files changed, 86 insertions(+), 51 deletions(-) create mode 100644 .editorconfig 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 f4b35eb..975ec60 100644 --- a/src/instances.js +++ b/src/instances.js @@ -2,10 +2,10 @@ const Fetcher = require('./Fetcher') const Sender = require('./Sender') const web3 = require('./setupWeb3') const fetcher = new Fetcher(web3) -const sender = new Sender(1, web3) +const sender = new Sender(web3) module.exports = { fetcher, web3, sender -} \ No newline at end of file +} diff --git a/src/relayController.js b/src/relayController.js index 50b1063..ee8830c 100644 --- a/src/relayController.js +++ b/src/relayController.js @@ -1,7 +1,7 @@ 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') @@ -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,27 +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 } - await redisClient.set('tx:' + nonce, JSON.stringify(tx)) + tx.date = Date.now() + await redisClient.set('tx:' + nonce, JSON.stringify(tx) ) nonce += 1 await redisClient.set('nonce', nonce) - try { - const txHash = await sender.sendTx(tx) - done(null, { - status: 200, - msg: { txHash } - }) - } catch (e) { - console.error('on transactionHash error', e.message) - done(null, { - status: 400, - msg: { error: 'Internal Relayer Error. Please use a different relayer service' } - }) - } + sender.sendTx(tx, done) } catch (e) { console.error(e, 'estimate gas failed') done(null, { @@ -150,4 +141,4 @@ withdrawQueue.process(async function(job, done){ } }) -module.exports = relayController \ No newline at end of file +module.exports = relayController diff --git a/src/sender.js b/src/sender.js index 1c6ceea..f596f20 100644 --- a/src/sender.js +++ b/src/sender.js @@ -1,37 +1,54 @@ const { redisClient } = require('./redis') const config = require('../config') +const { toBN, toHex, toWei, BN, fromWei } = require('web3-utils') class Sender { - constructor(minedNonce, web3) { - this.minedNonce = Number(minedNonce) + constructor(web3) { this.web3 = web3 + this.watherInterval = config.watherInterval + this.pendingTxTimeout = config.pendingTxTimeout + this.gasBumpPercentage = config.gasBumpPercentage + this.watcher() } - async main() { - const lastNonce = await redisClient.get('nonce') - for(let nonce = this.minedNonce; nonce < lastNonce + 1; nonce++) { - let tx = await redisClient.get('tx' + nonce) - tx = JSON.parse(tx) - const isMined = await this.checkTx(tx) - + 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())) + this.sendTx(tx, null, 9999) + } + } + } catch(e) { + console.error('watcher error:', e) + } finally { + setTimeout(() => this.watcher(), this.watherInterval) } } - async checkTx(tx) { - const networkNonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount) - if () - } - - async sendTx(tx, retryAttempt = 1) { + 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) - let txHash - result.once('transactionHash', function(_txHash){ - console.log(`A new successfully sent tx ${_txHash}`) - txHash = _txHash + + 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', 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.' + 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') { @@ -41,14 +58,19 @@ class Sender { const newNonce = tx.nonce + 1 tx.nonce = newNonce await redisClient.set('nonce', newNonce) - txHash = this.sendTx(tx, retryAttempt) + await redisClient.set('tx:' + newNonce, JSON.stringify(tx)) + this.sendTx(tx, done, retryAttempt) return } } - throw new Error(e.message) + if (done) { + done(null, { + status: 400, + msg: { error: 'Internal Relayer Error. Please use a different relayer service' } + }) + } }) - return txHash } } -module.exports = Sender \ No newline at end of file +module.exports = Sender