mirror of
https://github.com/tornadocash/tornado-relayer
synced 2024-02-02 15:04:06 +01:00
commit
c760a3e056
@ -1,4 +1,7 @@
|
||||
NET_ID=42
|
||||
RPC_URL=https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51
|
||||
PRIVATE_KEY=
|
||||
MIXER_ADDRESS=0xb2aD997a43768aB9279Cd9E72D5B75D789a09011
|
||||
# 2.5 means 2.5%
|
||||
RELAYER_FEE=2.5
|
||||
|
||||
APP_PORT=8000
|
@ -16,7 +16,8 @@
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
2,
|
||||
{"SwitchCase": 1}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
|
@ -2,10 +2,9 @@ FROM node:11
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
RUN npm install && npm cache clean --force
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK CMD curl -f http://localhost:8000/
|
||||
HEALTHCHECK CMD curl -f http://localhost:8000/status
|
||||
CMD ["npm", "run", "start"]
|
45
README.md
45
README.md
@ -1,40 +1,35 @@
|
||||
# Relayer for Tornado mixer [![Build Status](https://travis-ci.org/peppersec/tornado-mixer-relayer.svg?branch=master)](https://travis-ci.org/peppersec/tornado-mixer-relayer)
|
||||
|
||||
## Setup
|
||||
1. `npm i`
|
||||
2. `cp .env.example .env`
|
||||
3. Modify `.env` as needed
|
||||
4. If you want to change contracts' addresses go to [config.js](./config.js) file.
|
||||
|
||||
## Deploy Kovan
|
||||
1. `cp .env.example deploy/kovan/.env`
|
||||
2. `cd deploy/kovan`
|
||||
2. Modify `.env` as needed
|
||||
3. `docker-compose -p kovan up -d`
|
||||
|
||||
## Run locally
|
||||
1. `npm run start`
|
||||
2. `curl -X POST -H 'content-type:application/json' --data '<PROOF>' http://127.0.0.1:8000/relay`
|
||||
2. `curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/relay`
|
||||
Relayer should return a transaction hash.
|
||||
|
||||
## Proof example
|
||||
|
||||
## Input data example
|
||||
```json
|
||||
{
|
||||
"pi_a":[
|
||||
"0x0ed9b1afc791a551f5baa2f84786963b1463ca3f7c68eb0de3b267e6cb491f05",
|
||||
"0x1335f2af3c71e442fd82f63f8f1c605ca2612b8d0fa22b4cbd1239cca839aa3d"
|
||||
],
|
||||
"pi_b":[
|
||||
[
|
||||
"0x000189f7f1067a768d116cd86980eae6963dd9bc6c1f8204ceacf90a94f60d81",
|
||||
"0x1abb4b71da0efa67cbc76a97ac360826b17a88f07bd89151258bf076474a4804"
|
||||
],
|
||||
[
|
||||
"0x0526b509ba2cda2b21b09401d70d23ea0225be4fdaa9097af842ff6783d1e0f4",
|
||||
"0x15b11f9f5441adeea61534105902170a409b228e159fe7428abf6e863fc05273"
|
||||
]
|
||||
],
|
||||
"pi_c":[
|
||||
"0x2cd9a2305827f7da64aa1a3136c11ae1d3d7b3cb69832d8c04ab39d8b9393cda",
|
||||
"0x2090cd3f9d09d66ca4e1e9bed2c72d5fa174b47599cb47e572324b1a98a3cb7a"
|
||||
],
|
||||
"publicSignals":[
|
||||
"0x1e8a85160889dfb5c03a8e2a6cca18b4c476c0b486003e9ed666a33e04114658",
|
||||
"0x00bfb0befe19eac571ecaf7858e50d70273fbe2952cc8431f59399bb28665796",
|
||||
"0x00000000000000000000000003ebd0748aa4d1457cf479cce56309641e0a98f5",
|
||||
"proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
|
||||
"args": [
|
||||
"0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
|
||||
"0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
|
||||
"0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
|
||||
"0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
|
||||
"0x000000000000000000000000000000000000000000000000058d15e176280000",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
]
|
||||
],
|
||||
"contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
|
||||
}
|
||||
```
|
||||
|
498
abis/mixerABI.json
Normal file
498
abis/mixerABI.json
Normal file
@ -0,0 +1,498 @@
|
||||
[
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_newOperator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "changeOperator",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "nullifierHashes",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "_proof",
|
||||
"type": "bytes"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_root",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_nullifierHash",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "_recipient",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address payable",
|
||||
"name": "_relayer",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "_fee",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "_refund",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "withdraw",
|
||||
"outputs": [],
|
||||
"payable": true,
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "verifier",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "contract IVerifier",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_left",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_right",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "hashLeftRight",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "pure",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "FIELD_SIZE",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "levels",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "operator",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_root",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "isKnownRoot",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "commitments",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "denomination",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "currentRootIndex",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_newVerifier",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "updateVerifier",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_commitment",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "deposit",
|
||||
"outputs": [],
|
||||
"payable": true,
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getLastRoot",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "roots",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "ROOT_HISTORY_SIZE",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "_nullifierHash",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"name": "isSpent",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "zeros",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "ZERO_VALUE",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "filledSubtrees",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bytes32",
|
||||
"name": "",
|
||||
"type": "bytes32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "nextIndex",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract IVerifier",
|
||||
"name": "_verifier",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "_denomination",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint32",
|
||||
"name": "_merkleTreeHeight",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "bytes32",
|
||||
"name": "commitment",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint32",
|
||||
"name": "leafIndex",
|
||||
"type": "uint32"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "timestamp",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Deposit",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bytes32",
|
||||
"name": "nullifierHash",
|
||||
"type": "bytes32"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "relayer",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "fee",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Withdrawal",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
54
config.js
54
config.js
@ -1,10 +1,56 @@
|
||||
require('dotenv').config()
|
||||
|
||||
module.exports = {
|
||||
netId: process.env.NET_ID || 42,
|
||||
netId: Number(process.env.NET_ID) || 42,
|
||||
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51',
|
||||
privateKey: process.env.PRIVATE_KEY,
|
||||
mixerAddress: process.env.MIXER_ADDRESS,
|
||||
defaultGasPrice: 2,
|
||||
gasOracleUrls: ['https://www.etherchain.org/api/gasPriceOracle', 'https://gasprice.poa.network/']
|
||||
nonce: 0,
|
||||
mixers: {
|
||||
netId1: {
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': undefined,
|
||||
'500': undefined,
|
||||
'1000': undefined,
|
||||
'5000': undefined
|
||||
},
|
||||
tokenAddress: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
decimals: 18
|
||||
},
|
||||
eth: {
|
||||
mixerAddress: {
|
||||
'0.1': undefined,
|
||||
'1': undefined,
|
||||
'10': undefined,
|
||||
'100': undefined
|
||||
},
|
||||
decimals: 18
|
||||
}
|
||||
},
|
||||
netId42: {
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': '0x5D4538D2b07cD8Eb7b93c33B327f3E01A42e68d8',
|
||||
'500': undefined,
|
||||
'1000': undefined,
|
||||
'5000': undefined
|
||||
},
|
||||
tokenAddress: '0x8c158c7e57161dd4d3cb02bf1a3a97fcc78b75fd',
|
||||
decimals: 18
|
||||
},
|
||||
eth: {
|
||||
mixerAddress: {
|
||||
'0.1': '0xB7F60Bf8b969CE4B95Bb50a671860D99478C81Ee',
|
||||
'1': '0x27e94B8cfa33EA2b47E209Ba69804d44642B3545',
|
||||
'10': undefined,
|
||||
'100': undefined
|
||||
},
|
||||
decimals: 18
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultGasPrice: 2,
|
||||
gasOracleUrls: ['https://www.etherchain.org/api/gasPriceOracle', 'https://gasprice.poa.network/'],
|
||||
port: process.env.APP_PORT,
|
||||
relayerServiceFee: Number(process.env.RELAYER_FEE)
|
||||
}
|
18
deploy/kovan/docker-compose.yml
Normal file
18
deploy/kovan/docker-compose.yml
Normal file
@ -0,0 +1,18 @@
|
||||
version: '2.2'
|
||||
|
||||
services:
|
||||
relayer:
|
||||
build: ../../
|
||||
restart: always
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
VIRTUAL_HOST: kovan.tornado.cash
|
||||
LETSENCRYPT_HOST: kovan.tornado.cash
|
||||
env_file: ./.env
|
||||
healthcheck:
|
||||
test: curl -sS http://127.0.0.1:8000 || exit 1
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: frontend_default
|
18
deploy/mainnet/docker-compose.yml
Normal file
18
deploy/mainnet/docker-compose.yml
Normal file
@ -0,0 +1,18 @@
|
||||
version: '2.2'
|
||||
|
||||
services:
|
||||
relayer:
|
||||
build: ../../
|
||||
restart: always
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
VIRTUAL_HOST: mainnet.tornado.cash
|
||||
LETSENCRYPT_HOST: mainnet.tornado.cash
|
||||
env_file: ./.env
|
||||
healthcheck:
|
||||
test: curl -sS http://127.0.0.1:8000 || exit 1
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: frontend_default
|
@ -1,23 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
relayer:
|
||||
build: ./
|
||||
restart: always
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
VIRTUAL_HOST: relayer.tornado.cash
|
||||
LETSENCRYPT_HOST: relayer.tornado.cash
|
||||
env_file: ./.env
|
||||
|
||||
monitor:
|
||||
image: arefaslani/docker-telegram-notifier
|
||||
restart: always
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env_file: ./.env
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: frontend_default
|
@ -1,23 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
nginx:
|
||||
image: jwilder/nginx-proxy
|
||||
restart: always
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
- /etc/nginx/certs
|
||||
- /etc/nginx/vhost.d
|
||||
- /usr/share/nginx/html
|
||||
|
||||
letsencrypt:
|
||||
image: jrcs/letsencrypt-nginx-proxy-companion
|
||||
restart: always
|
||||
volumes_from:
|
||||
- nginx
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
90
index.js
90
index.js
@ -1,90 +0,0 @@
|
||||
const { numberToHex, toWei, toHex, toBN } = require('web3-utils')
|
||||
const Web3 = require('web3')
|
||||
const express = require('express')
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
if (err) {
|
||||
console.log('Invalid Request data')
|
||||
res.send('Invalid Request data')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
|
||||
next()
|
||||
})
|
||||
|
||||
const { netId, rpcUrl, privateKey, mixerAddress, defaultGasPrice } = require('./config')
|
||||
const { fetchGasPrice, isValidProof } = require('./utils')
|
||||
|
||||
const 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
|
||||
|
||||
const mixerABI = require('./mixerABI.json')
|
||||
const mixer = new web3.eth.Contract(mixerABI, mixerAddress)
|
||||
const gasPrices = { fast: defaultGasPrice }
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
// just for testing purposes
|
||||
res.send(`Tornado mixer relayer. Gas Price is ${JSON.stringify(gasPrices)}. Mixer address is ${mixerAddress}`)
|
||||
})
|
||||
|
||||
app.post('/relay', async (req, resp) => {
|
||||
const { valid , reason } = isValidProof(req.body)
|
||||
if (!valid) {
|
||||
console.log('Proof is invalid:', reason)
|
||||
return resp.status(400).json({ error: 'Proof is invalid' })
|
||||
}
|
||||
|
||||
let { pi_a, pi_b, pi_c, publicSignals } = req.body
|
||||
|
||||
const fee = toBN(publicSignals[3])
|
||||
const desiredFee = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN('1000000'))
|
||||
if (fee.lt(desiredFee)) {
|
||||
console.log('Fee is too low')
|
||||
return resp.status(400).json({ error: 'Fee is too low. Try to resend.' })
|
||||
}
|
||||
|
||||
try {
|
||||
const nullifier = publicSignals[1]
|
||||
const isSpent = await mixer.methods.isSpent(nullifier).call()
|
||||
if (isSpent) {
|
||||
return resp.status(400).json({ error: 'The note has been spent.' })
|
||||
}
|
||||
const root = publicSignals[0]
|
||||
const isKnownRoot = await mixer.methods.isKnownRoot(root).call()
|
||||
if (!isKnownRoot) {
|
||||
return resp.status(400).json({ error: 'The merkle root is too old or invalid.' })
|
||||
}
|
||||
const gas = await mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).estimateGas()
|
||||
const result = mixer.methods.withdraw(pi_a, pi_b, pi_c, publicSignals).send({
|
||||
gas: numberToHex(gas + 50000),
|
||||
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
||||
// TODO: nonce
|
||||
})
|
||||
result.once('transactionHash', function(hash){
|
||||
resp.json({ txHash: hash })
|
||||
}).on('error', function(e){
|
||||
console.log(e)
|
||||
return resp.status(400).json({ error: 'Proof is malformed.' })
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return resp.status(400).json({ error: 'Proof is malformed or spent.' })
|
||||
}
|
||||
})
|
||||
|
||||
app.listen(8000)
|
||||
|
||||
if (Number(netId) === 1) {
|
||||
fetchGasPrice({ gasPrices })
|
||||
console.log('Gas price oracle started.')
|
||||
}
|
320
mixerABI.json
320
mixerABI.json
@ -1,320 +0,0 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "filled_subtrees",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "nullifierHashes",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "transferValue",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "roots",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "commitments",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "zeros",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "levels",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "left",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "right",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "hashLeftRight",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "mimc_hash",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "pure",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "next_index",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint32"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "current_root",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "root",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "isKnownRoot",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getLastRoot",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_verifier",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_transferValue",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "_merkleTreeHeight",
|
||||
"type": "uint8"
|
||||
},
|
||||
{
|
||||
"name": "_emptyElement",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "commitment",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "leafIndex",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "timestamp",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Deposit",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "nullifierHash",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "fee",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Withdraw",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "commitment",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "deposit",
|
||||
"outputs": [],
|
||||
"payable": true,
|
||||
"stateMutability": "payable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "a",
|
||||
"type": "uint256[2]"
|
||||
},
|
||||
{
|
||||
"name": "b",
|
||||
"type": "uint256[2][2]"
|
||||
},
|
||||
{
|
||||
"name": "c",
|
||||
"type": "uint256[2]"
|
||||
},
|
||||
{
|
||||
"name": "input",
|
||||
"type": "uint256[4]"
|
||||
}
|
||||
],
|
||||
"name": "withdraw",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "nullifier",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "isSpent",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
4900
package-lock.json
generated
4900
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@ -2,22 +2,23 @@
|
||||
"name": "relay",
|
||||
"version": "1.0.0",
|
||||
"description": "Relayer for Tornado mixer. https://tornado.cash",
|
||||
"main": "index.js",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"start": "node src/index.js",
|
||||
"eslint": "npx eslint --ignore-path .gitignore .",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
|
||||
"author": "tornado.cash",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^8.0.0",
|
||||
"coingecko-api": "^1.0.6",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"web3": "^1.0.0-beta.55",
|
||||
"web3-utils": "^1.0.0"
|
||||
"web3": "^1.2.2",
|
||||
"web3-utils": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^6.0.1"
|
||||
"eslint": "^6.6.0"
|
||||
}
|
||||
}
|
||||
|
77
src/Fetcher.js
Normal file
77
src/Fetcher.js
Normal file
@ -0,0 +1,77 @@
|
||||
const CoinGecko = require('coingecko-api')
|
||||
const fetch = require('node-fetch')
|
||||
const { toWei } = require('web3-utils')
|
||||
const { gasOracleUrls, defaultGasPrice } = require('../config')
|
||||
const { getMainnetTokens } = require('./utils')
|
||||
const config = require ('../config')
|
||||
|
||||
|
||||
class Fetcher {
|
||||
constructor(web3) {
|
||||
this.web3 = web3
|
||||
this.ethPrices = {
|
||||
dai: '6700000000000000' // 0.0067
|
||||
}
|
||||
this.gasPrices = {
|
||||
fast: defaultGasPrice
|
||||
}
|
||||
}
|
||||
async fetchPrices() {
|
||||
const { tokenAddresses, currencyLookup } = getMainnetTokens()
|
||||
try {
|
||||
const CoinGeckoClient = new CoinGecko()
|
||||
const price = await CoinGeckoClient.simple.fetchTokenPrice({
|
||||
contract_addresses: tokenAddresses,
|
||||
vs_currencies: 'eth',
|
||||
assetPlatform: 'ethereum'
|
||||
})
|
||||
this.ethPrices = Object.entries(price.data).reduce((acc, token) => {
|
||||
if (token[1].eth) {
|
||||
acc[currencyLookup[token[0]]] = toWei(token[1].eth.toString())
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
setTimeout(() => this.fetchPrices(), 1000 * 30)
|
||||
} catch(e) {
|
||||
setTimeout(() => this.fetchPrices(), 1000 * 30)
|
||||
}
|
||||
}
|
||||
async fetchGasPrice({ oracleIndex = 0 } = {}) {
|
||||
oracleIndex = (oracleIndex + 1) % gasOracleUrls.length
|
||||
try {
|
||||
const response = await fetch(gasOracleUrls[oracleIndex])
|
||||
if (response.status === 200) {
|
||||
const json = await response.json()
|
||||
|
||||
if (json.slow) {
|
||||
this.gasPrices.low = Number(json.slow)
|
||||
}
|
||||
if (json.safeLow) {
|
||||
this.gasPrices.low = Number(json.safeLow)
|
||||
}
|
||||
if (json.standard) {
|
||||
this.gasPrices.standard = Number(json.standard)
|
||||
}
|
||||
if (json.fast) {
|
||||
this.gasPrices.fast = Number(json.fast)
|
||||
}
|
||||
} else {
|
||||
throw Error('Fetch gasPrice failed')
|
||||
}
|
||||
setTimeout(() => this.fetchGasPrice({ oracleIndex }), 15000)
|
||||
} catch (e) {
|
||||
setTimeout(() => this.fetchGasPrice({ oracleIndex }), 15000)
|
||||
}
|
||||
}
|
||||
async fetchNonce() {
|
||||
try {
|
||||
config.nonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
|
||||
console.log(`Current nonce: ${config.nonce}`)
|
||||
} catch(e) {
|
||||
console.error('fetchNonce failed', e.message)
|
||||
setTimeout(this.fetchNonce, 3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Fetcher
|
50
src/index.js
Normal file
50
src/index.js
Normal file
@ -0,0 +1,50 @@
|
||||
const express = require('express')
|
||||
const { netId, port, relayerServiceFee } = require('../config')
|
||||
const relayController = require('./relayController')
|
||||
const { fetcher, web3 } = require('./instances')
|
||||
const { getMixers } = require('./utils')
|
||||
const mixers = getMixers()
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
if (err) {
|
||||
console.log('Invalid Request data')
|
||||
res.send('Invalid Request data')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
res.header('Access-Control-Allow-Origin', '*')
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
|
||||
next()
|
||||
})
|
||||
|
||||
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 /status for settings')
|
||||
})
|
||||
|
||||
app.get('/status', function (req, res) {
|
||||
const { ethPrices, gasPrices } = fetcher
|
||||
res.json({ relayerAddress: web3.eth.defaultAccount, mixers, gasPrices, netId, ethPrices, relayerServiceFee })
|
||||
})
|
||||
|
||||
app.post('/relay', relayController)
|
||||
|
||||
app.listen(port || 8000)
|
||||
console.log('Gas price oracle started.')
|
||||
fetcher.fetchGasPrice()
|
||||
fetcher.fetchPrices()
|
||||
fetcher.fetchNonce()
|
||||
|
||||
console.log('Relayer started on port', port || 8000)
|
||||
console.log(`relayerAddress: ${web3.eth.defaultAccount}`)
|
||||
console.log(`mixers: ${JSON.stringify(mixers)}`)
|
||||
console.log(`gasPrices: ${JSON.stringify(fetcher.gasPrices)}`)
|
||||
console.log(`netId: ${netId}`)
|
||||
console.log(`ethPrices: ${JSON.stringify(fetcher.ethPrices)}`)
|
||||
console.log(`Service fee: ${relayerServiceFee}%`)
|
8
src/instances.js
Normal file
8
src/instances.js
Normal file
@ -0,0 +1,8 @@
|
||||
const Fetcher = require('./Fetcher')
|
||||
const web3 = require('./setupWeb3')
|
||||
const fetcher = new Fetcher(web3)
|
||||
|
||||
module.exports = {
|
||||
fetcher,
|
||||
web3
|
||||
}
|
103
src/relayController.js
Normal file
103
src/relayController.js
Normal file
@ -0,0 +1,103 @@
|
||||
const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils')
|
||||
const mixerABI = require('../abis/mixerABI.json')
|
||||
const {
|
||||
isValidProof, isValidArgs, isKnownContract, isEnoughFee
|
||||
} = require('./utils')
|
||||
const config = require('../config')
|
||||
|
||||
const { web3, fetcher } = require('./instances')
|
||||
|
||||
async function relay (req, resp) {
|
||||
const { proof, args, contract } = req.body
|
||||
const gasPrices = fetcher.gasPrices
|
||||
let { valid , reason } = isValidProof(proof)
|
||||
if (!valid) {
|
||||
console.log('Proof is invalid:', reason)
|
||||
return resp.status(400).json({ error: 'Proof format is invalid' })
|
||||
}
|
||||
|
||||
({ valid , reason } = isValidArgs(args))
|
||||
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))
|
||||
if (!valid) {
|
||||
console.log('Contract does not exist:', contract)
|
||||
return resp.status(400).json({ error: 'This relayer does not support the token' })
|
||||
}
|
||||
|
||||
const [ root, nullifierHash, recipient, relayer, fee, refund ] = [
|
||||
args[0],
|
||||
args[1],
|
||||
toChecksumAddress(args[2]),
|
||||
toChecksumAddress(args[3]),
|
||||
toBN(args[4]),
|
||||
toBN(args[5])
|
||||
]
|
||||
console.log('fee, refund', fee.toString(), refund.toString())
|
||||
if (currency === 'eth' && !refund.isZero()) {
|
||||
return resp.status(400).json({ error: 'Cannot send refund for eth currency.' })
|
||||
}
|
||||
|
||||
if (relayer !== web3.eth.defaultAccount) {
|
||||
console.log('This proof is for different relayer:', relayer)
|
||||
return resp.status(400).json({ error: 'Relayer address is invalid' })
|
||||
}
|
||||
|
||||
try {
|
||||
const mixer = new web3.eth.Contract(mixerABI, req.body.contract)
|
||||
const isSpent = await mixer.methods.isSpent(nullifierHash).call()
|
||||
if (isSpent) {
|
||||
return resp.status(400).json({ error: 'The note has been spent.' })
|
||||
}
|
||||
const isKnownRoot = await mixer.methods.isKnownRoot(root).call()
|
||||
if (!isKnownRoot) {
|
||||
return resp.status(400).json({ error: 'The merkle root is too old or invalid.' })
|
||||
}
|
||||
|
||||
let gas = await mixer.methods.withdraw(proof, ...args).estimateGas({
|
||||
from: web3.eth.defaultAccount,
|
||||
value: refund
|
||||
})
|
||||
|
||||
gas += 50000
|
||||
const ethPrices = fetcher.ethPrices
|
||||
const { isEnough, reason } = isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee })
|
||||
if (!isEnough) {
|
||||
console.log(`Wrong fee: ${reason}`)
|
||||
return resp.status(400).json({ error: reason })
|
||||
}
|
||||
|
||||
const data = mixer.methods.withdraw(proof, ...args).encodeABI()
|
||||
const tx = {
|
||||
from: web3.eth.defaultAccount,
|
||||
value: numberToHex(refund),
|
||||
gas: numberToHex(gas),
|
||||
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
||||
to: mixer._address,
|
||||
netId: config.netId,
|
||||
data,
|
||||
nonce: config.nonce
|
||||
}
|
||||
config.nonce++
|
||||
let signedTx = await web3.eth.accounts.signTransaction(tx, config.privateKey)
|
||||
let result = web3.eth.sendSignedTransaction(signedTx.rawTransaction)
|
||||
|
||||
result.once('transactionHash', function(txHash){
|
||||
resp.json({ txHash })
|
||||
console.log(`A new successfully sent tx ${txHash} for the ${recipient}`)
|
||||
}).on('error', function(e){
|
||||
config.nonce--
|
||||
console.error('on transactionHash error', e.message)
|
||||
return resp.status(400).json({ error: 'Proof is malformed.' })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e, 'estimate gas failed')
|
||||
return resp.status(400).json({ error: 'Proof is malformed or spent.' })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = relay
|
16
src/setupWeb3.js
Normal file
16
src/setupWeb3.js
Normal file
@ -0,0 +1,16 @@
|
||||
const Web3 = require('web3')
|
||||
const { rpcUrl, privateKey } = require('../config')
|
||||
|
||||
function setup() {
|
||||
try {
|
||||
const 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
|
||||
return web3
|
||||
} catch(e) {
|
||||
console.error('web3 failed')
|
||||
}
|
||||
}
|
||||
const web3 = setup()
|
||||
module.exports = web3
|
105
src/utils.js
Normal file
105
src/utils.js
Normal file
@ -0,0 +1,105 @@
|
||||
const { isHexStrict, toBN, toWei } = 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 isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
|
||||
// TODO tokens can have less then 18 decimals
|
||||
const feePercent = toBN(toWei(amount)).mul(toBN(relayerServiceFee * 10)).div(toBN('1000'))
|
||||
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
|
||||
let desiredFee
|
||||
switch (currency) {
|
||||
case 'eth': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
case 'dai': {
|
||||
desiredFee =
|
||||
expense.add(refund)
|
||||
.mul(toBN(10 ** 18))
|
||||
.div(toBN(ethPrices.dai))
|
||||
desiredFee = desiredFee.add(feePercent)
|
||||
break
|
||||
}
|
||||
}
|
||||
console.log('desired fee, feePercent', desiredFee.toString(), feePercent.toString())
|
||||
if (fee.lt(desiredFee)) {
|
||||
return { isEnough: false, reason: 'Not enough fee' }
|
||||
}
|
||||
return { isEnough: true }
|
||||
}
|
||||
|
||||
function getMainnetTokens() {
|
||||
const tokens = mixers['netId1']
|
||||
const tokenAddresses = []
|
||||
const currencyLookup = {}
|
||||
Object.entries(tokens).map(([currency, data]) => {
|
||||
if (currency !== 'eth') {
|
||||
tokenAddresses.push(data.tokenAddress)
|
||||
currencyLookup[data.tokenAddress] = currency
|
||||
}
|
||||
})
|
||||
return { tokenAddresses, currencyLookup }
|
||||
}
|
||||
|
||||
function getMixers() {
|
||||
return mixers[`netId${netId}`]
|
||||
}
|
||||
|
||||
module.exports = { isValidProof, isValidArgs, sleep, isKnownContract, isEnoughFee, getMixers, getMainnetTokens }
|
85
utils.js
85
utils.js
@ -1,85 +0,0 @@
|
||||
const fetch = require('node-fetch')
|
||||
const { isHexStrict } = require('web3-utils')
|
||||
const { gasOracleUrls } = require('./config')
|
||||
|
||||
async function fetchGasPrice({ gasPrices, oracleIndex = 0 }) {
|
||||
oracleIndex = (oracleIndex + 1) % gasOracleUrls.length
|
||||
try {
|
||||
const response = await fetch(gasOracleUrls[oracleIndex])
|
||||
if (response.status === 200) {
|
||||
const json = await response.json()
|
||||
|
||||
if (json.slow) {
|
||||
gasPrices.low = Number(json.slow)
|
||||
}
|
||||
if (json.safeLow) {
|
||||
gasPrices.low = Number(json.safeLow)
|
||||
}
|
||||
if (json.standard) {
|
||||
gasPrices.standard = Number(json.standard)
|
||||
}
|
||||
if (json.fast) {
|
||||
gasPrices.fast = Number(json.fast)
|
||||
}
|
||||
} else {
|
||||
throw Error('Fetch gasPrice failed')
|
||||
}
|
||||
setTimeout(() => fetchGasPrice({ gasPrices, oracleIndex }), 15000)
|
||||
} catch (e) {
|
||||
setTimeout(() => fetchGasPrice({ gasPrices, oracleIndex }), 15000)
|
||||
}
|
||||
}
|
||||
|
||||
function isValidProof(proof) {
|
||||
// validator expects `websnarkUtils.toSolidityInput(proof)` output
|
||||
|
||||
if (!(proof.pi_a && proof.pi_b && proof.pi_c && proof.publicSignals)) {
|
||||
return { valid: false, reason: 'One of inputs is empty. There must be pi_a, pi_b, pi_c and publicSignals' }
|
||||
}
|
||||
|
||||
Object.keys(proof).forEach(key => {
|
||||
if (!Array.isArray(proof[key])) {
|
||||
return { valid: false, reason: `Corrupted ${key}` }
|
||||
}
|
||||
if (key === 'pi_b') {
|
||||
if (!Array.isArray(proof[key][0]) || !Array.isArray(proof[key][1])) {
|
||||
return { valid: false, reason: `Corrupted ${key}` }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (proof.pi_a.length !== 2) {
|
||||
return { valid: false, reason: 'Corrupted pi_a' }
|
||||
}
|
||||
|
||||
if (proof.pi_b.length !== 2 || proof.pi_b[0].length !== 2 || proof.pi_b[1].length !== 2) {
|
||||
return { valid: false, reason: 'Corrupted pi_b' }
|
||||
}
|
||||
|
||||
if (proof.pi_c.length !== 2) {
|
||||
return { valid: false, reason: 'Corrupted pi_c' }
|
||||
}
|
||||
|
||||
if (proof.publicSignals.length !== 4) {
|
||||
return { valid: false, reason: 'Corrupted publicSignals' }
|
||||
}
|
||||
|
||||
for (let [key, input] of Object.entries(proof)) {
|
||||
if (key === 'pi_b') {
|
||||
input = input[0].concat(input[1])
|
||||
}
|
||||
|
||||
for (let i = 0; i < input.length; i++ ) {
|
||||
if (!isHexStrict(input[i]) || input[i].length !== 66) {
|
||||
return { valid: false, reason: `Corrupted ${key}` }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
module.exports = { fetchGasPrice, isValidProof, sleep }
|
Loading…
Reference in New Issue
Block a user