mirror of
https://github.com/tornadocash/tornado-relayer
synced 2024-02-02 15:04:06 +01:00
wip
This commit is contained in:
parent
df89ab41d5
commit
c4cf2863e3
@ -26,7 +26,7 @@
|
|||||||
"semi": ["error", "never"],
|
"semi": ["error", "never"],
|
||||||
"object-curly-spacing": ["error", "always"],
|
"object-curly-spacing": ["error", "always"],
|
||||||
"require-await": "error",
|
"require-await": "error",
|
||||||
"comma-dangle": ["error", "never"],
|
"comma-dangle": ["error", "only-multiline"],
|
||||||
"space-before-function-paren": [
|
"space-before-function-paren": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
|
1085
abis/mining.abi.json
Normal file
1085
abis/mining.abi.json
Normal file
File diff suppressed because it is too large
Load Diff
32
config.js
32
config.js
@ -2,15 +2,17 @@ require('dotenv').config()
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
netId: Number(process.env.NET_ID) || 42,
|
netId: Number(process.env.NET_ID) || 42,
|
||||||
redisUrl: process.env.REDIS_URL,
|
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
||||||
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
|
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
|
||||||
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/',
|
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/',
|
||||||
oracleAddress: '0xA2b8E7ee7c8a18ea561A5CF7C9C365592026E374',
|
oracleAddress: '0xA2b8E7ee7c8a18ea561A5CF7C9C365592026E374',
|
||||||
|
minerAddress: '0x4c4C5cCC263A4531b90042561523c4a1Ad571751',
|
||||||
|
minerMerkleTreeHeight: 10,
|
||||||
privateKey: process.env.PRIVATE_KEY,
|
privateKey: process.env.PRIVATE_KEY,
|
||||||
mixers: {
|
instances: {
|
||||||
netId1: {
|
netId1: {
|
||||||
eth: {
|
eth: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
|
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
|
||||||
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
|
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
|
||||||
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
|
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
|
||||||
@ -20,7 +22,7 @@ module.exports = {
|
|||||||
decimals: 18
|
decimals: 18
|
||||||
},
|
},
|
||||||
dai: {
|
dai: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
||||||
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
|
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
|
||||||
'10000': '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
|
'10000': '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
|
||||||
@ -31,7 +33,7 @@ module.exports = {
|
|||||||
decimals: 18
|
decimals: 18
|
||||||
},
|
},
|
||||||
cdai: {
|
cdai: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
|
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
|
||||||
'50000': '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
|
'50000': '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
|
||||||
'500000': '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
|
'500000': '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
|
||||||
@ -42,7 +44,7 @@ module.exports = {
|
|||||||
decimals: 8
|
decimals: 8
|
||||||
},
|
},
|
||||||
usdc: {
|
usdc: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
|
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
|
||||||
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
|
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
|
||||||
'10000': '0xD691F27f38B395864Ea86CfC7253969B409c362d',
|
'10000': '0xD691F27f38B395864Ea86CfC7253969B409c362d',
|
||||||
@ -53,7 +55,7 @@ module.exports = {
|
|||||||
decimals: 6
|
decimals: 6
|
||||||
},
|
},
|
||||||
cusdc: {
|
cusdc: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'5000': '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
|
'5000': '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
|
||||||
'50000': '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
|
'50000': '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
|
||||||
'500000': '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
|
'500000': '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
|
||||||
@ -64,7 +66,7 @@ module.exports = {
|
|||||||
decimals: 8
|
decimals: 8
|
||||||
},
|
},
|
||||||
usdt: {
|
usdt: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
|
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
|
||||||
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
|
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
|
||||||
'10000': '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
|
'10000': '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
|
||||||
@ -77,7 +79,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
netId42: {
|
netId42: {
|
||||||
eth: {
|
eth: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'0.1': '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
'0.1': '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
||||||
'1': '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
|
'1': '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
|
||||||
'10': '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
|
'10': '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
|
||||||
@ -87,7 +89,7 @@ module.exports = {
|
|||||||
decimals: 18
|
decimals: 18
|
||||||
},
|
},
|
||||||
dai: {
|
dai: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'100': '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
|
'100': '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
|
||||||
'1000': '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
|
'1000': '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
|
||||||
'10000': '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
|
'10000': '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
|
||||||
@ -98,7 +100,7 @@ module.exports = {
|
|||||||
decimals: 18
|
decimals: 18
|
||||||
},
|
},
|
||||||
cdai: {
|
cdai: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'5000': '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
|
'5000': '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
|
||||||
'50000': '0x7182EA067e0f050997444FCb065985Fd677C16b6',
|
'50000': '0x7182EA067e0f050997444FCb065985Fd677C16b6',
|
||||||
'500000': '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
|
'500000': '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
|
||||||
@ -109,7 +111,7 @@ module.exports = {
|
|||||||
decimals: 8
|
decimals: 8
|
||||||
},
|
},
|
||||||
usdc: {
|
usdc: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'100': '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
|
'100': '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
|
||||||
'1000': '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
|
'1000': '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
|
||||||
'10000': '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
|
'10000': '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
|
||||||
@ -120,7 +122,7 @@ module.exports = {
|
|||||||
decimals: 6
|
decimals: 6
|
||||||
},
|
},
|
||||||
cusdc: {
|
cusdc: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'5000': '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
|
'5000': '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
|
||||||
'50000': '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
|
'50000': '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
|
||||||
'500000': '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
|
'500000': '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
|
||||||
@ -131,7 +133,7 @@ module.exports = {
|
|||||||
decimals: 8
|
decimals: 8
|
||||||
},
|
},
|
||||||
usdt: {
|
usdt: {
|
||||||
mixerAddress: {
|
instanceAddress: {
|
||||||
'100': '0x327853Da7916a6A0935563FB1919A48843036b42',
|
'100': '0x327853Da7916a6A0935563FB1919A48843036b42',
|
||||||
'1000': '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
|
'1000': '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
|
||||||
'10000': '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
|
'10000': '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
|
||||||
@ -144,7 +146,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
defaultGasPrice: 20,
|
defaultGasPrice: 20,
|
||||||
port: process.env.APP_PORT,
|
port: process.env.APP_PORT || 8000,
|
||||||
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,
|
||||||
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
|
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
|
||||||
|
2
keys/.gitignore
vendored
Normal file
2
keys/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# avoid committing non-prduction blobs
|
||||||
|
./*
|
243053
keys/TreeUpdate.json
Normal file
243053
keys/TreeUpdate.json
Normal file
File diff suppressed because one or more lines are too long
BIN
keys/TreeUpdate_proving_key.bin
Normal file
BIN
keys/TreeUpdate_proving_key.bin
Normal file
Binary file not shown.
2709
package-lock.json
generated
2709
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
@ -2,9 +2,9 @@
|
|||||||
"name": "relay",
|
"name": "relay",
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
|
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
|
||||||
"main": "app.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app.js",
|
"server": "node src/server.js",
|
||||||
|
"treeUpdater": "node src/treeWatcher",
|
||||||
"eslint": "npx eslint --ignore-path .gitignore .",
|
"eslint": "npx eslint --ignore-path .gitignore .",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
@ -12,12 +12,16 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bull": "^3.12.1",
|
"bull": "^3.12.1",
|
||||||
|
"circomlib": "git+https://github.com/tornadocash/circomlib.git#5beb6aee94923052faeecea40135d45b6ce6172c",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"fixed-merkle-tree": "^0.4.0",
|
||||||
"gas-price-oracle": "^0.1.5",
|
"gas-price-oracle": "^0.1.5",
|
||||||
"ioredis": "^4.14.1",
|
"ioredis": "^4.14.1",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"web3": "^1.2.2",
|
"tornado-cash-anonymity-mining": "git+https://github.com/tornadocash/tornado-anonymity-mining.git#820bd83254f3264cebaf255869641ebc33288dc3",
|
||||||
|
"uuid": "^8.3.0",
|
||||||
|
"web3": "^1.3.0",
|
||||||
"web3-utils": "^1.2.2"
|
"web3-utils": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -4,6 +4,8 @@ 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 config = require('../config')
|
||||||
const { redisClient, redisOpts } = require('./redis')
|
const { redisClient, redisOpts } = require('./redis')
|
||||||
|
const { GasPriceOracle } = require('gas-price-oracle')
|
||||||
|
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
|
||||||
|
|
||||||
const { web3, fetcher, sender, gasPriceOracle } = require('./instances')
|
const { web3, fetcher, sender, gasPriceOracle } = require('./instances')
|
||||||
const withdrawQueue = new Queue('withdraw', redisOpts)
|
const withdrawQueue = new Queue('withdraw', redisOpts)
|
19
src.bak/treeUpdate.js
Normal file
19
src.bak/treeUpdate.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const { Controller } = require('tornado-cash-anonymity-mining')
|
||||||
|
const { web3 } = require('./instances')
|
||||||
|
const { farmingAddress, farmingMerkleTreeHeight } = require('../config')
|
||||||
|
|
||||||
|
const contract = web3.eth.contract(require('../abis/mining.abi.json'), farmingAddress)
|
||||||
|
const provingKeys = {
|
||||||
|
treeUpdateCircuit: require('.../keys/TreeUpdate.json'),
|
||||||
|
treeUpdateProvingKey: fs.readFileSync('../keys/TreeUpdate_proving_key.bin').buffer,
|
||||||
|
}
|
||||||
|
const controller = new Controller({
|
||||||
|
contract,
|
||||||
|
provingKeys,
|
||||||
|
merkleTreeHeight: farmingMerkleTreeHeight,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// await controller.init()
|
||||||
|
// await controller.treeUpdate(commitment)
|
178
src.bak/utils.js
Normal file
178
src.bak/utils.js
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
const { isHexStrict, toBN, toWei, BN } = require('web3-utils')
|
||||||
|
const { netId, mixers, relayerServiceFee } = require('../config')
|
||||||
|
|
||||||
|
function isValidProof(proof) {
|
||||||
|
// validator expects `websnarkUtils.toSolidityInput(proof)` output
|
||||||
|
|
||||||
|
if (!proof) {
|
||||||
|
return { valid: false, reason: 'The proof is empty.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
|
||||||
|
return { valid: false, reason: 'Corrupted proof' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidArgs(args) {
|
||||||
|
if (!args) {
|
||||||
|
return { valid: false, reason: 'Args are empty' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length !== 6) {
|
||||||
|
return { valid: false, reason: 'Length of args is lower than 6' }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
) {
|
||||||
|
return { valid: false, reason: 'The length one of the signals is incorrect' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
function isKnownContract(contract) {
|
||||||
|
const mixers = getMixers()
|
||||||
|
for (let currency of Object.keys(mixers)) {
|
||||||
|
for (let amount of Object.keys(mixers[currency].mixerAddress)) {
|
||||||
|
if (mixers[currency].mixerAddress[amount] === contract) {
|
||||||
|
return { valid: true, currency, amount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { valid: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromDecimals(value, decimals) {
|
||||||
|
value = value.toString()
|
||||||
|
let ether = value.toString()
|
||||||
|
const base = new BN('10').pow(new BN(decimals))
|
||||||
|
const baseLength = base.toString(10).length - 1 || 1
|
||||||
|
|
||||||
|
const negative = ether.substring(0, 1) === '-'
|
||||||
|
if (negative) {
|
||||||
|
ether = ether.substring(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ether === '.') {
|
||||||
|
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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')
|
||||||
|
}
|
||||||
|
|
||||||
|
let whole = comps[0]
|
||||||
|
let fraction = comps[1]
|
||||||
|
|
||||||
|
if (!whole) {
|
||||||
|
whole = '0'
|
||||||
|
}
|
||||||
|
if (!fraction) {
|
||||||
|
fraction = '0'
|
||||||
|
}
|
||||||
|
if (fraction.length > baseLength) {
|
||||||
|
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places')
|
||||||
|
}
|
||||||
|
|
||||||
|
while (fraction.length < baseLength) {
|
||||||
|
fraction += '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
whole = new BN(whole)
|
||||||
|
fraction = new BN(fraction)
|
||||||
|
let wei = whole.mul(base).add(fraction)
|
||||||
|
|
||||||
|
if (negative) {
|
||||||
|
wei = wei.mul(negative)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BN(wei.toString(10), 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 roundDecimal = 10 ** decimalsPoint
|
||||||
|
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) {
|
||||||
|
case 'eth': {
|
||||||
|
desiredFee = expense.add(feePercent)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
if (fee.lt(desiredFee)) {
|
||||||
|
return { isEnough: false, reason: 'Not enough fee' }
|
||||||
|
}
|
||||||
|
return { isEnough: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArgsForOracle() {
|
||||||
|
const tokens = mixers['netId1']
|
||||||
|
const tokenAddresses = []
|
||||||
|
const oneUintAmount = []
|
||||||
|
const currencyLookup = {}
|
||||||
|
Object.entries(tokens).map(([currency, data]) => {
|
||||||
|
if (currency !== 'eth') {
|
||||||
|
tokenAddresses.push(data.tokenAddress)
|
||||||
|
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
|
||||||
|
currencyLookup[data.tokenAddress] = currency
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { tokenAddresses, oneUintAmount, currencyLookup }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMixers() {
|
||||||
|
return mixers[`netId${netId}`]
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
isValidProof,
|
||||||
|
isValidArgs,
|
||||||
|
sleep,
|
||||||
|
isKnownContract,
|
||||||
|
isEnoughFee,
|
||||||
|
getMixers,
|
||||||
|
getArgsForOracle
|
||||||
|
}
|
21
src/controller.js
Normal file
21
src/controller.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const { getWithdrawInputError } = require('./validate')
|
||||||
|
const { postJob } = require('./queue')
|
||||||
|
|
||||||
|
async function tornadoWithdraw(req, res) {
|
||||||
|
const inputError = getWithdrawInputError(req.body)
|
||||||
|
if (inputError) {
|
||||||
|
console.log('Invalid input:', inputError)
|
||||||
|
return res.status(400).json({ error: inputError })
|
||||||
|
}
|
||||||
|
|
||||||
|
const { proof, args, contract } = req.body
|
||||||
|
const id = await postJob({
|
||||||
|
type: 'withdraw',
|
||||||
|
data: { proof, args, contract },
|
||||||
|
})
|
||||||
|
return res.json({ id })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tornadoWithdraw,
|
||||||
|
}
|
40
src/queue.js
Normal file
40
src/queue.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
const { v4: uuid } = require('uuid')
|
||||||
|
const Queue = require('bull')
|
||||||
|
const Redis = require('ioredis')
|
||||||
|
const { redisUrl } = require('../config')
|
||||||
|
const redis = new Redis(redisUrl)
|
||||||
|
|
||||||
|
const queue = new Queue('proofs', redisUrl)
|
||||||
|
|
||||||
|
async function postJob(type, data) {
|
||||||
|
const id = uuid()
|
||||||
|
|
||||||
|
const job = await queue.add(
|
||||||
|
'proofs',
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
// { removeOnComplete: true },
|
||||||
|
)
|
||||||
|
await redis.set(`job:${id}`, job.id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJob(uuid) {
|
||||||
|
const id = await redis.get(`job:${uuid}`)
|
||||||
|
return queue.getJobFromId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJobStatus(uuid) {
|
||||||
|
const job = await getJob(uuid)
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
postJob,
|
||||||
|
getJob,
|
||||||
|
getJobStatus,
|
||||||
|
queue,
|
||||||
|
}
|
34
src/server.js
Normal file
34
src/server.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const status = require('status')
|
||||||
|
const controller = require('controller')
|
||||||
|
const { port } = require('../config')
|
||||||
|
const { version } = require('../package.json')
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(express.json())
|
||||||
|
|
||||||
|
// Log error to console but don't send it to the client to avoid leaking data
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add CORS headers
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.header('Access-Control-Allow-Origin', '*')
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
console.log('Version:', version)
|
||||||
|
app.listen(port)
|
38
src/status.js
Normal file
38
src/status.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const queue = require('queue')
|
||||||
|
|
||||||
|
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 } = fetcher
|
||||||
|
res.json({
|
||||||
|
relayerAddress: web3.eth.defaultAccount,
|
||||||
|
mixers,
|
||||||
|
gasPrices: await gasPriceOracle.gasPrices(),
|
||||||
|
netId,
|
||||||
|
ethPrices,
|
||||||
|
relayerServiceFee,
|
||||||
|
nonce,
|
||||||
|
version,
|
||||||
|
latestBlock
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJob(req, res) {
|
||||||
|
const status = await queue.getJobStatus(req.params.id)
|
||||||
|
return res.send(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
status,
|
||||||
|
index,
|
||||||
|
getJob,
|
||||||
|
}
|
95
src/treeWatcher.js
Normal file
95
src/treeWatcher.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
const MerkleTree = require('fixed-merkle-tree')
|
||||||
|
const { redisUrl, rpcUrl, minerMerkleTreeHeight, minerAddress } = require('../config')
|
||||||
|
const { poseidonHash2 } = require('./utils')
|
||||||
|
const { toBN } = require('web3-utils')
|
||||||
|
const Redis = require('ioredis')
|
||||||
|
const redis = new Redis(redisUrl)
|
||||||
|
const Web3 = require('web3')
|
||||||
|
const web3 = new Web3(rpcUrl)
|
||||||
|
const contract = new web3.eth.Contract(require('../abis/mining.abi.json'), minerAddress)
|
||||||
|
|
||||||
|
let tree, eventSubscription, blockSubscription
|
||||||
|
|
||||||
|
async function fetchEvents(from = 0, to = 'latest') {
|
||||||
|
const events = await contract.getPastEvents('NewAccount', {
|
||||||
|
fromBlock: from,
|
||||||
|
toBlock: to,
|
||||||
|
})
|
||||||
|
return events
|
||||||
|
.sort((a, b) => a.returnValues.index - b.returnValues.index)
|
||||||
|
.map((e) => toBN(e.returnValues.commitment))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processNewEvent(err, event) {
|
||||||
|
if (err) {
|
||||||
|
throw new Error(`Event handler error: ${err}`)
|
||||||
|
// console.error(err)
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('New account event', event.returnValues)
|
||||||
|
const { commitment, index } = event.returnValues
|
||||||
|
if (tree.elements().length === Number(index)) {
|
||||||
|
tree.insert(toBN(commitment))
|
||||||
|
await updateRedis()
|
||||||
|
} else if (tree.elements().length === Number(index) + 1) {
|
||||||
|
console.log('Replacing element', index)
|
||||||
|
tree.update(index, toBN(commitment))
|
||||||
|
await updateRedis()
|
||||||
|
} else {
|
||||||
|
console.log(`Invalid element index ${index}, rebuilding tree`)
|
||||||
|
await rebuild()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processNewBlock(err) {
|
||||||
|
if (err) {
|
||||||
|
throw new Error(`Event handler error: ${err}`)
|
||||||
|
// console.error(err)
|
||||||
|
// return
|
||||||
|
}
|
||||||
|
await updateRedis()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRedis() {
|
||||||
|
const rootOnContract = await contract.methods.getLastAccountRoot().call()
|
||||||
|
if (!tree.root().eq(toBN(rootOnContract))) {
|
||||||
|
console.log(`Invalid tree root: ${tree.root()} != ${toBN(rootOnContract)}, rebuilding tree`)
|
||||||
|
await rebuild()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rootInRedis = await redis.get('tree:root')
|
||||||
|
if (!rootInRedis || !tree.root().eq(toBN(rootInRedis))) {
|
||||||
|
const serializedTree = JSON.stringify(tree.serialize())
|
||||||
|
await redis.set('tree:elements', serializedTree)
|
||||||
|
await redis.set('tree:root', tree.root().toString())
|
||||||
|
await redis.publish('treeUpdate', tree.root().toString())
|
||||||
|
console.log('Updated tree in redis, new root:', tree.root().toString())
|
||||||
|
} else {
|
||||||
|
console.log('Tree in redis is up to date, skipping update')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rebuild() {
|
||||||
|
await eventSubscription.unsubscribe()
|
||||||
|
await blockSubscription.unsubscribe()
|
||||||
|
setTimeout(init, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
console.log('Initializing')
|
||||||
|
const block = await web3.eth.getBlockNumber()
|
||||||
|
const events = await fetchEvents(0, block)
|
||||||
|
tree = new MerkleTree(minerMerkleTreeHeight, events, { hashFunction: poseidonHash2 })
|
||||||
|
console.log(`Rebuilt tree with ${events.length} elements, root: ${tree.root()}`)
|
||||||
|
eventSubscription = contract.events.NewAccount({ fromBlock: block + 1 }, processNewEvent)
|
||||||
|
blockSubscription = web3.eth.subscribe('newBlockHeaders', processNewBlock)
|
||||||
|
await updateRedis()
|
||||||
|
}
|
||||||
|
|
||||||
|
init()
|
||||||
|
|
||||||
|
process.on('unhandledRejection', error => {
|
||||||
|
console.error('Unhandled promise rejection', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
198
src/utils.js
198
src/utils.js
@ -1,178 +1,40 @@
|
|||||||
const { isHexStrict, toBN, toWei, BN } = require('web3-utils')
|
const { instances, netId } = require('../config')
|
||||||
const { netId, mixers, relayerServiceFee } = require('../config')
|
const { poseidon } = require('circomlib')
|
||||||
|
const { toBN } = require('web3-utils')
|
||||||
|
|
||||||
function isValidProof(proof) {
|
function getInstance(address) {
|
||||||
// validator expects `websnarkUtils.toSolidityInput(proof)` output
|
const inst = instances[`netId${netId}`]
|
||||||
|
for (const currency of Object.keys(inst)) {
|
||||||
if (!proof) {
|
for (const amount of Object.keys(inst[currency].instanceAddress)) {
|
||||||
return { valid: false, reason: 'The proof is empty.' }
|
if (inst[currency].instanceAddress[amount] === address) {
|
||||||
|
return { currency, amount }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
|
// async function setSafeInterval(func, interval) {
|
||||||
return { valid: false, reason: 'Corrupted proof' }
|
// try {
|
||||||
}
|
// await func()
|
||||||
|
// } catch (e) {
|
||||||
|
// console.error('Unhandled promise error:', e)
|
||||||
|
// } finally {
|
||||||
|
// setTimeout(() => setSafeInterval(func, interval), interval)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
return { valid: true }
|
const poseidonHash = (items) => toBN(poseidon(items).toString())
|
||||||
}
|
const poseidonHash2 = (a, b) => poseidonHash([a, b])
|
||||||
|
|
||||||
function isValidArgs(args) {
|
function setSafeInterval(func, interval) {
|
||||||
if (!args) {
|
func().catch(console.error).finally(() => {
|
||||||
return { valid: false, reason: 'Args are empty' }
|
setTimeout(() => setSafeInterval(func, interval), interval)
|
||||||
}
|
|
||||||
|
|
||||||
if (args.length !== 6) {
|
|
||||||
return { valid: false, reason: 'Length of args is lower than 6' }
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
return { valid: false, reason: 'The length one of the signals is incorrect' }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
function isKnownContract(contract) {
|
|
||||||
const mixers = getMixers()
|
|
||||||
for (let currency of Object.keys(mixers)) {
|
|
||||||
for (let amount of Object.keys(mixers[currency].mixerAddress)) {
|
|
||||||
if (mixers[currency].mixerAddress[amount] === contract) {
|
|
||||||
return { valid: true, currency, amount }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { valid: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
function sleep(ms) {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromDecimals(value, decimals) {
|
|
||||||
value = value.toString()
|
|
||||||
let ether = value.toString()
|
|
||||||
const base = new BN('10').pow(new BN(decimals))
|
|
||||||
const baseLength = base.toString(10).length - 1 || 1
|
|
||||||
|
|
||||||
const negative = ether.substring(0, 1) === '-'
|
|
||||||
if (negative) {
|
|
||||||
ether = ether.substring(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ether === '.') {
|
|
||||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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')
|
|
||||||
}
|
|
||||||
|
|
||||||
let whole = comps[0]
|
|
||||||
let fraction = comps[1]
|
|
||||||
|
|
||||||
if (!whole) {
|
|
||||||
whole = '0'
|
|
||||||
}
|
|
||||||
if (!fraction) {
|
|
||||||
fraction = '0'
|
|
||||||
}
|
|
||||||
if (fraction.length > baseLength) {
|
|
||||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places')
|
|
||||||
}
|
|
||||||
|
|
||||||
while (fraction.length < baseLength) {
|
|
||||||
fraction += '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
whole = new BN(whole)
|
|
||||||
fraction = new BN(fraction)
|
|
||||||
let wei = whole.mul(base).add(fraction)
|
|
||||||
|
|
||||||
if (negative) {
|
|
||||||
wei = wei.mul(negative)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BN(wei.toString(10), 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 roundDecimal = 10 ** decimalsPoint
|
|
||||||
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) {
|
|
||||||
case 'eth': {
|
|
||||||
desiredFee = expense.add(feePercent)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
if (fee.lt(desiredFee)) {
|
|
||||||
return { isEnough: false, reason: 'Not enough fee' }
|
|
||||||
}
|
|
||||||
return { isEnough: true }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArgsForOracle() {
|
|
||||||
const tokens = mixers['netId1']
|
|
||||||
const tokenAddresses = []
|
|
||||||
const oneUintAmount = []
|
|
||||||
const currencyLookup = {}
|
|
||||||
Object.entries(tokens).map(([currency, data]) => {
|
|
||||||
if (currency !== 'eth') {
|
|
||||||
tokenAddresses.push(data.tokenAddress)
|
|
||||||
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
|
|
||||||
currencyLookup[data.tokenAddress] = currency
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return { tokenAddresses, oneUintAmount, currencyLookup }
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMixers() {
|
|
||||||
return mixers[`netId${netId}`]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
isValidProof,
|
getInstance,
|
||||||
isValidArgs,
|
setSafeInterval,
|
||||||
sleep,
|
poseidonHash2,
|
||||||
isKnownContract,
|
|
||||||
isEnoughFee,
|
|
||||||
getMixers,
|
|
||||||
getArgsForOracle
|
|
||||||
}
|
}
|
||||||
|
83
src/validate.js
Normal file
83
src/validate.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
const { isHexStrict } = require('web3-utils')
|
||||||
|
const { getInstance } = require('./utils')
|
||||||
|
const { rewardAccount } = require('../config')
|
||||||
|
|
||||||
|
function getProofError(proof) {
|
||||||
|
if (!proof) {
|
||||||
|
return 'The proof is empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
|
||||||
|
return 'Corrupted proof'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArgsError(args, expectedLengths) {
|
||||||
|
if (!args) {
|
||||||
|
return 'Args are empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(args)) {
|
||||||
|
return 'Args should be an array'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length !== expectedLengths.length) {
|
||||||
|
return `Expected ${expectedLengths.length} args`
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (!isHexStrict(args[i])) {
|
||||||
|
return `Corrupted signal ${i}: ${args[i]}`
|
||||||
|
}
|
||||||
|
if (args[i].length !== 2 + expectedLengths * 20) {
|
||||||
|
return `Signal ${i} has invalid length: ${args[i]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getContractError(contract) {
|
||||||
|
if (!contract) {
|
||||||
|
return 'The contract is empty'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHexStrict(contract) || contract.length !== 42) {
|
||||||
|
return 'Corrupted contract'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getInstance(contract)) {
|
||||||
|
return `This relayer does not support the token: ${contract}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRewardAddressError(address) {
|
||||||
|
if (address.toLowerCase() !== rewardAccount.toLowerCase()) {
|
||||||
|
return 'This proof is for different relayer'
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWithdrawInputError(input) {
|
||||||
|
return getProofError(input.proof) || getArgsError(input.args, [32, 32, 20, 20, 32, 32]) || getContractError(input.contract) || getRewardAddressError(input.args[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClaimInputError(input) {
|
||||||
|
return getProofError(input.proof) || getArgsError(input.args, [32, 32, 20, 20, 32, 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRewardInputError(input) {
|
||||||
|
return getProofError(input.proof) || getArgsError(input.args, [32, 32, 20, 20, 32, 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getWithdrawInputError,
|
||||||
|
getClaimInputError,
|
||||||
|
getRewardInputError,
|
||||||
|
}
|
102
src/worker.js
Normal file
102
src/worker.js
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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 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)
|
||||||
|
|
||||||
|
let web3
|
||||||
|
let nonce
|
||||||
|
let currentTx
|
||||||
|
let tree
|
||||||
|
|
||||||
|
async function fetchTree() {
|
||||||
|
const elements = await redis.get('tree:elements')
|
||||||
|
const convert = (_, val) => typeof(val) === 'string' ? toBN(val) : val
|
||||||
|
tree = MerkleTree.deserialize(JSON.parse(elements, convert), poseidonHash2)
|
||||||
|
|
||||||
|
if (currentTx) {
|
||||||
|
// todo replace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function watcher() {
|
||||||
|
if (currentTx && Date.now() - currentTx.date > gasBumpInterval) {
|
||||||
|
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 this.sendTx(tx, null, 9999)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
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')
|
||||||
|
await fetchTree()
|
||||||
|
setSafeInterval(watcher, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function checkTornadoFee(contract, fee, refund) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function process(job) {
|
||||||
|
if (job.type !== 'tornadoWithdraw') {
|
||||||
|
throw new Error('not implemented')
|
||||||
|
}
|
||||||
|
console.log(Date.now(), ' withdraw started', job.id)
|
||||||
|
const { proof, args, contract } = job.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()
|
||||||
|
const tx = {
|
||||||
|
from: web3.eth.defaultAccount,
|
||||||
|
value: numberToHex(refund),
|
||||||
|
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
||||||
|
to: contract,
|
||||||
|
netId,
|
||||||
|
data,
|
||||||
|
nonce,
|
||||||
|
}
|
||||||
|
// nonce++ later
|
||||||
|
|
||||||
|
const gas = await web3.eth.estimateGas(tx)
|
||||||
|
tx.gas = gas
|
||||||
|
let signedTx = await this.web3.eth.accounts.signTransaction(tx, privateKey)
|
||||||
|
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
|
||||||
|
|
||||||
|
result.once('transactionHash', async (txHash) => {
|
||||||
|
console.log(`A new successfully sent tx ${txHash}`)
|
||||||
|
job.data.txHash = txHash
|
||||||
|
await job.update(job.data)
|
||||||
|
})
|
||||||
|
|
||||||
|
await result
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await init()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// main()
|
||||||
|
fetchTree()
|
Loading…
Reference in New Issue
Block a user