update gas-price-oracle

This commit is contained in:
Alexey 2020-07-16 16:33:35 +03:00
parent f8cb27e6d5
commit 1ac1b6c5ce
9 changed files with 125 additions and 103 deletions

@ -17,7 +17,9 @@
"indent": [ "indent": [
"error", "error",
2, 2,
{"SwitchCase": 1} {
"SwitchCase": 1
}
], ],
"linebreak-style": [ "linebreak-style": [
"error", "error",
@ -35,6 +37,14 @@
"error", "error",
"always" "always"
], ],
"require-await": "error" "require-await": "error",
"space-before-function-paren": [
"error",
{
"anonymous": "always",
"named": "never",
"asyncArrow": "always"
}
]
} }
} }

@ -1,7 +1,7 @@
require('dotenv').config() require('dotenv').config()
module.exports = { module.exports = {
version: 2.7, version: 2.8,
netId: Number(process.env.NET_ID) || 42, netId: Number(process.env.NET_ID) || 42,
redisUrl: process.env.REDIS_URL, redisUrl: process.env.REDIS_URL,
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/', rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
@ -145,7 +145,6 @@ module.exports = {
} }
}, },
defaultGasPrice: 20, defaultGasPrice: 20,
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 || 200, maxGasPrice: process.env.MAX_GAS_PRICE || 200,

40
package-lock.json generated

