works for regular tornado. dirty and WIP though
This commit is contained in:
parent
d888fdbd44
commit
c16164876e
@ -1,6 +1,11 @@
|
||||
require('dotenv').config()
|
||||
|
||||
module.exports = {
|
||||
function updateConfig(options) {
|
||||
config = Object.assign(config, options)
|
||||
}
|
||||
|
||||
let config = {
|
||||
updateConfig,
|
||||
netId: Number(process.env.NET_ID) || 42,
|
||||
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
||||
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
|
||||
@ -154,3 +159,5 @@ module.exports = {
|
||||
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20,
|
||||
rewardAccount: '0x0000000000000000000000000000000000000000',
|
||||
}
|
||||
|
||||
module.exports = config
|
||||
|
@ -10,7 +10,7 @@ async function tornadoWithdraw(req, res) {
|
||||
|
||||
const { proof, args, contract } = req.body
|
||||
const id = await postJob({
|
||||
type: 'withdraw',
|
||||
type: 'tornadoWithdraw',
|
||||
data: { proof, args, contract },
|
||||
})
|
||||
return res.json({ id })
|
||||
|
@ -6,11 +6,10 @@ const redis = new Redis(redisUrl)
|
||||
|
||||
const queue = new Queue('proofs', redisUrl)
|
||||
|
||||
async function postJob(type, data) {
|
||||
async function postJob({ type, data }) {
|
||||
const id = uuid()
|
||||
|
||||
const job = await queue.add(
|
||||
'proofs',
|
||||
{
|
||||
id,
|
||||
type,
|
||||
|
@ -1,8 +1,9 @@
|
||||
const express = require('express')
|
||||
const status = require('status')
|
||||
const controller = require('controller')
|
||||
const status = require('./status')
|
||||
const controller = require('./controller')
|
||||
const { port } = require('../config')
|
||||
const { version } = require('../package.json')
|
||||
const worker = require('./worker')
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
@ -27,8 +28,11 @@ app.get('/', status.index)
|
||||
app.get('/v1/status', status.status)
|
||||
app.post('/v1/jobs/:id', status.getJob)
|
||||
app.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
|
||||
app.post('/v1/miningReward', controller.miningReward)
|
||||
app.post('/v1/miningWithdraw', controller.miningWithdraw)
|
||||
app.get('/status', status.status)
|
||||
app.post('/relay', controller.tornadoWithdraw)
|
||||
// app.post('/v1/miningReward', controller.miningReward)
|
||||
// app.post('/v1/miningWithdraw', controller.miningWithdraw)
|
||||
|
||||
console.log('Version:', version)
|
||||
app.listen(port)
|
||||
worker.start()
|
||||
app.listen(port || 8000)
|
||||
console.log(`Relayer ${version} started on port ${port || 8000}`)
|
||||
|
@ -1,29 +1,34 @@
|
||||
const queue = require('queue')
|
||||
const queue = require('./queue')
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
const gasPriceOracle = new GasPriceOracle()
|
||||
const { netId, relayerServiceFee, instances } = require('../config')
|
||||
const { version } = require('../package.json')
|
||||
|
||||
async function status(req, res) {
|
||||
let nonce = await redisClient.get('nonce')
|
||||
let latestBlock = null
|
||||
try {
|
||||
latestBlock = await web3.eth.getBlockNumber()
|
||||
} catch (e) {
|
||||
console.error('Problem with RPC', e)
|
||||
const ethPrices = {
|
||||
dai: '6700000000000000', // 0.0067
|
||||
cdai: '157380000000000',
|
||||
cusdc: '164630000000000',
|
||||
usdc: '7878580000000000',
|
||||
usdt: '7864940000000000',
|
||||
}
|
||||
const { ethPrices } = fetcher
|
||||
res.json({
|
||||
relayerAddress: web3.eth.defaultAccount,
|
||||
mixers,
|
||||
relayerAddress: require('../config').rewardAccount,
|
||||
instances: instances.netId42,
|
||||
gasPrices: await gasPriceOracle.gasPrices(),
|
||||
netId,
|
||||
ethPrices,
|
||||
relayerServiceFee,
|
||||
nonce,
|
||||
nonce: 123,
|
||||
version,
|
||||
latestBlock
|
||||
latestBlock: 12312312,
|
||||
})
|
||||
}
|
||||
|
||||
function index(req, res) {
|
||||
res.send('This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/v1/status>/status</a> for settings')
|
||||
res.send(
|
||||
'This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/v1/status>/status</a> for settings',
|
||||
)
|
||||
}
|
||||
|
||||
async function getJob(req, res) {
|
||||
|
@ -1,6 +1,5 @@
|
||||
const { isAddress } = require('web3-utils')
|
||||
const { isAddress, toChecksumAddress } = require('web3-utils')
|
||||
const { getInstance } = require('./utils')
|
||||
const { rewardAccount } = require('../config')
|
||||
|
||||
const Ajv = require('ajv')
|
||||
const ajv = new Ajv({ format: 'fast' })
|
||||
@ -13,7 +12,7 @@ ajv.addKeyword('isAddress', {
|
||||
return false
|
||||
}
|
||||
},
|
||||
errors: true
|
||||
errors: true,
|
||||
})
|
||||
|
||||
ajv.addKeyword('isKnownContract', {
|
||||
@ -24,18 +23,18 @@ ajv.addKeyword('isKnownContract', {
|
||||
return false
|
||||
}
|
||||
},
|
||||
errors: true
|
||||
errors: true,
|
||||
})
|
||||
|
||||
ajv.addKeyword('isFeeRecipient', {
|
||||
validate: (schema, data) => {
|
||||
try {
|
||||
return rewardAccount === data
|
||||
return require('../config').rewardAccount === toChecksumAddress(data)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
errors: true
|
||||
errors: true,
|
||||
})
|
||||
|
||||
const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress: true }
|
||||
@ -54,11 +53,11 @@ const tornadoWithdrawSchema = {
|
||||
type: 'array',
|
||||
maxItems: 6,
|
||||
minItems: 6,
|
||||
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type]
|
||||
}
|
||||
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['proof', 'contract', 'args']
|
||||
required: ['proof', 'contract', 'args'],
|
||||
}
|
||||
|
||||
const miningRewardSchema = {
|
||||
@ -79,10 +78,10 @@ const miningRewardSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
relayer: relayerType,
|
||||
encryptedAccount: encryptedAccountType
|
||||
encryptedAccount: encryptedAccountType,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['relayer', 'encryptedAccount']
|
||||
required: ['relayer', 'encryptedAccount'],
|
||||
},
|
||||
account: {
|
||||
type: 'object',
|
||||
@ -91,11 +90,17 @@ const miningRewardSchema = {
|
||||
inputNullifierHash: bytes32Type,
|
||||
outputRoot: bytes32Type,
|
||||
outputPathIndices: bytes32Type,
|
||||
outputCommitment: bytes32Type
|
||||
outputCommitment: bytes32Type,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['inputRoot', 'inputNullifierHash', 'outputRoot', 'outputPathIndices', 'outputCommitment']
|
||||
}
|
||||
required: [
|
||||
'inputRoot',
|
||||
'inputNullifierHash',
|
||||
'outputRoot',
|
||||
'outputPathIndices',
|
||||
'outputCommitment',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
@ -107,12 +112,12 @@ const miningRewardSchema = {
|
||||
'depositRoot',
|
||||
'withdrawalRoot',
|
||||
'extData',
|
||||
'account'
|
||||
]
|
||||
}
|
||||
'account',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['proof', 'args']
|
||||
required: ['proof', 'args'],
|
||||
}
|
||||
|
||||
const miningWithdrawSchema = {
|
||||
@ -130,10 +135,10 @@ const miningWithdrawSchema = {
|
||||
properties: {
|
||||
recipient: addressType,
|
||||
relayer: relayerType,
|
||||
encryptedAccount: encryptedAccountType
|
||||
encryptedAccount: encryptedAccountType,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['relayer', 'encryptedAccount', 'recipient']
|
||||
required: ['relayer', 'encryptedAccount', 'recipient'],
|
||||
},
|
||||
account: {
|
||||
type: 'object',
|
||||
@ -142,18 +147,24 @@ const miningWithdrawSchema = {
|
||||
inputNullifierHash: bytes32Type,
|
||||
outputRoot: bytes32Type,
|
||||
outputPathIndices: bytes32Type,
|
||||
outputCommitment: bytes32Type
|
||||
outputCommitment: bytes32Type,
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['inputRoot', 'inputNullifierHash', 'outputRoot', 'outputPathIndices', 'outputCommitment']
|
||||
}
|
||||
required: [
|
||||
'inputRoot',
|
||||
'inputNullifierHash',
|
||||
'outputRoot',
|
||||
'outputPathIndices',
|
||||
'outputCommitment',
|
||||
],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['amount', 'fee', 'extDataHash', 'extData', 'account']
|
||||
}
|
||||
required: ['amount', 'fee', 'extDataHash', 'extData', 'account'],
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
required: ['proof', 'args']
|
||||
required: ['proof', 'args'],
|
||||
}
|
||||
|
||||
const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema)
|
||||
@ -184,5 +195,5 @@ function getMiningWithdrawInputError(data) {
|
||||
module.exports = {
|
||||
getTornadoWithdrawInputError,
|
||||
getMiningRewardInputError,
|
||||
getMiningWithdrawInputError
|
||||
getMiningWithdrawInputError,
|
||||
}
|
||||
|
132
src/worker.js
132
src/worker.js
@ -1,27 +1,27 @@
|
||||
const { queue } = require('./queue')
|
||||
const Web3 = require('web3')
|
||||
const { rpcUrl, redisUrl, privateKey, netId, gasBumpInterval, gasBumpPercentage, maxGasPrice } = require('../config')
|
||||
const { numberToHex, toWei, toHex, toBN, fromWei, toChecksumAddress, BN } = require('web3-utils')
|
||||
const tornadoABI = require('../abis/tornadoABI.json')
|
||||
const { numberToHex, toBN } = require('web3-utils')
|
||||
const MerkleTree = require('fixed-merkle-tree')
|
||||
const { setSafeInterval, poseidonHash2 } = require('./utils')
|
||||
const Redis = require('ioredis')
|
||||
const redis = new Redis(redisUrl)
|
||||
const redisSubscribe = new Redis(redisUrl)
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
|
||||
queue.process(process)
|
||||
redisSubscribe.subscribe('treeUpdate', fetchTree)
|
||||
|
||||
const tornadoABI = require('../abis/tornadoABI.json')
|
||||
const { queue } = require('./queue')
|
||||
const { poseidonHash2 } = require('./utils')
|
||||
const { rpcUrl, redisUrl, privateKey, updateConfig, rewardAccount } = require('../config')
|
||||
const TxManager = require('./TxManager')
|
||||
|
||||
let web3
|
||||
let nonce
|
||||
let currentTx
|
||||
let currentJob
|
||||
let tree
|
||||
let txManager
|
||||
const redis = new Redis(redisUrl)
|
||||
const redisSubscribe = new Redis(redisUrl)
|
||||
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
|
||||
|
||||
async function fetchTree() {
|
||||
const elements = await redis.get('tree:elements')
|
||||
const convert = (_, val) => typeof(val) === 'string' ? toBN(val) : val
|
||||
const convert = (_, val) => (typeof val === 'string' ? toBN(val) : val)
|
||||
tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2)
|
||||
|
||||
if (currentTx) {
|
||||
@ -29,75 +29,55 @@ async function fetchTree() {
|
||||
}
|
||||
}
|
||||
|
||||
async function watcher() {
|
||||
if (currentTx && Date.now() - currentTx.date > gasBumpInterval) {
|
||||
bumpGasPrice()
|
||||
}
|
||||
}
|
||||
|
||||
async function bumpGasPrice() {
|
||||
const newGasPrice = toBN(currentTx.gasPrice).mul(toBN(gasBumpPercentage)).div(toBN(100))
|
||||
const maxGasPrice = toBN(toWei(maxGasPrice.toString(), 'Gwei'))
|
||||
currentTx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
|
||||
currentTx.date = Date.now()
|
||||
console.log(`Resubmitting with gas price ${fromWei(currentTx.gasPrice.toString(), 'gwei')} gwei`)
|
||||
await sendTx(currentTx, updateTxHash)
|
||||
}
|
||||
|
||||
async function init() {
|
||||
async function start() {
|
||||
web3 = new Web3(rpcUrl, null, { transactionConfirmationBlocks: 1 })
|
||||
const account = web3.eth.accounts.privateKeyToAccount('0x' + privateKey)
|
||||
web3.eth.accounts.wallet.add('0x' + privateKey)
|
||||
web3.eth.defaultAccount = account.address
|
||||
nonce = await web3.eth.getTransactionCount(account.address, 'latest')
|
||||
updateConfig({ rewardAccount: account.address })
|
||||
txManager = new TxManager({ privateKey, rpcUrl })
|
||||
queue.process(process)
|
||||
redisSubscribe.subscribe('treeUpdate', fetchTree)
|
||||
await fetchTree()
|
||||
setSafeInterval(watcher, 1000)
|
||||
console.log('Worker started')
|
||||
}
|
||||
|
||||
async function checkTornadoFee(contract, fee, refund) {
|
||||
|
||||
async function checkTornadoFee(/* contract, fee, refund*/) {
|
||||
const { fast } = await gasPriceOracle.gasPrices()
|
||||
console.log('fast', fast)
|
||||
}
|
||||
|
||||
async function process(job) {
|
||||
if (job.type !== 'tornadoWithdraw') {
|
||||
if (job.data.type !== 'tornadoWithdraw') {
|
||||
throw new Error('not implemented')
|
||||
}
|
||||
currentJob = job
|
||||
console.log(Date.now(), ' withdraw started', job.id)
|
||||
const { proof, args, contract } = job.data
|
||||
const { proof, args, contract } = job.data.data
|
||||
const fee = toBN(args[4])
|
||||
const refund = toBN(args[5])
|
||||
await checkTornadoFee(contract, fee, refund)
|
||||
|
||||
|
||||
const instance = new web3.eth.Contract(tornadoABI, contract)
|
||||
const data = instance.methods.withdraw(proof, ...args).encodeABI()
|
||||
const gasPrices = await gasPriceOracle.gasPrices()
|
||||
currentTx = {
|
||||
from: web3.eth.defaultAccount,
|
||||
currentTx = await txManager.createTx({
|
||||
value: numberToHex(refund),
|
||||
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
||||
to: contract,
|
||||
netId,
|
||||
data,
|
||||
nonce,
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
currentTx.gas = await web3.eth.estimateGas(currentTx)
|
||||
}
|
||||
catch (e) {
|
||||
await currentTx
|
||||
.send()
|
||||
.on('transactionHash', updateTxHash)
|
||||
.on('mined', (receipt) => {
|
||||
console.log('Mined in block', receipt.blockNumber)
|
||||
})
|
||||
.on('confirmations', updateConfirmations)
|
||||
} catch (e) {
|
||||
console.error('Revert', e)
|
||||
throw new Error(`Revert by smart contract ${e.message}`)
|
||||
}
|
||||
|
||||
nonce++
|
||||
await sendTx(currentTx, updateTxHash)
|
||||
}
|
||||
|
||||
async function waitForTx(hash) {
|
||||
|
||||
}
|
||||
|
||||
async function updateTxHash(txHash) {
|
||||
@ -106,46 +86,10 @@ async function updateTxHash(txHash) {
|
||||
await currentJob.update(currentJob.data)
|
||||
}
|
||||
|
||||
async function sendTx(tx, onTxHash, retryAttempt) {
|
||||
let signedTx = await this.web3.eth.accounts.signTransaction(tx, privateKey)
|
||||
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
|
||||
|
||||
if (onTxHash) {
|
||||
result.once('transactionHash', onTxHash)
|
||||
async function updateConfirmations(confirmations) {
|
||||
console.log(`Confirmations count ${confirmations}`)
|
||||
currentJob.data.confirmations = confirmations
|
||||
await currentJob.update(currentJob.data)
|
||||
}
|
||||
|
||||
try { // await returns once tx is mined
|
||||
await result
|
||||
} catch (e) {
|
||||
console.log(`Error for tx with nonce ${tx.nonce}\n${e.message}`)
|
||||
if (nonceErrors.includes(e.message)) {
|
||||
console.log('nonce too low, retrying')
|
||||
if (retryAttempt <= 10) {
|
||||
tx.nonce++
|
||||
return sendTx(tx, onTxHash, retryAttempt + 1)
|
||||
}
|
||||
}
|
||||
if (gasPriceErrors.includes(e.message)) {
|
||||
return bumpGasPrice()
|
||||
}
|
||||
throw new Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const nonceErrors = [
|
||||
'Returned error: Transaction nonce is too low. Try incrementing the nonce.',
|
||||
'Returned error: nonce too low',
|
||||
]
|
||||
|
||||
const gasPriceErrors = [
|
||||
'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.',
|
||||
'Returned error: replacement transaction underpriced',
|
||||
]
|
||||
|
||||
async function main() {
|
||||
await init()
|
||||
|
||||
}
|
||||
|
||||
// main()
|
||||
fetchTree()
|
||||
module.exports = { start, process }
|
||||
|
Loading…
Reference in New Issue
Block a user