commit
b88edf9d0a
9
.editorconfig
Normal file
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
|
11
.env.example
11
.env.example
@ -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
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
|
Loading…
Reference in New Issue
Block a user