@ -179,6 +179,14 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
}, },
"axios": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
"requires": {
"follow-redirects": "1.5.10"
}
},
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@ -198,6 +206,11 @@
"tweetnacl": "^0.14.3" "tweetnacl": "^0.14.3"
} }
}, },
"bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A=="
},
"bindings": { "bindings": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@ -1462,6 +1475,24 @@
"integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
"dev": true "dev": true
}, },
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"requires": {
"ms": "2.0.0"
}
}
}
},
"for-each": { "for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -1535,6 +1566,15 @@
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
"dev": true "dev": true
}, },
"gas-price-oracle": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/gas-price-oracle/-/gas-price-oracle-0.1.4.tgz",
"integrity": "sha512-lIxzxu5LtkdUewaFl6MlBU2Me6rWL58ENeAOD2AM/KZYB0Os7EcIBq87ixTced4UF2zb9bzPcVPoEVuH7icOJQ==",
"requires": {
"axios": "^0.19.2",
"bignumber.js": "^9.0.0"
}
},
"get-port": { "get-port": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-5.0.0.tgz", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.0.0.tgz",

@ -1,7 +1,7 @@
{ {
"name": "relay", "name": "relay",
"version": "1.0.0", "version": "1.0.0",
"description": "Relayer for Tornado mixer. https://tornado.cash", "description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"main": "app.js", "main": "app.js",
"scripts": { "scripts": {
"start": "node app.js", "start": "node app.js",
@ -14,6 +14,7 @@
"bull": "^3.12.1", "bull": "^3.12.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"gas-price-oracle": "^0.1.4",
"ioredis": "^4.14.1", "ioredis": "^4.14.1",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"web3": "^1.2.2", "web3": "^1.2.2",

@ -1,6 +1,5 @@
const fetch = require('node-fetch')
const Web3 = require('web3') const Web3 = require('web3')
const { gasOracleUrls, defaultGasPrice, oracleRpcUrl, oracleAddress } = require('../config') const { defaultGasPrice, oracleRpcUrl, oracleAddress } = require('../config')
const { getArgsForOracle } = require('./utils') const { getArgsForOracle } = require('./utils')
const { redisClient } = require('./redis') const { redisClient } = require('./redis')
const priceOracleABI = require('../abis/PriceOracle.abi.json') const priceOracleABI = require('../abis/PriceOracle.abi.json')
@ -39,46 +38,17 @@ class Fetcher {
return acc return acc
}, {}) }, {})
setTimeout(() => this.fetchPrices(), 1000 * 30) setTimeout(() => this.fetchPrices(), 1000 * 30)
} catch(e) { } catch (e) {
console.error('fetchPrices', e.message) console.error('fetchPrices', e.message)
setTimeout(() => this.fetchPrices(), 1000 * 30) setTimeout(() => this.fetchPrices(), 1000 * 30)
} }
} }
async fetchGasPrice({ oracleIndex = 0 } = {}) {
oracleIndex = (oracleIndex + 1) % gasOracleUrls.length
const url = gasOracleUrls[oracleIndex]
const delimiter = url === 'https://ethgasstation.info/json/ethgasAPI.json' ? 10 : 1
try {
const response = await fetch(url)
if (response.status === 200) {
const json = await response.json()
if (Number(json.fast) === 0) {
throw new Error('Fetch gasPrice failed')
}
if (json.fast) {
this.gasPrices.fast = Number(json.fast) / delimiter
}
if (json.percentile_97) {
this.gasPrices.fast = parseInt(json.percentile_90) + 1 / delimiter
}
// console.log('gas price fetch', this.gasPrices)
} else {
throw Error('Fetch gasPrice failed')
}
setTimeout(() => this.fetchGasPrice({ oracleIndex }), 15000)
} catch (e) {
console.log('fetchGasPrice', e.message)
setTimeout(() => this.fetchGasPrice({ oracleIndex }), 15000)
}
}
async fetchNonce() { async fetchNonce() {
try { try {
const nonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount) const nonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
await redisClient.set('nonce', nonce) await redisClient.set('nonce', nonce)
console.log(`Current nonce: ${nonce}`) console.log(`Current nonce: ${nonce}`)
} catch(e) { } catch (e) {
console.error('fetchNonce failed', e.message) console.error('fetchNonce failed', e.message)
setTimeout(this.fetchNonce, 3000) setTimeout(this.fetchNonce, 3000)
} }

@ -10,7 +10,7 @@ const {
maxGasPrice maxGasPrice
} = require('../config') } = require('../config')
const relayController = require('./relayController') const relayController = require('./relayController')
const { fetcher, web3 } = require('./instances') const { fetcher, web3, gasPriceOracle } = require('./instances')
const { getMixers } = require('./utils') const { getMixers } = require('./utils')
const mixers = getMixers() const mixers = getMixers()
const { redisClient } = require('./redis') const { redisClient } = require('./redis')
@ -26,7 +26,7 @@ app.use((err, req, res, next) => {
} }
}) })
app.use(function(req, res, next) { app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*') res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
next() next()
@ -39,11 +39,11 @@ app.get('/', function (req, res) {
app.get('/status', async function (req, res) { app.get('/status', async function (req, res) {
let nonce = await redisClient.get('nonce') let nonce = await redisClient.get('nonce')
const { ethPrices, gasPrices } = fetcher const { ethPrices } = fetcher
res.json({ res.json({
relayerAddress: web3.eth.defaultAccount, relayerAddress: web3.eth.defaultAccount,
mixers, mixers,
gasPrices, gasPrices: await gasPriceOracle.gasPrices(),
netId, netId,
ethPrices, ethPrices,
relayerServiceFee, relayerServiceFee,
@ -57,30 +57,28 @@ app.post('/relay', relayController)
let server = app.listen(port || 8000) let server = app.listen(port || 8000)
server.setTimeout(600000) server.setTimeout(600000)
console.log('Gas price oracle started.') console.log('Gas price oracle started.')
fetcher.fetchGasPrice()
fetcher.fetchPrices() fetcher.fetchPrices()
fetcher.fetchNonce() fetcher.fetchNonce()
console.log('Relayer started on port', port || 8000) console.log('Relayer started on port', port || 8000)
console.log(`relayerAddress: ${web3.eth.defaultAccount}`) console.log(`relayerAddress: ${web3.eth.defaultAccount}`)
console.log(`mixers: ${JSON.stringify(mixers)}`) console.log(`mixers: ${JSON.stringify(mixers)}`)
console.log(`gasPrices: ${JSON.stringify(fetcher.gasPrices)}`)
console.log(`netId: ${netId}`) console.log(`netId: ${netId}`)
console.log(`ethPrices: ${JSON.stringify(fetcher.ethPrices)}`) console.log(`ethPrices: ${JSON.stringify(fetcher.ethPrices)}`)
const { GAS_PRICE_BUMP_PERCENTAGE, ALLOWABLE_PENDING_TX_TIMEOUT, NONCE_WATCHER_INTERVAL, MAX_GAS_PRICE } = process.env const { GAS_PRICE_BUMP_PERCENTAGE, ALLOWABLE_PENDING_TX_TIMEOUT, NONCE_WATCHER_INTERVAL, MAX_GAS_PRICE } = process.env
if(!NONCE_WATCHER_INTERVAL) { if (!NONCE_WATCHER_INTERVAL) {
console.log(`NONCE_WATCHER_INTERVAL is not set. Using default value ${watherInterval / 1000} sec`) console.log(`NONCE_WATCHER_INTERVAL is not set. Using default value ${watherInterval / 1000} sec`)
} }
if(!GAS_PRICE_BUMP_PERCENTAGE) { if (!GAS_PRICE_BUMP_PERCENTAGE) {
console.log(`GAS_PRICE_BUMP_PERCENTAGE is not set. Using default value ${gasBumpPercentage}%`) console.log(`GAS_PRICE_BUMP_PERCENTAGE is not set. Using default value ${gasBumpPercentage}%`)
} }
if(!ALLOWABLE_PENDING_TX_TIMEOUT) { if (!ALLOWABLE_PENDING_TX_TIMEOUT) {
console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${pendingTxTimeout / 1000} sec`) console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${pendingTxTimeout / 1000} sec`)
} }
if(!MAX_GAS_PRICE) { if (!MAX_GAS_PRICE) {
console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${maxGasPrice} Gwei`) console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${maxGasPrice} Gwei`)
} }

@ -1,11 +1,15 @@
const { rpcUrl } = require('../config')
const Fetcher = require('./Fetcher') const Fetcher = require('./Fetcher')
const Sender = require('./sender') const Sender = require('./sender')
const { GasPriceOracle } = require('gas-price-oracle')
const web3 = require('./setupWeb3') const web3 = require('./setupWeb3')
const fetcher = new Fetcher(web3) const fetcher = new Fetcher(web3)
const sender = new Sender(web3) const sender = new Sender(web3)
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
module.exports = { module.exports = {
fetcher, fetcher,
web3, web3,
sender sender,
gasPriceOracle
} }

@ -7,7 +7,7 @@ const {
const config = require('../config') const config = require('../config')
const { redisClient, redisOpts } = require('./redis') const { redisClient, redisOpts } = require('./redis')
const { web3, fetcher, sender } = require('./instances') const { web3, fetcher, sender, gasPriceOracle } = require('./instances')
const withdrawQueue = new Queue('withdraw', redisOpts) const withdrawQueue = new Queue('withdraw', redisOpts)
const reponseCbs = {} const reponseCbs = {}
@ -22,26 +22,26 @@ 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) {
console.log('Proof is invalid:', reason) console.log('Proof is invalid:', reason)
return resp.status(400).json({ error: 'Proof format is invalid' }) return resp.status(400).json({ error: 'Proof format is invalid' })
} }
({ valid , reason } = isValidArgs(args)) ({ valid, reason } = isValidArgs(args))
if (!valid) { if (!valid) {
console.log('Args are invalid:', reason) console.log('Args are invalid:', reason)
return resp.status(400).json({ error: 'Withdraw arguments are invalid' }) return resp.status(400).json({ error: 'Withdraw arguments are invalid' })
} }
let currency, amount let currency, amount
( { valid, currency, amount } = isKnownContract(contract)) ({ valid, currency, amount } = isKnownContract(contract))
if (!valid) { if (!valid) {
console.log('Contract does not exist:', contract) console.log('Contract does not exist:', contract)
return resp.status(400).json({ error: 'This relayer does not support the token' }) return resp.status(400).json({ error: 'This relayer does not support the token' })
} }
const [ root, nullifierHash, recipient, relayer, fee, refund ] = [ const [root, nullifierHash, recipient, relayer, fee, refund] = [
args[0], args[0],
args[1], args[1],
toChecksumAddress(args[2]), toChecksumAddress(args[2]),
@ -65,9 +65,9 @@ async function relayController(req, resp) {
reponseCbs[requestJob.id] = resp reponseCbs[requestJob.id] = resp
} }
withdrawQueue.process(async function(job, done){ withdrawQueue.process(async function (job, done) {
console.log(Date.now(), ' withdraw started', job.id) console.log(Date.now(), ' withdraw started', job.id)
const gasPrices = fetcher.gasPrices const gasPrices = await gasPriceOracle.gasPrices()
const { contract, nullifierHash, root, proof, args, refund, currency, amount, fee } = job.data const { contract, nullifierHash, root, proof, args, refund, currency, amount, fee } = job.data
console.log(JSON.stringify(job.data)) console.log(JSON.stringify(job.data))
// job.data contains the custom data passed when the job was created // job.data contains the custom data passed when the job was created
@ -128,7 +128,7 @@ withdrawQueue.process(async function(job, done){
nonce nonce
} }
tx.date = Date.now() tx.date = Date.now()
await redisClient.set('tx:' + nonce, JSON.stringify(tx) ) await redisClient.set('tx:' + nonce, JSON.stringify(tx))
nonce += 1 nonce += 1
await redisClient.set('nonce', nonce) await redisClient.set('nonce', nonce)
sender.sendTx(tx, done) sender.sendTx(tx, done)

@ -19,15 +19,15 @@ class Sender {
tx = JSON.parse(tx) tx = JSON.parse(tx)
if (Date.now() - tx.date > this.pendingTxTimeout) { if (Date.now() - tx.date > this.pendingTxTimeout) {
const newGasPrice = toBN(tx.gasPrice).mul(toBN(this.gasBumpPercentage)).div(toBN(100)) const newGasPrice = toBN(tx.gasPrice).mul(toBN(this.gasBumpPercentage)).div(toBN(100))
const maxGasPrice = toBN(toWei(config.maxGasPrice)) const maxGasPrice = toBN(toWei(config.maxGasPrice.toString()))
tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice)) tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
tx.date = Date.now() tx.date = Date.now()
await redisClient.set('tx:' + tx.nonce, JSON.stringify(tx) ) await redisClient.set('tx:' + tx.nonce, JSON.stringify(tx))
console.log('resubmitting with gas price', fromWei(tx.gasPrice.toString(), 'gwei'), ' gwei') console.log('resubmitting with gas price', fromWei(tx.gasPrice.toString(), 'gwei'), ' gwei')
this.sendTx(tx, null, 9999) this.sendTx(tx, null, 9999)
} }
} }
} catch(e) { } catch (e) {
console.error('watcher error:', e) console.error('watcher error:', e)
} finally { } finally {
setTimeout(() => this.watcher(), this.watherInterval) setTimeout(() => this.watcher(), this.watherInterval)
@ -38,7 +38,7 @@ class Sender {
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)
result.once('transactionHash', function(txHash){ result.once('transactionHash', (txHash) => {
console.log(`A new successfully sent tx ${txHash}`) console.log(`A new successfully sent tx ${txHash}`)
if (done) { if (done) {
done(null, { done(null, {
@ -46,14 +46,14 @@ class Sender {
msg: { txHash } msg: { txHash }
}) })
} }
}).on('error', async function(e){ }).on('error', async (e) => {
console.log(`Error for tx with nonce ${tx.nonce}\n${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') {
console.log('nonce too low, retrying') console.log('nonce too low, retrying')
if(retryAttempt <= 10) { if (retryAttempt <= 10) {
retryAttempt++ retryAttempt++
const newNonce = tx.nonce + 1 const newNonce = tx.nonce + 1
tx.nonce = newNonce tx.nonce = newNonce