This commit is contained in:
Alexey 2020-05-08 20:29:31 +03:00
parent f50379a391
commit a49746180e
7 changed files with 86 additions and 51 deletions

9
.editorconfig Normal file
View 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

View File

@ -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

View File

@ -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
}

View File

@ -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')
} }

View File

@ -2,10 +2,10 @@ const Fetcher = require('./Fetcher')
const Sender = require('./Sender') 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(1, web3) const sender = new Sender(web3)
module.exports = { module.exports = {
fetcher, fetcher,
web3, web3,
sender sender
} }

View File

@ -1,7 +1,7 @@
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')
@ -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,27 +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
} }
await redisClient.set('tx:' + nonce, JSON.stringify(tx)) 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)
try { sender.sendTx(tx, done)
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' }
})
}
} catch (e) { } catch (e) {
console.error(e, 'estimate gas failed') console.error(e, 'estimate gas failed')
done(null, { done(null, {
@ -150,4 +141,4 @@ withdrawQueue.process(async function(job, done){
} }
}) })
module.exports = relayController module.exports = relayController

View File

@ -1,37 +1,54 @@
const { redisClient } = require('./redis') const { redisClient } = require('./redis')
const config = require('../config') const config = require('../config')
const { toBN, toHex, toWei, BN, fromWei } = require('web3-utils')
class Sender { class Sender {
constructor(minedNonce, web3) { constructor(web3) {
this.minedNonce = Number(minedNonce)
this.web3 = web3 this.web3 = web3
this.watherInterval = config.watherInterval
this.pendingTxTimeout = config.pendingTxTimeout
this.gasBumpPercentage = config.gasBumpPercentage
this.watcher()
} }
async main() { async watcher() {
const lastNonce = await redisClient.get('nonce') try {
for(let nonce = this.minedNonce; nonce < lastNonce + 1; nonce++) { const networkNonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
let tx = await redisClient.get('tx' + nonce) let tx = await redisClient.get('tx:' + networkNonce)
tx = JSON.parse(tx) if (tx) {
const isMined = await this.checkTx(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) { async sendTx(tx, done, retryAttempt = 1) {
const networkNonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
if ()
}
async sendTx(tx, retryAttempt = 1) {
let signedTx = await this.web3.eth.accounts.signTransaction(tx, config.privateKey) let signedTx = await this.web3.eth.accounts.signTransaction(tx, config.privateKey)
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction) let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
let txHash
result.once('transactionHash', function(_txHash){ result.once('transactionHash', function(txHash){
console.log(`A new successfully sent tx ${_txHash}`) console.log(`A new successfully sent tx ${txHash}`)
txHash = _txHash if (done) {
done(null, {
status: 200,
msg: { txHash }
})
}
}).on('error', async function(e){ }).on('error', async function(e){
console.log('error', e.message) 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.' 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: Transaction nonce is too low. Try incrementing the nonce.'
|| e.message === 'Returned error: nonce too low' || e.message === 'Returned error: nonce too low'
|| e.message === 'Returned error: replacement transaction underpriced') { || e.message === 'Returned error: replacement transaction underpriced') {
@ -41,14 +58,19 @@ class Sender {
const newNonce = tx.nonce + 1 const newNonce = tx.nonce + 1
tx.nonce = newNonce tx.nonce = newNonce
await redisClient.set('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 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 module.exports = Sender