diff --git a/.eslintrc.json b/.eslintrc.json
index 8f9e21f..169b4e2 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -21,23 +21,12 @@
"SwitchCase": 1
}
],
- "linebreak-style": [
- "error",
- "unix"
- ],
- "quotes": [
- "error",
- "single"
- ],
- "semi": [
- "error",
- "never"
- ],
- "object-curly-spacing": [
- "error",
- "always"
- ],
+ "linebreak-style": ["error", "unix"],
+ "quotes": ["error", "single"],
+ "semi": ["error", "never"],
+ "object-curly-spacing": ["error", "always"],
"require-await": "error",
+ "comma-dangle": ["error", "never"],
"space-before-function-paren": [
"error",
{
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..f44e0be
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+ "semi": false,
+ "arrowParens": "always",
+ "singleQuote": true,
+ "printWidth": 110,
+ "trailingComma": "none"
+}
diff --git a/README.md b/README.md
index feb3d22..e25010c 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,21 @@
# Relayer for Tornado Cash [![Build Status](https://travis-ci.org/tornadocash/relayer.svg?branch=master)](https://travis-ci.org/tornadocash/relayer) [![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/tornadocash/relayer.svg)](https://hub.docker.com/r/tornadocash/relayer/builds)
## Run locally
+
1. `npm i`
2. `cp .env.example .env`
3. Modify `.env` as needed
4. `npm run start`
5. Go to `http://127.0.0.1:8000`
-6. In order to execute withdraw request, you can run following command
+6. In order to execute withdraw request, you can run following command
+
```bash
curl -X POST -H 'content-type:application/json' --data '' http://127.0.0.1:8000/relay
```
+
Relayer should return a transaction hash.
-*Note.* If you want to change contracts' addresses go to [config.js](./config.js) file.
+_Note._ If you want to change contracts' addresses go to [config.js](./config.js) file.
## Deploy with docker-compose
@@ -20,11 +23,11 @@ docker-compose.yml contains a stack that will automatically provision SSL certif
1. Download docker-compose.yml
2. Change environment variables for `kovan` containers as appropriate
- * add `PRIVATE_KEY` for your relayer address (without 0x prefix)
- * set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` to your domain and add DNS record pointing to your relayer ip address
- * customize `RELAYER_FEE`
- * update `RPC_URL` if needed
- * update `REDIS_URL` if needed
+ - add `PRIVATE_KEY` for your relayer address (without 0x prefix)
+ - set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` to your domain and add DNS record pointing to your relayer ip address
+ - customize `RELAYER_FEE`
+ - update `RPC_URL` if needed
+ - update `REDIS_URL` if needed
3. Run `docker-compose up -d`
## Run as a Docker container
@@ -33,26 +36,26 @@ docker-compose.yml contains a stack that will automatically provision SSL certif
2. Modify `.env` as needed
3. `docker run -d --env-file .env -p 80:8000 tornadocash/relayer`
-In that case you will need to add https termination yourself because browsers with default settings will prevent https
+In that case you will need to add https termination yourself because browsers with default settings will prevent https
tornado.cash UI from submitting your request over http connection
## Input data example
+
```json
{
- "proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
- "args": [
- "0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
- "0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
- "0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
- "0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
- "0x000000000000000000000000000000000000000000000000058d15e176280000",
- "0x0000000000000000000000000000000000000000000000000000000000000000"
- ],
- "contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
+ "proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
+ "args": [
+ "0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
+ "0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
+ "0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
+ "0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
+ "0x000000000000000000000000000000000000000000000000058d15e176280000",
+ "0x0000000000000000000000000000000000000000000000000000000000000000"
+ ],
+ "contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
}
```
-
Disclaimer:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/app.js b/app.js
index 93dd2bc..28a9d8c 100644
--- a/app.js
+++ b/app.js
@@ -1 +1 @@
-module.exports = require('./src/index')
\ No newline at end of file
+module.exports = require('./src/index')
diff --git a/src/Fetcher.js b/src/Fetcher.js
index c2670ea..4bdbfed 100644
--- a/src/Fetcher.js
+++ b/src/Fetcher.js
@@ -30,9 +30,7 @@ class Fetcher {
}
async fetchPrices() {
try {
- let prices = await this.oracle.methods
- .getPricesInETH(this.tokenAddresses, this.oneUintAmount)
- .call()
+ let prices = await this.oracle.methods.getPricesInETH(this.tokenAddresses, this.oneUintAmount).call()
this.ethPrices = prices.reduce((acc, price, i) => {
acc[this.currencyLookup[this.tokenAddresses[i]]] = price
return acc
diff --git a/src/index.js b/src/index.js
index 36a28e8..2be9be0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -34,16 +34,17 @@ app.use(function (req, res, next) {
app.get('/', function (req, res) {
// just for testing purposes
- res.send('This is tornado.cash Relayer service. Check the /status for settings')
+ res.send(
+ 'This is tornado.cash Relayer service. Check the /status for settings'
+ )
})
-
app.get('/status', async function (req, res) {
let nonce = await redisClient.get('nonce')
let latestBlock = null
try {
latestBlock = await web3.eth.getBlockNumber()
- } catch(e) {
+ } catch (e) {
console.error('Problem with RPC', e)
}
const { ethPrices } = fetcher
@@ -74,7 +75,12 @@ console.log(`mixers: ${JSON.stringify(mixers)}`)
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
+const {
+ GAS_PRICE_BUMP_PERCENTAGE,
+ ALLOWABLE_PENDING_TX_TIMEOUT,
+ NONCE_WATCHER_INTERVAL,
+ MAX_GAS_PRICE
+} = process.env
if (!NONCE_WATCHER_INTERVAL) {
console.log(`NONCE_WATCHER_INTERVAL is not set. Using default value ${watherInterval / 1000} sec`)
}
diff --git a/src/redis.js b/src/redis.js
index 0f007b2..fa710e6 100644
--- a/src/redis.js
+++ b/src/redis.js
@@ -16,4 +16,4 @@ const redisOpts = {
}
}
-module.exports = { redisOpts, redisClient }
\ No newline at end of file
+module.exports = { redisOpts, redisClient }
diff --git a/src/relayController.js b/src/relayController.js
index 3df7cd2..6195d45 100644
--- a/src/relayController.js
+++ b/src/relayController.js
@@ -1,9 +1,7 @@
const Queue = require('bull')
const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils')
const mixerABI = require('../abis/mixerABI.json')
-const {
- isValidProof, isValidArgs, isKnownContract, isEnoughFee
-} = require('./utils')
+const { isValidProof, isValidArgs, isKnownContract, isEnoughFee } = require('./utils')
const config = require('../config')
const { redisClient, redisOpts } = require('./redis')
@@ -28,14 +26,15 @@ async function relayController(req, resp) {
return resp.status(400).json({ error: 'Proof format is invalid' })
}
- ({ valid, reason } = isValidArgs(args))
+ // eslint-disable-next-line no-extra-semi
+ ;({ 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' })
@@ -59,9 +58,20 @@ async function relayController(req, resp) {
return resp.status(400).json({ error: 'Relayer address is invalid' })
}
- requestJob = await withdrawQueue.add({
- contract, nullifierHash, root, proof, args, currency, amount, fee: fee.toString(), refund: refund.toString()
- }, { removeOnComplete: true })
+ requestJob = await withdrawQueue.add(
+ {
+ contract,
+ nullifierHash,
+ root,
+ proof,
+ args,
+ currency,
+ amount,
+ fee: fee.toString(),
+ refund: refund.toString()
+ },
+ { removeOnComplete: true }
+ )
reponseCbs[requestJob.id] = resp
}
@@ -102,7 +112,15 @@ withdrawQueue.process(async function (job, done) {
gas += 50000
const ethPrices = fetcher.ethPrices
- const { isEnough, reason } = isEnoughFee({ gas, gasPrices, currency, amount, refund: toBN(refund), ethPrices, fee: toBN(fee) })
+ const { isEnough, reason } = isEnoughFee({
+ gas,
+ gasPrices,
+ currency,
+ amount,
+ refund: toBN(refund),
+ ethPrices,
+ fee: toBN(fee)
+ })
if (!isEnough) {
console.log(`Wrong fee: ${reason}`)
done(null, {
diff --git a/src/sender.js b/src/sender.js
index ae53e62..12a24ef 100644
--- a/src/sender.js
+++ b/src/sender.js
@@ -38,38 +38,43 @@ class Sender {
let signedTx = await this.web3.eth.accounts.signTransaction(tx, config.privateKey)
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
- result.once('transactionHash', (txHash) => {
- console.log(`A new successfully sent tx ${txHash}`)
- if (done) {
- done(null, {
- status: 200,
- msg: { txHash }
- })
- }
- }).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') {
- 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
+ result
+ .once('transactionHash', (txHash) => {
+ console.log(`A new successfully sent tx ${txHash}`)
+ if (done) {
+ done(null, {
+ status: 200,
+ msg: { txHash }
+ })
}
- }
- if (done) {
- done(null, {
- status: 400,
- msg: { error: 'Internal Relayer Error. Please use a different relayer service' }
- })
- }
- })
+ })
+ .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'
+ ) {
+ 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' }
+ })
+ }
+ })
}
}
diff --git a/src/setupWeb3.js b/src/setupWeb3.js
index c7568fd..e8f828f 100644
--- a/src/setupWeb3.js
+++ b/src/setupWeb3.js
@@ -8,9 +8,9 @@ function setup() {
web3.eth.accounts.wallet.add('0x' + privateKey)
web3.eth.defaultAccount = account.address
return web3
- } catch(e) {
+ } catch (e) {
console.error('web3 failed')
}
}
const web3 = setup()
-module.exports = web3
\ No newline at end of file
+module.exports = web3
diff --git a/src/utils.js b/src/utils.js
index f00a0dd..1482d43 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -4,7 +4,7 @@ const { netId, mixers, relayerServiceFee } = require('../config')
function isValidProof(proof) {
// validator expects `websnarkUtils.toSolidityInput(proof)` output
- if (!(proof)) {
+ if (!proof) {
return { valid: false, reason: 'The proof is empty.' }
}
@@ -16,8 +16,7 @@ function isValidProof(proof) {
}
function isValidArgs(args) {
-
- if (!(args)) {
+ if (!args) {
return { valid: false, reason: 'Args are empty' }
}
@@ -25,18 +24,20 @@ function isValidArgs(args) {
return { valid: false, reason: 'Length of args is lower than 6' }
}
- for(let signal of args) {
+ for (let signal of args) {
if (!isHexStrict(signal)) {
return { valid: false, reason: `Corrupted signal ${signal}` }
}
}
- if (args[0].length !== 66 ||
- args[1].length !== 66 ||
- args[2].length !== 42 ||
- args[3].length !== 42 ||
- args[4].length !== 66 ||
- args[5].length !== 66) {
+ if (
+ args[0].length !== 66 ||
+ args[1].length !== 66 ||
+ args[2].length !== 42 ||
+ args[3].length !== 42 ||
+ args[4].length !== 66 ||
+ args[5].length !== 66
+ ) {
return { valid: false, reason: 'The length one of the signals is incorrect' }
}
@@ -56,7 +57,7 @@ function isKnownContract(contract) {
}
function sleep(ms) {
- return new Promise(resolve => setTimeout(resolve, ms))
+ return new Promise((resolve) => setTimeout(resolve, ms))
}
function fromDecimals(value, decimals) {
@@ -77,9 +78,7 @@ function fromDecimals(value, decimals) {
// Split it into a whole and fractional part
const comps = ether.split('.')
if (comps.length > 2) {
- throw new Error(
- '[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points'
- )
+ throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points')
}
let whole = comps[0]
@@ -92,9 +91,7 @@ function fromDecimals(value, decimals) {
fraction = '0'
}
if (fraction.length > baseLength) {
- throw new Error(
- '[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places'
- )
+ throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places')
}
while (fraction.length < baseLength) {
@@ -114,9 +111,15 @@ function fromDecimals(value, decimals) {
function isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
const { decimals } = mixers[`netId${netId}`][currency]
- const decimalsPoint = Math.floor(relayerServiceFee) === relayerServiceFee ? 0 : relayerServiceFee.toString().split('.')[1].length
+ const decimalsPoint =
+ Math.floor(relayerServiceFee) === relayerServiceFee
+ ? 0
+ : relayerServiceFee.toString().split('.')[1].length
+
const roundDecimal = 10 ** decimalsPoint
- const feePercent = toBN(fromDecimals(amount, decimals)).mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100))
+ const feePercent = toBN(fromDecimals(amount, decimals))
+ .mul(toBN(relayerServiceFee * roundDecimal))
+ .div(toBN(roundDecimal * 100))
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
let desiredFee
switch (currency) {
@@ -125,18 +128,23 @@ function isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee
break
}
default: {
- desiredFee =
- expense.add(refund)
- .mul(toBN(10 ** decimals))
- .div(toBN(ethPrices[currency]))
+ desiredFee = expense
+ .add(refund)
+ .mul(toBN(10 ** decimals))
+ .div(toBN(ethPrices[currency]))
desiredFee = desiredFee.add(feePercent)
break
}
}
- console.log('sent fee, desired fee, feePercent', fee.toString(), desiredFee.toString(), feePercent.toString())
+ console.log(
+ 'sent fee, desired fee, feePercent',
+ fee.toString(),
+ desiredFee.toString(),
+ feePercent.toString()
+ )
if (fee.lt(desiredFee)) {
return { isEnough: false, reason: 'Not enough fee' }
- }
+ }
return { isEnough: true }
}
@@ -148,11 +156,7 @@ function getArgsForOracle() {
Object.entries(tokens).map(([currency, data]) => {
if (currency !== 'eth') {
tokenAddresses.push(data.tokenAddress)
- oneUintAmount.push(
- toBN('10')
- .pow(toBN(data.decimals.toString()))
- .toString()
- )
+ oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
currencyLookup[data.tokenAddress] = currency
}
})
@@ -163,4 +167,12 @@ function getMixers() {
return mixers[`netId${netId}`]
}
-module.exports = { isValidProof, isValidArgs, sleep, isKnownContract, isEnoughFee, getMixers, getArgsForOracle }
+module.exports = {
+ isValidProof,
+ isValidArgs,
+ sleep,
+ isKnownContract,
+ isEnoughFee,
+ getMixers,
+ getArgsForOracle
+}