commit
c760a3e056
@ -1,4 +1,7 @@
|
|||||||
NET_ID=42
|
NET_ID=42
|
||||||
RPC_URL=https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51
|
RPC_URL=https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51
|
||||||
PRIVATE_KEY=
|
PRIVATE_KEY=
|
||||||
MIXER_ADDRESS=0xb2aD997a43768aB9279Cd9E72D5B75D789a09011
|
# 2.5 means 2.5%
|
||||||
|
RELAYER_FEE=2.5
|
||||||
|
|
||||||
|
APP_PORT=8000
|
@ -16,7 +16,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"indent": [
|
||||||
"error",
|
"error",
|
||||||
2
|
2,
|
||||||
|
{"SwitchCase": 1}
|
||||||
],
|
],
|
||||||
"linebreak-style": [
|
"linebreak-style": [
|
||||||
"error",
|
"error",
|
||||||
|
@ -2,10 +2,9 @@ FROM node:11
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm install
|
RUN npm install && npm cache clean --force
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
HEALTHCHECK CMD curl -f http://localhost:8000/
|
HEALTHCHECK CMD curl -f http://localhost:8000/status
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
47
README.md
47
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)
|
# 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
|
## Setup
|
||||||
1. `npm i`
|
1. `npm i`
|
||||||
2. `cp .env.example .env`
|
2. `cp .env.example .env`
|
||||||
3. Modify `.env` as needed
|
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
|
## Run locally
|
||||||
1. `npm run start`
|
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.
|
Relayer should return a transaction hash.
|
||||||
|
|
||||||
## Proof example
|
|
||||||
|
## Input data example
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"pi_a":[
|
"proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
|
||||||
"0x0ed9b1afc791a551f5baa2f84786963b1463ca3f7c68eb0de3b267e6cb491f05",
|
"args": [
|
||||||
"0x1335f2af3c71e442fd82f63f8f1c605ca2612b8d0fa22b4cbd1239cca839aa3d"
|
"0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
|
||||||
],
|
"0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
|
||||||
"pi_b":[
|
"0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
|
||||||
[
|
"0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
|
||||||
"0x000189f7f1067a768d116cd86980eae6963dd9bc6c1f8204ceacf90a94f60d81",
|
"0x000000000000000000000000000000000000000000000000058d15e176280000",
|
||||||
"0x1abb4b71da0efa67cbc76a97ac360826b17a88f07bd89151258bf076474a4804"
|
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
],
|
],
|
||||||
[
|
"contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
|
||||||
"0x0526b509ba2cda2b21b09401d70d23ea0225be4fdaa9097af842ff6783d1e0f4",
|
|
||||||
"0x15b11f9f5441adeea61534105902170a409b228e159fe7428abf6e863fc05273"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"pi_c":[
|
|
||||||
"0x2cd9a2305827f7da64aa1a3136c11ae1d3d7b3cb69832d8c04ab39d8b9393cda",
|
|
||||||
"0x2090cd3f9d09d66ca4e1e9bed2c72d5fa174b47599cb47e572324b1a98a3cb7a"
|
|
||||||
],
|
|
||||||
"publicSignals":[
|
|
||||||
"0x1e8a85160889dfb5c03a8e2a6cca18b4c476c0b486003e9ed666a33e04114658",
|
|
||||||
"0x00bfb0befe19eac571ecaf7858e50d70273fbe2952cc8431f59399bb28665796",
|
|
||||||
"0x00000000000000000000000003ebd0748aa4d1457cf479cce56309641e0a98f5",
|
|
||||||
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
52
config.js
52
config.js
@ -1,10 +1,56 @@
|
|||||||
require('dotenv').config()
|
require('dotenv').config()
|
||||||
|
|
||||||
module.exports = {
|
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',
|
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/v3/a3f4d001c1fc4a359ea70dd27fd9cb51',
|
||||||
privateKey: process.env.PRIVATE_KEY,
|
privateKey: process.env.PRIVATE_KEY,
|
||||||
mixerAddress: process.env.MIXER_ADDRESS,
|
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,
|
defaultGasPrice: 2,
|
||||||
gasOracleUrls: ['https://www.etherchain.org/api/gasPriceOracle', 'https://gasprice.poa.network/']
|
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"
|
|
||||||
}
|
|
||||||
]
|
|
4930
package-lock.json
generated
4930
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",
|
"name": "relay",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Relayer for Tornado mixer. https://tornado.cash",
|
"description": "Relayer for Tornado mixer. https://tornado.cash",
|
||||||
"main": "index.js",
|
"main": "src/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node src/index.js",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"author": "Alexey Pertsev <alexey@peppersec.com> (https://peppersec.com)",
|
"author": "tornado.cash",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^8.0.0",
|
"coingecko-api": "^1.0.6",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"node-fetch": "^2.6.0",
|
"node-fetch": "^2.6.0",
|
||||||
"web3": "^1.0.0-beta.55",
|
"web3": "^1.2.2",
|
||||||
"web3-utils": "^1.0.0"
|
"web3-utils": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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