mirror of
https://github.com/tornadocash/tornado-relayer
synced 2024-02-02 15:04:06 +01:00
linter
This commit is contained in:
parent
cb6cd89665
commit
850cfb3f7e
@ -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",
|
||||
{
|
||||
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": false,
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"printWidth": 110,
|
||||
"trailingComma": "none"
|
||||
}
|
41
README.md
41
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 '<input 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.
|
||||
|
2
app.js
2
app.js
@ -1 +1 @@
|
||||
module.exports = require('./src/index')
|
||||
module.exports = require('./src/index')
|
||||
|
@ -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
|
||||
|
14
src/index.js
14
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 <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> for settings')
|
||||
res.send(
|
||||
'This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> 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`)
|
||||
}
|
||||
|
@ -16,4 +16,4 @@ const redisOpts = {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { redisOpts, redisClient }
|
||||
module.exports = { redisOpts, redisClient }
|
||||
|
@ -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, {
|
||||
|
@ -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' }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
module.exports = web3
|
||||
|
74
src/utils.js
74
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user