From 1ac1b6c5ce361db99a85fef219c45a6103f15641 Mon Sep 17 00:00:00 2001 From: Alexey Date: Thu, 16 Jul 2020 16:33:35 +0300 Subject: [PATCH] update gas-price-oracle --- .eslintrc.json | 86 +++++++++++++++++++++++------------------- config.js | 3 +- package-lock.json | 40 ++++++++++++++++++++ package.json | 3 +- src/Fetcher.js | 36 ++---------------- src/index.js | 18 ++++----- src/instances.js | 6 ++- src/relayController.js | 16 ++++---- src/sender.js | 20 +++++----- 9 files changed, 125 insertions(+), 103 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e1b8769..8f9e21f 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,40 +1,50 @@ { - "env": { - "node": true, - "browser": true, - "es6": true, - "mocha": true - }, - "extends": "eslint:recommended", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 2018 - }, - "rules": { - "indent": [ - "error", - 2, - {"SwitchCase": 1} - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "never" - ], - "object-curly-spacing": [ - "error", - "always" - ], - "require-await": "error" - } + "env": { + "node": true, + "browser": true, + "es6": true, + "mocha": true + }, + "extends": "eslint:recommended", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 2018 + }, + "rules": { + "indent": [ + "error", + 2, + { + "SwitchCase": 1 + } + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "never" + ], + "object-curly-spacing": [ + "error", + "always" + ], + "require-await": "error", + "space-before-function-paren": [ + "error", + { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + } + ] + } } diff --git a/config.js b/config.js index ebe2f54..5c37f5b 100644 --- a/config.js +++ b/config.js @@ -1,7 +1,7 @@ require('dotenv').config() module.exports = { - version: 2.7, + version: 2.8, netId: Number(process.env.NET_ID) || 42, redisUrl: process.env.REDIS_URL, rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/', @@ -145,7 +145,6 @@ 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), maxGasPrice: process.env.MAX_GAS_PRICE || 200, diff --git a/package-lock.json b/package-lock.json index 84a245d..2ac1bff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -179,6 +179,14 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -198,6 +206,11 @@ "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": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -1462,6 +1475,24 @@ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "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": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -1535,6 +1566,15 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "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": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.0.0.tgz", diff --git a/package.json b/package.json index 12827b6..3a51ba3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "relay", "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", "scripts": { "start": "node app.js", @@ -14,6 +14,7 @@ "bull": "^3.12.1", "dotenv": "^8.2.0", "express": "^4.17.1", + "gas-price-oracle": "^0.1.4", "ioredis": "^4.14.1", "node-fetch": "^2.6.0", "web3": "^1.2.2", diff --git a/src/Fetcher.js b/src/Fetcher.js index 6965ac2..c2670ea 100644 --- a/src/Fetcher.js +++ b/src/Fetcher.js @@ -1,6 +1,5 @@ -const fetch = require('node-fetch') const Web3 = require('web3') -const { gasOracleUrls, defaultGasPrice, oracleRpcUrl, oracleAddress } = require('../config') +const { defaultGasPrice, oracleRpcUrl, oracleAddress } = require('../config') const { getArgsForOracle } = require('./utils') const { redisClient } = require('./redis') const priceOracleABI = require('../abis/PriceOracle.abi.json') @@ -39,46 +38,17 @@ class Fetcher { return acc }, {}) setTimeout(() => this.fetchPrices(), 1000 * 30) - } catch(e) { + } catch (e) { console.error('fetchPrices', e.message) 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() { try { const nonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount) await redisClient.set('nonce', nonce) console.log(`Current nonce: ${nonce}`) - } catch(e) { + } catch (e) { console.error('fetchNonce failed', e.message) setTimeout(this.fetchNonce, 3000) } diff --git a/src/index.js b/src/index.js index 0588094..6caad6c 100644 --- a/src/index.js +++ b/src/index.js @@ -10,7 +10,7 @@ const { maxGasPrice } = require('../config') const relayController = require('./relayController') -const { fetcher, web3 } = require('./instances') +const { fetcher, web3, gasPriceOracle } = require('./instances') const { getMixers } = require('./utils') const mixers = getMixers() 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-Headers', 'Origin, X-Requested-With, Content-Type, Accept') next() @@ -39,11 +39,11 @@ app.get('/', function (req, res) { app.get('/status', async function (req, res) { let nonce = await redisClient.get('nonce') - const { ethPrices, gasPrices } = fetcher + const { ethPrices } = fetcher res.json({ relayerAddress: web3.eth.defaultAccount, mixers, - gasPrices, + gasPrices: await gasPriceOracle.gasPrices(), netId, ethPrices, relayerServiceFee, @@ -57,30 +57,28 @@ app.post('/relay', relayController) let server = app.listen(port || 8000) server.setTimeout(600000) console.log('Gas price oracle started.') -fetcher.fetchGasPrice() fetcher.fetchPrices() fetcher.fetchNonce() console.log('Relayer started on port', port || 8000) console.log(`relayerAddress: ${web3.eth.defaultAccount}`) console.log(`mixers: ${JSON.stringify(mixers)}`) -console.log(`gasPrices: ${JSON.stringify(fetcher.gasPrices)}`) console.log(`netId: ${netId}`) console.log(`ethPrices: ${JSON.stringify(fetcher.ethPrices)}`) 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`) } -if(!GAS_PRICE_BUMP_PERCENTAGE) { +if (!GAS_PRICE_BUMP_PERCENTAGE) { 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`) } -if(!MAX_GAS_PRICE) { +if (!MAX_GAS_PRICE) { console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${maxGasPrice} Gwei`) } diff --git a/src/instances.js b/src/instances.js index 29e9edd..a87c675 100644 --- a/src/instances.js +++ b/src/instances.js @@ -1,11 +1,15 @@ +const { rpcUrl } = require('../config') const Fetcher = require('./Fetcher') const Sender = require('./sender') +const { GasPriceOracle } = require('gas-price-oracle') const web3 = require('./setupWeb3') const fetcher = new Fetcher(web3) const sender = new Sender(web3) +const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl }) module.exports = { fetcher, web3, - sender + sender, + gasPriceOracle } diff --git a/src/relayController.js b/src/relayController.js index ee8830c..3df7cd2 100644 --- a/src/relayController.js +++ b/src/relayController.js @@ -7,7 +7,7 @@ const { const config = require('../config') 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 reponseCbs = {} @@ -22,26 +22,26 @@ async function relayController(req, resp) { let requestJob const { proof, args, contract } = req.body - let { valid , reason } = isValidProof(proof) + let { valid, reason } = isValidProof(proof) if (!valid) { console.log('Proof is invalid:', reason) return resp.status(400).json({ error: 'Proof format is invalid' }) } - ({ valid , reason } = isValidArgs(args)) + ({ valid, reason } = isValidArgs(args)) if (!valid) { console.log('Args are invalid:', reason) return resp.status(400).json({ error: 'Withdraw arguments are invalid' }) } let currency, amount - ( { valid, currency, amount } = isKnownContract(contract)) + ({ valid, currency, amount } = isKnownContract(contract)) if (!valid) { console.log('Contract does not exist:', contract) 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[1], toChecksumAddress(args[2]), @@ -65,9 +65,9 @@ async function relayController(req, 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) - const gasPrices = fetcher.gasPrices + const gasPrices = await gasPriceOracle.gasPrices() const { contract, nullifierHash, root, proof, args, refund, currency, amount, fee } = job.data console.log(JSON.stringify(job.data)) // job.data contains the custom data passed when the job was created @@ -128,7 +128,7 @@ withdrawQueue.process(async function(job, done){ nonce } tx.date = Date.now() - await redisClient.set('tx:' + nonce, JSON.stringify(tx) ) + await redisClient.set('tx:' + nonce, JSON.stringify(tx)) nonce += 1 await redisClient.set('nonce', nonce) sender.sendTx(tx, done) diff --git a/src/sender.js b/src/sender.js index 6dd44e5..ae53e62 100644 --- a/src/sender.js +++ b/src/sender.js @@ -19,15 +19,15 @@ class Sender { 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)) + const maxGasPrice = toBN(toWei(config.maxGasPrice.toString())) tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice)) 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') this.sendTx(tx, null, 9999) } } - } catch(e) { + } catch (e) { console.error('watcher error:', e) } finally { setTimeout(() => this.watcher(), this.watherInterval) @@ -38,7 +38,7 @@ class Sender { let signedTx = await this.web3.eth.accounts.signTransaction(tx, config.privateKey) 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}`) if (done) { done(null, { @@ -46,14 +46,14 @@ class Sender { msg: { txHash } }) } - }).on('error', async function(e){ + }).on('error', async (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') { + 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) { + if (retryAttempt <= 10) { retryAttempt++ const newNonce = tx.nonce + 1 tx.nonce = newNonce