Compare commits
No commits in common. "main" and "archive" have entirely different histories.
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
|
||||
.env
|
||||
.env*
|
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@ -0,0 +1,10 @@
|
||||
FROM node:12
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install && npm cache clean --force
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK CMD curl -f http://localhost:8000/status
|
||||
CMD ["npm", "run", "start"]
|
85
README.md
85
README.md
@ -1,60 +1,61 @@
|
||||
# Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions)![Sidechains version](https://img.shields.io/badge/version-5.2.1-blue?logo=docker)![Mainnet version](https://img.shields.io/badge/version-4.1.5-blue?logo=docker)
|
||||
# Relayer for Tornado Cash [![Build Status](https://github.com/tornadocash/relayer/workflows/build/badge.svg)](https://github.com/tornadocash/relayer/actions) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/tornadocash/relayer?logo=docker&logoColor=%23FFFFFF&sort=semver)](https://hub.docker.com/repository/docker/tornadocash/relayer)
|
||||
|
||||
**\*Tornado Cash was sanctioned by the US Treasury on 08/08/2022, this makes it illegal for US citizens to interact with Tornado Cash and all of it's associated deployed smart contracts. Please understand the laws where you live and take all necessary steps to protect and anonymize yourself.**
|
||||
## Run locally
|
||||
|
||||
**\*It is recommended to run your Relayer on a VPS instnace (Virtual Private Server). Ensure SSH configuration is enabled for security, you can find information about SSH keygen and management [here](https://www.ssh.com/academy/ssh/keygen).**
|
||||
|
||||
## Deploy with script and docker-compose
|
||||
|
||||
_The following instructions are for Ubuntu 22.10, other operating systems may vary._
|
||||
|
||||
#### Installation:
|
||||
|
||||
Just run in terminal:
|
||||
1. `npm i`
|
||||
2. `cp .env.example .env`
|
||||
3. Modify `.env` as needed
|
||||
4. `npm run start`
|
||||
5. Go to `http://127.0.0.1:8000`
|
||||
6. In order to execute withdraw request, you can run following command
|
||||
|
||||
```bash
|
||||
curl -s https://git.tornado.ws/tornadocash/tornado-relayer/raw/branch/main/install.sh | bash
|
||||
curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/relay
|
||||
```
|
||||
|
||||
#### Configuring environments:
|
||||
Relayer should return a transaction hash.
|
||||
|
||||
1. Go to `tornado-relayer` folder on the server home directory
|
||||
2. Check environment files:
|
||||
_Note._ If you want to change contracts' addresses go to [config.js](./config.js) file.
|
||||
|
||||
By default each network is preconfigured the naming of `.env.<NETWORK>`
|
||||
## Deploy with docker-compose
|
||||
|
||||
- `.env.eth` for Ethereum Mainnet
|
||||
- `.env.bsc` for Binance Smart Chain
|
||||
- `.env.arb` for Arbitrum
|
||||
- `.env.op` for Optimism
|
||||
- `.env.gnosis` for Gnosis (xdai)
|
||||
- `.env.polygon` for Polygon (matic)
|
||||
- `.env.avax` for Avalanche C-Chain
|
||||
docker-compose.yml contains a stack that will automatically provision SSL certificates for your domain name and will add a https redirect to port 80.
|
||||
|
||||
3. Configure (fill) environment files for those networks on which the relayer will be deployed:
|
||||
1. Download docker-compose.yml
|
||||
2. Change environment variables for `kovan` containers as appropriate
|
||||
- add `PRIVATE_KEY` for your relayer address (without 0x prefix)
|
||||
- set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` to your domain and add DNS record pointing to your relayer ip address
|
||||
- customize `RELAYER_FEE`
|
||||
- update `RPC_URL` if needed
|
||||
- update `REDIS_URL` if needed
|
||||
3. Run `docker-compose up -d`
|
||||
|
||||
- Set `PRIVATE_KEY` to your relayer address (remove the 0x from your private key) to each environment file
|
||||
- _It is recommended not to reuse the same private keys for each network as a security measure_
|
||||
- Set `VIRTUAL_HOST` and `LETSENCRYPT_HOST` a unique subndomain for every network to each environment file
|
||||
- eg: `mainnet.example.com` for Ethereum, `binance.example.com` for Binance etc
|
||||
- add a A wildcard record DNS record with the value assigned to your instance IP address to configure subdomains
|
||||
- Set `RELAYER_FEE` to what you would like to charge as your fee (remember 0.3% is deducted from your staked relayer balance)
|
||||
- Set `RPC_URL` to a non-censoring RPC (You can [run your own](https://github.com/feshchenkod/rpc-nodes), or use a [free option](https://chainnodes.org/))
|
||||
- Set `ORACLE_RPC_URL` to an Ethereum native RPC endpoint
|
||||
## Run as a Docker container
|
||||
|
||||
4(Optional). If you want to run relayer for [Nova](https://nova.tornado.ws), fill `.env.nova` file by instructions in [Nova branch](https://git.tornado.ws/tornadocash/tornado-relayer/src/branch/nova), because config is very specific
|
||||
1. `cp .env.example .env`
|
||||
2. Modify `.env` as needed
|
||||
3. `docker run -d --env-file .env -p 80:8000 tornadocash/relayer`
|
||||
|
||||
#### Deployment:
|
||||
In that case you will need to add https termination yourself because browsers with default settings will prevent https
|
||||
tornado.cash UI from submitting your request over http connection
|
||||
|
||||
1. Build and deploy the docker source for the configured networks specified via `--profile <NETWORK_SYMBOL>`, for example (if you run relayer only for Ethereum Mainnet, Binance Smart Chain and Arbitrum):
|
||||
## Input data example
|
||||
|
||||
- `docker-compose --profile eth --profile bsc --profile arb up -d`
|
||||
```json
|
||||
{
|
||||
"proof": "0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6",
|
||||
"args": [
|
||||
"0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3",
|
||||
"0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d",
|
||||
"0x03ebd0748aa4d1457cf479cce56309641e0a98f5",
|
||||
"0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5",
|
||||
"0x000000000000000000000000000000000000000000000000058d15e176280000",
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
],
|
||||
"contract": "0xA27E34Ad97F171846bAf21399c370c9CE6129e0D"
|
||||
}
|
||||
```
|
||||
|
||||
2. Visit your domain addresses and check each `/status` endpoint to ensure there is no errors in the `status` fields
|
||||
2. Optional: if you want to run Nova relayer, just add `--profile nova` to docker-compose command
|
||||
|
||||
If you want to change some relayer parameters, for example, RPC url or fee percent, stop the relayer software with command `docker-compose down --remove-orphans`, change in corresponding `.env.{name}` file what you need and rerun relayer as described above.
|
||||
|
||||
#### Disclaimer:
|
||||
Disclaimer:
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
28
abis/PriceOracle.abi.json
Normal file
28
abis/PriceOracle.abi.json
Normal file
@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "contract IERC20[]",
|
||||
"name": "fromTokens",
|
||||
"type": "address[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "oneUnitAmounts",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"name": "getPricesInETH",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "prices",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
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"
|
||||
}
|
||||
]
|
1
app.js
Normal file
1
app.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./src/index')
|
153
config.js
Normal file
153
config.js
Normal file
@ -0,0 +1,153 @@
|
||||
require('dotenv').config()
|
||||
|
||||
module.exports = {
|
||||
netId: Number(process.env.NET_ID) || 42,
|
||||
redisUrl: process.env.REDIS_URL,
|
||||
rpcUrl: process.env.RPC_URL || 'https://kovan.infura.io/',
|
||||
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://mainnet.infura.io/',
|
||||
oracleAddress: '0xA2b8E7ee7c8a18ea561A5CF7C9C365592026E374',
|
||||
privateKey: process.env.PRIVATE_KEY,
|
||||
mixers: {
|
||||
netId1: {
|
||||
eth: {
|
||||
mixerAddress: {
|
||||
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
|
||||
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
|
||||
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
|
||||
'100': '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291'
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
||||
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
|
||||
'10000': '0xF60dD140cFf0706bAE9Cd734Ac3ae76AD9eBC32A',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
|
||||
symbol: 'DAI',
|
||||
decimals: 18
|
||||
},
|
||||
cdai: {
|
||||
mixerAddress: {
|
||||
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
|
||||
'50000': '0xBA214C1c1928a32Bffe790263E38B4Af9bFCD659',
|
||||
'500000': '0xb1C8094B234DcE6e03f10a5b673c1d8C69739A00',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8
|
||||
},
|
||||
usdc: {
|
||||
mixerAddress: {
|
||||
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
|
||||
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
|
||||
'10000': '0xD691F27f38B395864Ea86CfC7253969B409c362d',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||
symbol: 'USDC',
|
||||
decimals: 6
|
||||
},
|
||||
cusdc: {
|
||||
mixerAddress: {
|
||||
'5000': '0xaEaaC358560e11f52454D997AAFF2c5731B6f8a6',
|
||||
'50000': '0x1356c899D8C9467C7f71C195612F8A395aBf2f0a',
|
||||
'500000': '0xA60C772958a3eD56c1F15dD055bA37AC8e523a0D',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0x39AA39c021dfbaE8faC545936693aC917d5E7563',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8
|
||||
},
|
||||
usdt: {
|
||||
mixerAddress: {
|
||||
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
|
||||
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
|
||||
'10000': '0xF67721A2D8F736E75a49FdD7FAd2e31D8676542a',
|
||||
'100000': '0x9AD122c22B14202B4490eDAf288FDb3C7cb3ff5E'
|
||||
},
|
||||
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||
symbol: 'USDT',
|
||||
decimals: 6
|
||||
}
|
||||
},
|
||||
netId42: {
|
||||
eth: {
|
||||
mixerAddress: {
|
||||
'0.1': '0x8b3f5393bA08c24cc7ff5A66a832562aAB7bC95f',
|
||||
'1': '0xD6a6AC46d02253c938B96D12BE439F570227aE8E',
|
||||
'10': '0xe1BE96331391E519471100c3c1528B66B8F4e5a7',
|
||||
'100': '0xd037E0Ac98Dab2fCb7E296c69C6e52767Ae5414D'
|
||||
},
|
||||
symbol: 'ETH',
|
||||
decimals: 18
|
||||
},
|
||||
dai: {
|
||||
mixerAddress: {
|
||||
'100': '0xdf2d3cC5F361CF95b3f62c4bB66deFe3FDE47e3D',
|
||||
'1000': '0xD96291dFa35d180a71964D0894a1Ae54247C4ccD',
|
||||
'10000': '0xb192794f72EA45e33C3DF6fe212B9c18f6F45AE3',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0x4F96Fe3b7A6Cf9725f59d353F723c1bDb64CA6Aa',
|
||||
symbol: 'DAI',
|
||||
decimals: 18
|
||||
},
|
||||
cdai: {
|
||||
mixerAddress: {
|
||||
'5000': '0x6Fc9386ABAf83147b3a89C36D422c625F44121C8',
|
||||
'50000': '0x7182EA067e0f050997444FCb065985Fd677C16b6',
|
||||
'500000': '0xC22ceFd90fbd1FdEeE554AE6Cc671179BC3b10Ae',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0xe7bc397DBd069fC7d0109C0636d06888bb50668c',
|
||||
symbol: 'cDAI',
|
||||
decimals: 8
|
||||
},
|
||||
usdc: {
|
||||
mixerAddress: {
|
||||
'100': '0x137E2B6d185018e7f09f6cf175a970e7fC73826C',
|
||||
'1000': '0xcC7f1633A5068E86E3830e692e3e3f8f520525Af',
|
||||
'10000': '0x28C8f149a0ab8A9bdB006B8F984fFFCCE52ef5EF',
|
||||
'100000': undefined
|
||||
},
|
||||
tokenAddress: '0x75B0622Cec14130172EaE9Cf166B92E5C112FaFF',
|
||||
symbol: 'USDC',
|
||||
decimals: 6
|
||||
},
|
||||
cusdc: {
|
||||
mixerAddress: {
|
||||
'5000': '0xc0648F28ABA385c8a1421Bbf1B59e3c474F89cB0',
|
||||
'50000': '0x0C53853379c6b1A7B74E0A324AcbDD5Eabd4981D',
|
||||
'500000': '0xf84016A0E03917cBe700D318EB1b7a53e6e3dEe1',
|
||||
'5000000': undefined
|
||||
},
|
||||
tokenAddress: '0xcfC9bB230F00bFFDB560fCe2428b4E05F3442E35',
|
||||
symbol: 'cUSDC',
|
||||
decimals: 8
|
||||
},
|
||||
usdt: {
|
||||
mixerAddress: {
|
||||
'100': '0x327853Da7916a6A0935563FB1919A48843036b42',
|
||||
'1000': '0x531AA4DF5858EA1d0031Dad16e3274609DE5AcC0',
|
||||
'10000': '0x0958275F0362cf6f07D21373aEE0cf37dFe415dD',
|
||||
'100000': '0x14aEd24B67EaF3FF28503eB92aeb217C47514364'
|
||||
},
|
||||
tokenAddress: '0x03c5F29e9296006876d8DF210BCFfD7EA5Db1Cf1',
|
||||
symbol: 'USDT',
|
||||
decimals: 6
|
||||
}
|
||||
}
|
||||
},
|
||||
defaultGasPrice: 20,
|
||||
port: process.env.APP_PORT,
|
||||
relayerServiceFee: Number(process.env.RELAYER_FEE),
|
||||
maxGasPrice: process.env.MAX_GAS_PRICE || 200,
|
||||
watherInterval: Number(process.env.NONCE_WATCHER_INTERVAL || 30) * 1000,
|
||||
pendingTxTimeout: Number(process.env.ALLOWABLE_PENDING_TX_TIMEOUT || 180) * 1000,
|
||||
gasBumpPercentage: process.env.GAS_PRICE_BUMP_PERCENTAGE || 20
|
||||
}
|
@ -1,471 +1,89 @@
|
||||
version: "2"
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
command: [redis-server, --appendonly, "yes"]
|
||||
volumes:
|
||||
- redis:/data
|
||||
kovan:
|
||||
image: tornadocash/relayer
|
||||
restart: always
|
||||
environment:
|
||||
VIRTUAL_HOST: example.duckdns.org
|
||||
LETSENCRYPT_HOST: example.duckdns.org
|
||||
NET_ID: 42
|
||||
RPC_URL: https://kovan.infura.io
|
||||
# ORACLE_RPC_URL should always point to the mainnet
|
||||
ORACLE_RPC_URL: https://mainnet.infura.io
|
||||
# without 0x prefix
|
||||
PRIVATE_KEY:
|
||||
# 2.5 means 2.5%
|
||||
RELAYER_FEE: 2.5
|
||||
REDIS_URL: redis://redis/0
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: nginx
|
||||
restart: always
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- conf:/etc/nginx/conf.d
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- html:/usr/share/nginx/html
|
||||
- certs:/etc/nginx/certs
|
||||
logging:
|
||||
driver: none
|
||||
mainnet:
|
||||
image: tornadocash/relayer
|
||||
restart: always
|
||||
environment:
|
||||
VIRTUAL_HOST: example2.duckdns.org
|
||||
LETSENCRYPT_HOST: example2.duckdns.org
|
||||
NET_ID: 1
|
||||
RPC_URL: https://mainnet.infura.io
|
||||
# ORACLE_RPC_URL should always point to the mainnet
|
||||
ORACLE_RPC_URL: https://mainnet.infura.io
|
||||
# without 0x prefix
|
||||
PRIVATE_KEY:
|
||||
# 2.5 means 2.5%
|
||||
RELAYER_FEE: 2.5
|
||||
REDIS_URL: redis://redis/1
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
dockergen:
|
||||
image: poma/docker-gen
|
||||
container_name: dockergen
|
||||
restart: always
|
||||
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
|
||||
volumes_from:
|
||||
- nginx
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
command: [redis-server, --appendonly, 'yes']
|
||||
volumes:
|
||||
- redis:/data
|
||||
|
||||
letsencrypt:
|
||||
image: jrcs/letsencrypt-nginx-proxy-companion
|
||||
container_name: letsencrypt
|
||||
restart: always
|
||||
environment:
|
||||
NGINX_DOCKER_GEN_CONTAINER: dockergen
|
||||
volumes_from:
|
||||
- nginx
|
||||
- dockergen
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: nginx
|
||||
restart: always
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- conf:/etc/nginx/conf.d
|
||||
- vhost:/etc/nginx/vhost.d
|
||||
- html:/usr/share/nginx/html
|
||||
- certs:/etc/nginx/certs
|
||||
logging:
|
||||
driver: none
|
||||
|
||||
# ---------------------- ETH Mainnet ----------------------- #
|
||||
dockergen:
|
||||
image: poma/docker-gen
|
||||
container_name: dockergen
|
||||
restart: always
|
||||
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
|
||||
volumes_from:
|
||||
- nginx
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
eth-server:
|
||||
build: .
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["eth"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.eth
|
||||
environment:
|
||||
NET_ID: 1
|
||||
REDIS_URL: redis://redis/0
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
eth-treeWatcher:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["eth"]
|
||||
restart: always
|
||||
command: treeWatcher
|
||||
env_file: .env.eth
|
||||
environment:
|
||||
NET_ID: 1
|
||||
REDIS_URL: redis://redis/0
|
||||
depends_on: [redis, eth-server]
|
||||
|
||||
eth-priceWatcher:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["eth"]
|
||||
restart: always
|
||||
command: priceWatcher
|
||||
env_file: .env.eth
|
||||
environment:
|
||||
NET_ID: 1
|
||||
REDIS_URL: redis://redis/0
|
||||
depends_on: [redis, eth-server]
|
||||
|
||||
eth-healthWatcher:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["eth"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.eth
|
||||
environment:
|
||||
NET_ID: 1
|
||||
REDIS_URL: redis://redis/0
|
||||
depends_on: [redis, eth-server]
|
||||
|
||||
eth-worker1:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["eth"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.eth
|
||||
environment:
|
||||
NET_ID: 1
|
||||
REDIS_URL: redis://redis/0
|
||||
depends_on: [redis, eth-server]
|
||||
|
||||
# # This is additional worker for ethereum mainnet
|
||||
# # So you can process transactions from multiple addresses, but before it you need to set up those addresses as workers
|
||||
# eth-worker2:
|
||||
# image: tornadocash/relayer:mainnet-v4
|
||||
# profiles: [ 'eth' ]
|
||||
# restart: always
|
||||
# command: worker
|
||||
# env_file: .env2.eth
|
||||
# environment:
|
||||
# REDIS_URL: redis://redis/0
|
||||
|
||||
# # this container will proxy *.onion domain to the server container
|
||||
# # if you want to run *only* as .onion service, you don't need `nginx`, `letsencrypt`, `dockergen` containers
|
||||
# tor:
|
||||
# image: strm/tor
|
||||
# restart: always
|
||||
# depends_on: [server]
|
||||
# environment:
|
||||
# LISTEN_PORT: 80
|
||||
# REDIRECT: server:8000
|
||||
# # Generate a new key with
|
||||
# # docker run --rm --entrypoint shallot strm/tor-hiddenservice-nginx ^foo
|
||||
# PRIVATE_KEY: |
|
||||
# -----BEGIN RSA PRIVATE KEY-----
|
||||
# ...
|
||||
# -----END RSA PRIVATE KEY-----
|
||||
|
||||
# # auto update docker containers when new image is pushed to docker hub (be careful with that)
|
||||
# watchtower:
|
||||
# image: v2tec/watchtower
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# # this container will send Telegram notifications when other containers are stopped/restarted
|
||||
# # it's best to run this container on some other instance, otherwise it can't notify if the whole instance goes down
|
||||
# notifier:
|
||||
# image: poma/docker-telegram-notifier
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
# environment:
|
||||
# # How to create bot: https://core.telegram.org/bots#3-how-do-i-create-a-bot
|
||||
# # How to get chat id: https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id/32572159#32572159
|
||||
# TELEGRAM_NOTIFIER_BOT_TOKEN: ...
|
||||
# TELEGRAM_NOTIFIER_CHAT_ID: ...
|
||||
|
||||
# # this container will send Telegram notifications if specified address doesn't have enough funds
|
||||
# monitor_mainnet:
|
||||
# image: peppersec/monitor_eth
|
||||
# restart: always
|
||||
# environment:
|
||||
# TELEGRAM_NOTIFIER_BOT_TOKEN: ...
|
||||
# TELEGRAM_NOTIFIER_CHAT_ID: ...
|
||||
# ADDRESS: '0x0000000000000000000000000000000000000000'
|
||||
# THRESHOLD: 0.5 # ETH
|
||||
# RPC_URL: https://mainnet.infura.io
|
||||
# BLOCK_EXPLORER: etherscan.io
|
||||
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- BSC (Binance Smart Chain) ----------------------- #
|
||||
|
||||
bsc-server:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["bsc"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.bsc
|
||||
environment:
|
||||
NET_ID: 56
|
||||
REDIS_URL: redis://redis/1
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
bsc-healthWatcher:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["bsc"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.bsc
|
||||
environment:
|
||||
NET_ID: 56
|
||||
REDIS_URL: redis://redis/1
|
||||
depends_on: [redis, bsc-server]
|
||||
|
||||
bsc-worker1:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["bsc"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.bsc
|
||||
environment:
|
||||
NET_ID: 56
|
||||
REDIS_URL: redis://redis/1
|
||||
depends_on: [redis, bsc-server]
|
||||
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- Polygon (MATIC) --------------------- #
|
||||
|
||||
polygon-server:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["polygon"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.polygon
|
||||
environment:
|
||||
NET_ID: 137
|
||||
REDIS_URL: redis://redis/2
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
polygon-healthWatcher:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["polygon"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.polygon
|
||||
environment:
|
||||
NET_ID: 137
|
||||
REDIS_URL: redis://redis/2
|
||||
depends_on: [redis, polygon-server]
|
||||
|
||||
polygon-worker1:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["polygon"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.polygon
|
||||
environment:
|
||||
NET_ID: 137
|
||||
REDIS_URL: redis://redis/2
|
||||
depends_on: [redis, polygon-server]
|
||||
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- Gnosis (XDAI) ---------------------- #
|
||||
|
||||
gnosis-server:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["gnosis"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.gnosis
|
||||
environment:
|
||||
NET_ID: 100
|
||||
REDIS_URL: redis://redis/3
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
gnosis-healthWatcher:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["gnosis"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.gnosis
|
||||
environment:
|
||||
NET_ID: 100
|
||||
REDIS_URL: redis://redis/3
|
||||
depends_on: [redis, gnosis-server]
|
||||
|
||||
gnosis-worker1:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["gnosis"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.gnosis
|
||||
environment:
|
||||
NET_ID: 100
|
||||
REDIS_URL: redis://redis/3
|
||||
depends_on: [redis, gnosis-server]
|
||||
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- AVAX ---------------------- #
|
||||
|
||||
avax-server:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["avax"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.avax
|
||||
environment:
|
||||
NET_ID: 43114
|
||||
REDIS_URL: redis://redis/4
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
avax-healthWatcher:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["avax"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.avax
|
||||
environment:
|
||||
NET_ID: 43114
|
||||
REDIS_URL: redis://redis/4
|
||||
depends_on: [redis, avax-server]
|
||||
|
||||
avax-worker1:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["avax"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.avax
|
||||
environment:
|
||||
NET_ID: 43114
|
||||
REDIS_URL: redis://redis/4
|
||||
depends_on: [redis, avax-server]
|
||||
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- OP ------------------------ #
|
||||
|
||||
op-server:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["op"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.op
|
||||
environment:
|
||||
NET_ID: 10
|
||||
REDIS_URL: redis://redis/5
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
op-healthWatcher:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["op"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.op
|
||||
environment:
|
||||
NET_ID: 10
|
||||
REDIS_URL: redis://redis/5
|
||||
depends_on: [redis, op-server]
|
||||
|
||||
op-worker1:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["op"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.op
|
||||
environment:
|
||||
NET_ID: 10
|
||||
REDIS_URL: redis://redis/5
|
||||
depends_on: [redis, op-server]
|
||||
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- Arbitrum ----------------------- #
|
||||
|
||||
arb-server:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["arb"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.arb
|
||||
environment:
|
||||
NET_ID: 42161
|
||||
REDIS_URL: redis://redis/6
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
arb-healthWatcher:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["arb"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.arb
|
||||
environment:
|
||||
NET_ID: 42161
|
||||
REDIS_URL: redis://redis/6
|
||||
depends_on: [redis, arb-server]
|
||||
|
||||
arb-worker1:
|
||||
image: tornadocash/relayer:sidechain-v5
|
||||
profiles: ["arb"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.arb
|
||||
environment:
|
||||
NET_ID: 42161
|
||||
REDIS_URL: redis://redis/6
|
||||
depends_on: [redis, arb-server]
|
||||
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- Goerli (Ethereum Testnet) ---------------------- #
|
||||
|
||||
goerli-server:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["geth"]
|
||||
restart: always
|
||||
command: server
|
||||
env_file: .env.goerli
|
||||
environment:
|
||||
NET_ID: 5
|
||||
REDIS_URL: redis://redis/7
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
|
||||
goerli-treeWatcher:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["goerli"]
|
||||
restart: always
|
||||
command: treeWatcher
|
||||
env_file: .env.goerli
|
||||
environment:
|
||||
NET_ID: 5
|
||||
REDIS_URL: redis://redis/7
|
||||
depends_on: [redis, goerli-server]
|
||||
|
||||
goerli-priceWatcher:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["goerli"]
|
||||
restart: always
|
||||
command: priceWatcher
|
||||
env_file: .env.goerli
|
||||
environment:
|
||||
NET_ID: 5
|
||||
REDIS_URL: redis://redis/7
|
||||
depends_on: [redis, goerli-server]
|
||||
|
||||
goerli-healthWatcher:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["goerli"]
|
||||
restart: always
|
||||
command: healthWatcher
|
||||
env_file: .env.goerli
|
||||
environment:
|
||||
NET_ID: 5
|
||||
REDIS_URL: redis://redis/7
|
||||
depends_on: [redis, goerli-server]
|
||||
|
||||
goerli-worker1:
|
||||
image: tornadocash/relayer:mainnet-v4
|
||||
profiles: ["goerli"]
|
||||
restart: always
|
||||
command: worker
|
||||
env_file: .env.goerli
|
||||
environment:
|
||||
NET_ID: 5
|
||||
REDIS_URL: redis://redis/7
|
||||
depends_on: [redis, goerli-server]
|
||||
# -------------------------------------------------- #
|
||||
|
||||
# ---------------------- Tornado Nova (Gnosis Chain) ----------------------- #
|
||||
|
||||
server:
|
||||
image: tornadocash/relayer:nova
|
||||
profiles: ["nova"]
|
||||
restart: always
|
||||
command: start:prod
|
||||
env_file: .env.nova
|
||||
environment:
|
||||
REDIS_URL: redis://redis/8
|
||||
nginx_proxy_read_timeout: 600
|
||||
depends_on: [redis]
|
||||
letsencrypt:
|
||||
image: jrcs/letsencrypt-nginx-proxy-companion
|
||||
container_name: letsencrypt
|
||||
restart: always
|
||||
environment:
|
||||
NGINX_DOCKER_GEN_CONTAINER: dockergen
|
||||
volumes_from:
|
||||
- nginx
|
||||
- dockergen
|
||||
|
||||
volumes:
|
||||
conf:
|
||||
vhost:
|
||||
html:
|
||||
certs:
|
||||
redis:
|
||||
conf:
|
||||
vhost:
|
||||
html:
|
||||
certs:
|
||||
redis:
|
||||
|
120
install.sh
120
install.sh
@ -1,120 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Script must be running from root
|
||||
if [ "$EUID" -ne 0 ];
|
||||
then echo "Please run as root";
|
||||
exit 1;
|
||||
fi;
|
||||
|
||||
relayer_soft_git_repo="https://git.tornado.ws/tornadocash/tornado-relayer";
|
||||
|
||||
user_home_dir=$(eval echo ~$USER);
|
||||
relayer_folder="$user_home_dir/tornado-relayer";
|
||||
relayer_mainnet_soft_source_folder="$relayer_folder/mainnet-soft-source";
|
||||
relayer_sidechains_soft_source_folder="$relayer_folder/sidechains-soft-source";
|
||||
nova_relayer_soft_source_folder="$relayer_folder/nova-soft-source";
|
||||
script_log_file="/tmp/tornado-relayer-installation.log"
|
||||
if [ -f $script_log_file ]; then rm $script_log_file; fi;
|
||||
|
||||
function echo_log_err(){
|
||||
echo $1 1>&2;
|
||||
echo -e "$1\n" &>> $script_log_file;
|
||||
}
|
||||
|
||||
function echo_log_err_and_exit(){
|
||||
echo_log_err "$1";
|
||||
exit 1;
|
||||
}
|
||||
|
||||
function is_package_installed(){
|
||||
if [ $(dpkg-query -W -f='${Status}' $1 2>/dev/null | grep -c "ok installed") -eq 0 ]; then return 1; else return 0; fi;
|
||||
}
|
||||
|
||||
function install_requred_packages(){
|
||||
apt update &>> $script_log_file;
|
||||
|
||||
requred_packages=("curl" "git-all" "ufw" "nginx");
|
||||
local package;
|
||||
for package in ${requred_packages[@]}; do
|
||||
if ! is_package_installed $package; then
|
||||
# Kill apache process, because Debian configuring nginx package right during installation
|
||||
if [ $package = "nginx" ]; then systemctl stop apache2; fi;
|
||||
apt install --yes --force-yes -o DPkg::Options::="--force-confold" $package &>> $script_log_file;
|
||||
if ! is_package_installed $package; then
|
||||
echo_log_err_and_exit "Error: cannot install \"$package\" package";
|
||||
fi;
|
||||
fi;
|
||||
done;
|
||||
|
||||
echo -e "\nAll required packages installed successfully";
|
||||
}
|
||||
|
||||
function install_node(){
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash;
|
||||
. ~/.nvm/nvm.sh;
|
||||
. ~/.profile;
|
||||
. ~/.bashrc;
|
||||
nvm install 14.21.3;
|
||||
}
|
||||
|
||||
function install_repositories(){
|
||||
git clone $relayer_soft_git_repo -b main $relayer_folder
|
||||
git clone $relayer_soft_git_repo -b mainnet-v4 $relayer_mainnet_soft_source_folder;
|
||||
git clone $relayer_soft_git_repo -b sidechain-v5 $relayer_sidechains_soft_source_folder;
|
||||
git clone $relayer_soft_git_repo -b nova $nova_relayer_soft_source_folder;
|
||||
}
|
||||
|
||||
function install_docker_utilities(){
|
||||
local kernel_name=$(uname -s);
|
||||
local processor_type=$(uname -m);
|
||||
|
||||
curl -SL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-$kernel_name-$processor_type -o /usr/local/bin/docker-compose;
|
||||
chmod +x /usr/local/bin/docker-compose;
|
||||
|
||||
curl -s https://get.docker.com | bash;
|
||||
}
|
||||
|
||||
function configure_firewall(){
|
||||
ufw allow https/tcp;
|
||||
ufw allow http/tcp;
|
||||
ufw insert 1 allow OpenSSH;
|
||||
echo "y" | ufw enable;
|
||||
}
|
||||
|
||||
function configure_nginx_reverse_proxy(){
|
||||
systemctl stop apache2;
|
||||
|
||||
cp $relayer_folder/tornado.conf /etc/nginx/sites-available/default;
|
||||
echo "stream { map_hash_bucket_size 128; map_hash_max_size 128; include /etc/nginx/conf.d/streams/*.conf; }" >> /etc/nginx/nginx.conf;
|
||||
mkdir /etc/nginx/conf.d/streams;
|
||||
cp $relayer_folder/tornado-stream.conf /etc/nginx/conf.d/streams/tornado-stream.conf;
|
||||
|
||||
systemctl restart nginx;
|
||||
systemctl stop nginx;
|
||||
}
|
||||
|
||||
function build_relayer_docker_containers(){
|
||||
cd $relayer_mainnet_soft_source_folder && npm run build;
|
||||
cd $relayer_sidechains_soft_source_folder && npm run build;
|
||||
cd $nova_relayer_soft_source_folder && npm run build:docker;
|
||||
}
|
||||
|
||||
function prepare_environments(){
|
||||
cp $relayer_mainnet_soft_source_folder/.env.example $relayer_folder/.env.eth;
|
||||
cp $nova_relayer_soft_source_folder/.env.example $relayer_folder/.env.nova;
|
||||
tee $relayer_folder/.env.bsc $relayer_folder/.env.arb $relayer_folder/.env.goerli $relayer_folder/.env.polygon $relayer_folder/.env.op \
|
||||
$relayer_folder/.env.avax $relayer_folder/.env.gnosis < $relayer_sidechains_soft_source_folder/.env.example > /dev/null;
|
||||
}
|
||||
|
||||
function main(){
|
||||
install_requred_packages;
|
||||
install_node;
|
||||
install_repositories;
|
||||
configure_firewall;
|
||||
configure_nginx_reverse_proxy;
|
||||
install_docker_utilities;
|
||||
build_relayer_docker_containers;
|
||||
prepare_environments;
|
||||
cd $relayer_folder;
|
||||
}
|
||||
|
||||
main;
|
4138
package-lock.json
generated
Normal file
4138
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "relay",
|
||||
"version": "3.0.3",
|
||||
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node app.js",
|
||||
"eslint": "npx eslint --ignore-path .gitignore .",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "tornado.cash",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bull": "^3.12.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"express": "^4.17.1",
|
||||
"gas-price-oracle": "^0.2.2",
|
||||
"ioredis": "^4.14.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"web3": "^1.2.2",
|
||||
"web3-utils": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^6.6.0"
|
||||
}
|
||||
}
|
56
src/Fetcher.js
Normal file
56
src/Fetcher.js
Normal file
@ -0,0 +1,56 @@
|
||||
const Web3 = require('web3')
|
||||
const { defaultGasPrice, oracleRpcUrl, oracleAddress } = require('../config')
|
||||
const { getArgsForOracle } = require('./utils')
|
||||
const { redisClient } = require('./redis')
|
||||
const priceOracleABI = require('../abis/PriceOracle.abi.json')
|
||||
|
||||
class Fetcher {
|
||||
constructor(web3) {
|
||||
this.web3 = web3
|
||||
this.oracleWeb3 = new Web3(oracleRpcUrl)
|
||||
this.oracle = new this.oracleWeb3.eth.Contract(priceOracleABI, oracleAddress)
|
||||
this.ethPrices = {
|
||||
dai: '6700000000000000', // 0.0067
|
||||
cdai: '157380000000000',
|
||||
cusdc: '164630000000000',
|
||||
usdc: '7878580000000000',
|
||||
usdt: '7864940000000000'
|
||||
}
|
||||
this.tokenAddresses
|
||||
this.oneUintAmount
|
||||
this.currencyLookup
|
||||
this.gasPrices = {
|
||||
fast: defaultGasPrice
|
||||
}
|
||||
|
||||
const { tokenAddresses, oneUintAmount, currencyLookup } = getArgsForOracle()
|
||||
this.tokenAddresses = tokenAddresses
|
||||
this.oneUintAmount = oneUintAmount
|
||||
this.currencyLookup = currencyLookup
|
||||
}
|
||||
async fetchPrices() {
|
||||
try {
|
||||
let prices = await this.oracle.methods.getPricesInETH(this.tokenAddresses, this.oneUintAmount).call()
|
||||
this.ethPrices = prices.reduce((acc, price, i) => {
|
||||
acc[this.currencyLookup[this.tokenAddresses[i]]] = price
|
||||
return acc
|
||||
}, {})
|
||||
setTimeout(() => this.fetchPrices(), 1000 * 30)
|
||||
} catch (e) {
|
||||
console.error('fetchPrices', e.message)
|
||||
setTimeout(() => this.fetchPrices(), 1000 * 30)
|
||||
}
|
||||
}
|
||||
async fetchNonce() {
|
||||
try {
|
||||
const nonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
|
||||
await redisClient.set('nonce', nonce)
|
||||
console.log(`Current nonce: ${nonce}`)
|
||||
} catch (e) {
|
||||
console.error('fetchNonce failed', e.message)
|
||||
setTimeout(this.fetchNonce, 3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Fetcher
|
98
src/index.js
Normal file
98
src/index.js
Normal file
@ -0,0 +1,98 @@
|
||||
const express = require('express')
|
||||
const {
|
||||
netId,
|
||||
port,
|
||||
relayerServiceFee,
|
||||
gasBumpPercentage,
|
||||
pendingTxTimeout,
|
||||
watherInterval,
|
||||
maxGasPrice
|
||||
} = require('../config')
|
||||
const relayController = require('./relayController')
|
||||
const { fetcher, web3, gasPriceOracle } = require('./instances')
|
||||
const { getMixers } = require('./utils')
|
||||
const mixers = getMixers()
|
||||
const { redisClient } = require('./redis')
|
||||
const { version } = require('../package.json')
|
||||
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 <a href=/status>/status</a> for settings'
|
||||
)
|
||||
})
|
||||
|
||||
app.get('/status', async function (req, res) {
|
||||
let nonce = await redisClient.get('nonce')
|
||||
let latestBlock = null
|
||||
try {
|
||||
latestBlock = await web3.eth.getBlockNumber()
|
||||
} catch (e) {
|
||||
console.error('Problem with RPC', e)
|
||||
}
|
||||
const { ethPrices } = fetcher
|
||||
res.json({
|
||||
relayerAddress: web3.eth.defaultAccount,
|
||||
mixers,
|
||||
gasPrices: await gasPriceOracle.gasPrices(),
|
||||
netId,
|
||||
ethPrices,
|
||||
relayerServiceFee,
|
||||
nonce,
|
||||
version,
|
||||
latestBlock
|
||||
})
|
||||
})
|
||||
|
||||
app.post('/relay', relayController)
|
||||
console.log('Version:', version)
|
||||
let server = app.listen(port || 8000)
|
||||
server.setTimeout(600000)
|
||||
console.log('Gas price oracle started.')
|
||||
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(`netId: ${netId}`)
|
||||
console.log(`ethPrices: ${JSON.stringify(fetcher.ethPrices)}`)
|
||||
|
||||
const {
|
||||
GAS_PRICE_BUMP_PERCENTAGE,
|
||||
ALLOWABLE_PENDING_TX_TIMEOUT,
|
||||
NONCE_WATCHER_INTERVAL,
|
||||
MAX_GAS_PRICE
|
||||
} = process.env
|
||||
if (!NONCE_WATCHER_INTERVAL) {
|
||||
console.log(`NONCE_WATCHER_INTERVAL is not set. Using default value ${watherInterval / 1000} sec`)
|
||||
}
|
||||
|
||||
if (!GAS_PRICE_BUMP_PERCENTAGE) {
|
||||
console.log(`GAS_PRICE_BUMP_PERCENTAGE is not set. Using default value ${gasBumpPercentage}%`)
|
||||
}
|
||||
|
||||
if (!ALLOWABLE_PENDING_TX_TIMEOUT) {
|
||||
console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${pendingTxTimeout / 1000} sec`)
|
||||
}
|
||||
|
||||
if (!MAX_GAS_PRICE) {
|
||||
console.log(`ALLOWABLE_PENDING_TX_TIMEOUT is not set. Using default value ${maxGasPrice} Gwei`)
|
||||
}
|
15
src/instances.js
Normal file
15
src/instances.js
Normal file
@ -0,0 +1,15 @@
|
||||
const { rpcUrl } = require('../config')
|
||||
const Fetcher = require('./Fetcher')
|
||||
const Sender = require('./sender')
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
const web3 = require('./setupWeb3')
|
||||
const fetcher = new Fetcher(web3)
|
||||
const sender = new Sender(web3)
|
||||
const gasPriceOracle = new GasPriceOracle({ defaultRpc: rpcUrl })
|
||||
|
||||
module.exports = {
|
||||
fetcher,
|
||||
web3,
|
||||
sender,
|
||||
gasPriceOracle
|
||||
}
|
19
src/redis.js
Normal file
19
src/redis.js
Normal file
@ -0,0 +1,19 @@
|
||||
const { redisUrl } = require('../config')
|
||||
const Redis = require('ioredis')
|
||||
const redisClient = new Redis(redisUrl)
|
||||
const subscriber = new Redis(redisUrl)
|
||||
|
||||
const redisOpts = {
|
||||
createClient: function (type) {
|
||||
switch (type) {
|
||||
case 'client':
|
||||
return redisClient
|
||||
case 'subscriber':
|
||||
return subscriber
|
||||
default:
|
||||
return new Redis(redisUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { redisOpts, redisClient }
|
162
src/relayController.js
Normal file
162
src/relayController.js
Normal file
@ -0,0 +1,162 @@
|
||||
const Queue = require('bull')
|
||||
const { numberToHex, toWei, toHex, toBN, toChecksumAddress } = require('web3-utils')
|
||||
const mixerABI = require('../abis/mixerABI.json')
|
||||
const { isValidProof, isValidArgs, isKnownContract, isEnoughFee } = require('./utils')
|
||||
const config = require('../config')
|
||||
const { redisClient, redisOpts } = require('./redis')
|
||||
|
||||
const { web3, fetcher, sender, gasPriceOracle } = require('./instances')
|
||||
const withdrawQueue = new Queue('withdraw', redisOpts)
|
||||
|
||||
const reponseCbs = {}
|
||||
let respLambda = (job, { msg, status }) => {
|
||||
const resp = reponseCbs[job.id]
|
||||
resp.status(status).json(msg)
|
||||
delete reponseCbs[job.id]
|
||||
}
|
||||
withdrawQueue.on('completed', respLambda)
|
||||
|
||||
async function relayController(req, resp) {
|
||||
let requestJob
|
||||
|
||||
const { proof, args, contract } = req.body
|
||||
let { valid, reason } = isValidProof(proof)
|
||||
if (!valid) {
|
||||
console.log('Proof is invalid:', reason)
|
||||
return resp.status(400).json({ error: 'Proof format is invalid' })
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-extra-semi
|
||||
;({ valid, reason } = isValidArgs(args))
|
||||
if (!valid) {
|
||||
console.log('Args are invalid:', reason)
|
||||
return resp.status(400).json({ error: 'Withdraw arguments are invalid' })
|
||||
}
|
||||
|
||||
let currency, amount
|
||||
;({ valid, currency, amount } = isKnownContract(contract))
|
||||
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(), recipient)
|
||||
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' })
|
||||
}
|
||||
|
||||
requestJob = await withdrawQueue.add(
|
||||
{
|
||||
contract,
|
||||
nullifierHash,
|
||||
root,
|
||||
proof,
|
||||
args,
|
||||
currency,
|
||||
amount,
|
||||
fee: fee.toString(),
|
||||
refund: refund.toString()
|
||||
},
|
||||
{ removeOnComplete: true }
|
||||
)
|
||||
reponseCbs[requestJob.id] = resp
|
||||
}
|
||||
|
||||
withdrawQueue.process(async function (job, done) {
|
||||
console.log(Date.now(), ' withdraw started', job.id)
|
||||
const gasPrices = await gasPriceOracle.gasPrices()
|
||||
const { contract, nullifierHash, root, proof, args, refund, currency, amount, fee } = job.data
|
||||
console.log(JSON.stringify(job.data))
|
||||
// job.data contains the custom data passed when the job was created
|
||||
// job.id contains id of this job.
|
||||
try {
|
||||
const mixer = new web3.eth.Contract(mixerABI, contract)
|
||||
const isSpent = await mixer.methods.isSpent(nullifierHash).call()
|
||||
if (isSpent) {
|
||||
done(null, {
|
||||
status: 400,
|
||||
msg: {
|
||||
error: 'The note has been spent.'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const isKnownRoot = await mixer.methods.isKnownRoot(root).call()
|
||||
if (!isKnownRoot) {
|
||||
done(null, {
|
||||
status: 400,
|
||||
msg: {
|
||||
error: 'The merkle root is too old or invalid.'
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
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: toBN(refund),
|
||||
ethPrices,
|
||||
fee: toBN(fee)
|
||||
})
|
||||
if (!isEnough) {
|
||||
console.log(`Wrong fee: ${reason}`)
|
||||
done(null, {
|
||||
status: 400,
|
||||
msg: { error: reason }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const data = mixer.methods.withdraw(proof, ...args).encodeABI()
|
||||
let nonce = Number(await redisClient.get('nonce'))
|
||||
console.log('nonce', nonce)
|
||||
const tx = {
|
||||
from: web3.eth.defaultAccount,
|
||||
value: numberToHex(refund),
|
||||
gas: numberToHex(gas),
|
||||
gasPrice: toHex(toWei(gasPrices.fast.toString(), 'gwei')),
|
||||
// you can use this gasPrice to test watcher
|
||||
// gasPrice: numberToHex(100000000),
|
||||
to: mixer._address,
|
||||
netId: config.netId,
|
||||
data,
|
||||
nonce
|
||||
}
|
||||
tx.date = Date.now()
|
||||
await redisClient.set('tx:' + nonce, JSON.stringify(tx))
|
||||
nonce += 1
|
||||
await redisClient.set('nonce', nonce)
|
||||
sender.sendTx(tx, done)
|
||||
} catch (e) {
|
||||
console.error(e, 'estimate gas failed')
|
||||
done(null, {
|
||||
status: 400,
|
||||
msg: { error: 'Internal Relayer Error. Please use a different relayer service' }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = relayController
|
81
src/sender.js
Normal file
81
src/sender.js
Normal file
@ -0,0 +1,81 @@
|
||||
const { redisClient } = require('./redis')
|
||||
const config = require('../config')
|
||||
const { toBN, toHex, toWei, BN, fromWei } = require('web3-utils')
|
||||
|
||||
class Sender {
|
||||
constructor(web3) {
|
||||
this.web3 = web3
|
||||
this.watherInterval = config.watherInterval
|
||||
this.pendingTxTimeout = config.pendingTxTimeout
|
||||
this.gasBumpPercentage = 100 + Number(config.gasBumpPercentage)
|
||||
this.watcher()
|
||||
}
|
||||
|
||||
async watcher() {
|
||||
try {
|
||||
const networkNonce = await this.web3.eth.getTransactionCount(this.web3.eth.defaultAccount)
|
||||
let tx = await redisClient.get('tx:' + networkNonce)
|
||||
if (tx) {
|
||||
tx = JSON.parse(tx)
|
||||
if (Date.now() - tx.date > this.pendingTxTimeout) {
|
||||
const newGasPrice = toBN(tx.gasPrice).mul(toBN(this.gasBumpPercentage)).div(toBN(100))
|
||||
const maxGasPrice = toBN(toWei(config.maxGasPrice.toString(), 'Gwei'))
|
||||
tx.gasPrice = toHex(BN.min(newGasPrice, maxGasPrice))
|
||||
tx.date = Date.now()
|
||||
await redisClient.set('tx:' + tx.nonce, JSON.stringify(tx))
|
||||
console.log('resubmitting with gas price', fromWei(tx.gasPrice.toString(), 'gwei'), ' gwei')
|
||||
this.sendTx(tx, null, 9999)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('watcher error:', e)
|
||||
} finally {
|
||||
setTimeout(() => this.watcher(), this.watherInterval)
|
||||
}
|
||||
}
|
||||
|
||||
async sendTx(tx, done, retryAttempt = 1) {
|
||||
let signedTx = await this.web3.eth.accounts.signTransaction(tx, config.privateKey)
|
||||
let result = this.web3.eth.sendSignedTransaction(signedTx.rawTransaction)
|
||||
|
||||
result
|
||||
.once('transactionHash', (txHash) => {
|
||||
console.log(`A new successfully sent tx ${txHash}`)
|
||||
if (done) {
|
||||
done(null, {
|
||||
status: 200,
|
||||
msg: { txHash }
|
||||
})
|
||||
}
|
||||
})
|
||||
.on('error', async (e) => {
|
||||
console.log(`Error for tx with nonce ${tx.nonce}\n${e.message}`)
|
||||
if (
|
||||
e.message ===
|
||||
'Returned error: Transaction gas price supplied is too low. There is another transaction with same nonce in the queue. Try increasing the gas price or incrementing the nonce.' ||
|
||||
e.message === 'Returned error: Transaction nonce is too low. Try incrementing the nonce.' ||
|
||||
e.message === 'Returned error: nonce too low' ||
|
||||
e.message === 'Returned error: replacement transaction underpriced'
|
||||
) {
|
||||
console.log('nonce too low, retrying')
|
||||
if (retryAttempt <= 10) {
|
||||
retryAttempt++
|
||||
const newNonce = tx.nonce + 1
|
||||
tx.nonce = newNonce
|
||||
await redisClient.set('nonce', newNonce)
|
||||
await redisClient.set('tx:' + newNonce, JSON.stringify(tx))
|
||||
this.sendTx(tx, done, retryAttempt)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (done) {
|
||||
done(null, {
|
||||
status: 400,
|
||||
msg: { error: 'Internal Relayer Error. Please use a different relayer service' }
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Sender
|
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
|
178
src/utils.js
Normal file
178
src/utils.js
Normal file
@ -0,0 +1,178 @@
|
||||
const { isHexStrict, toBN, toWei, BN } = require('web3-utils')
|
||||
const { netId, mixers, relayerServiceFee } = require('../config')
|
||||
|
||||
function isValidProof(proof) {
|
||||
// validator expects `websnarkUtils.toSolidityInput(proof)` output
|
||||
|
||||
if (!proof) {
|
||||
return { valid: false, reason: 'The proof is empty.' }
|
||||
}
|
||||
|
||||
if (!isHexStrict(proof) || proof.length !== 2 + 2 * 8 * 32) {
|
||||
return { valid: false, reason: 'Corrupted proof' }
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
function isValidArgs(args) {
|
||||
if (!args) {
|
||||
return { valid: false, reason: 'Args are empty' }
|
||||
}
|
||||
|
||||
if (args.length !== 6) {
|
||||
return { valid: false, reason: 'Length of args is lower than 6' }
|
||||
}
|
||||
|
||||
for (let signal of args) {
|
||||
if (!isHexStrict(signal)) {
|
||||
return { valid: false, reason: `Corrupted signal ${signal}` }
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
args[0].length !== 66 ||
|
||||
args[1].length !== 66 ||
|
||||
args[2].length !== 42 ||
|
||||
args[3].length !== 42 ||
|
||||
args[4].length !== 66 ||
|
||||
args[5].length !== 66
|
||||
) {
|
||||
return { valid: false, reason: 'The length one of the signals is incorrect' }
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
function isKnownContract(contract) {
|
||||
const mixers = getMixers()
|
||||
for (let currency of Object.keys(mixers)) {
|
||||
for (let amount of Object.keys(mixers[currency].mixerAddress)) {
|
||||
if (mixers[currency].mixerAddress[amount] === contract) {
|
||||
return { valid: true, currency, amount }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { valid: false }
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function fromDecimals(value, decimals) {
|
||||
value = value.toString()
|
||||
let ether = value.toString()
|
||||
const base = new BN('10').pow(new BN(decimals))
|
||||
const baseLength = base.toString(10).length - 1 || 1
|
||||
|
||||
const negative = ether.substring(0, 1) === '-'
|
||||
if (negative) {
|
||||
ether = ether.substring(1)
|
||||
}
|
||||
|
||||
if (ether === '.') {
|
||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, invalid value')
|
||||
}
|
||||
|
||||
// Split it into a whole and fractional part
|
||||
const comps = ether.split('.')
|
||||
if (comps.length > 2) {
|
||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal points')
|
||||
}
|
||||
|
||||
let whole = comps[0]
|
||||
let fraction = comps[1]
|
||||
|
||||
if (!whole) {
|
||||
whole = '0'
|
||||
}
|
||||
if (!fraction) {
|
||||
fraction = '0'
|
||||
}
|
||||
if (fraction.length > baseLength) {
|
||||
throw new Error('[ethjs-unit] while converting number ' + value + ' to wei, too many decimal places')
|
||||
}
|
||||
|
||||
while (fraction.length < baseLength) {
|
||||
fraction += '0'
|
||||
}
|
||||
|
||||
whole = new BN(whole)
|
||||
fraction = new BN(fraction)
|
||||
let wei = whole.mul(base).add(fraction)
|
||||
|
||||
if (negative) {
|
||||
wei = wei.mul(negative)
|
||||
}
|
||||
|
||||
return new BN(wei.toString(10), 10)
|
||||
}
|
||||
|
||||
function isEnoughFee({ gas, gasPrices, currency, amount, refund, ethPrices, fee }) {
|
||||
const { decimals } = mixers[`netId${netId}`][currency]
|
||||
const decimalsPoint =
|
||||
Math.floor(relayerServiceFee) === relayerServiceFee
|
||||
? 0
|
||||
: relayerServiceFee.toString().split('.')[1].length
|
||||
|
||||
const roundDecimal = 10 ** decimalsPoint
|
||||
const feePercent = toBN(fromDecimals(amount, decimals))
|
||||
.mul(toBN(relayerServiceFee * roundDecimal))
|
||||
.div(toBN(roundDecimal * 100))
|
||||
const expense = toBN(toWei(gasPrices.fast.toString(), 'gwei')).mul(toBN(gas))
|
||||
let desiredFee
|
||||
switch (currency) {
|
||||
case 'eth': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
desiredFee = expense
|
||||
.add(refund)
|
||||
.mul(toBN(10 ** decimals))
|
||||
.div(toBN(ethPrices[currency]))
|
||||
desiredFee = desiredFee.add(feePercent)
|
||||
break
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
'sent fee, desired fee, feePercent',
|
||||
fee.toString(),
|
||||
desiredFee.toString(),
|
||||
feePercent.toString()
|
||||
)
|
||||
if (fee.lt(desiredFee)) {
|
||||
return { isEnough: false, reason: 'Not enough fee' }
|
||||
}
|
||||
return { isEnough: true }
|
||||
}
|
||||
|
||||
function getArgsForOracle() {
|
||||
const tokens = mixers['netId1']
|
||||
const tokenAddresses = []
|
||||
const oneUintAmount = []
|
||||
const currencyLookup = {}
|
||||
Object.entries(tokens).map(([currency, data]) => {
|
||||
if (currency !== 'eth') {
|
||||
tokenAddresses.push(data.tokenAddress)
|
||||
oneUintAmount.push(toBN('10').pow(toBN(data.decimals.toString())).toString())
|
||||
currencyLookup[data.tokenAddress] = currency
|
||||
}
|
||||
})
|
||||
return { tokenAddresses, oneUintAmount, currencyLookup }
|
||||
}
|
||||
|
||||
function getMixers() {
|
||||
return mixers[`netId${netId}`]
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isValidProof,
|
||||
isValidArgs,
|
||||
sleep,
|
||||
isKnownContract,
|
||||
isEnoughFee,
|
||||
getMixers,
|
||||
getArgsForOracle
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
map $ssl_preread_server_name $name {
|
||||
yourdomain.com tornado_mainnet;
|
||||
|
||||
default tornado_mainnet;
|
||||
}
|
||||
|
||||
upstream tornado_mainnet {
|
||||
server 127.0.0.1:4380;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 0.0.0.0:443;
|
||||
proxy_pass $name;
|
||||
ssl_preread on;
|
||||
}
|
87
tornado.conf
87
tornado.conf
@ -1,87 +0,0 @@
|
||||
# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the
|
||||
# scheme used to connect to this server
|
||||
map $http_x_forwarded_proto $proxy_x_forwarded_proto {
|
||||
default $http_x_forwarded_proto;
|
||||
'' $scheme;
|
||||
}
|
||||
# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the
|
||||
# server port the client connected to
|
||||
map $http_x_forwarded_port $proxy_x_forwarded_port {
|
||||
default $http_x_forwarded_port;
|
||||
'' $server_port;
|
||||
}
|
||||
# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any
|
||||
# Connection header that may have been passed to this server
|
||||
map $http_upgrade $proxy_connection {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
# Apply fix for very long server names
|
||||
server_names_hash_bucket_size 128;
|
||||
# Default dhparam
|
||||
# Set appropriate X-Forwarded-Ssl header based on $proxy_x_forwarded_proto
|
||||
map $proxy_x_forwarded_proto $proxy_x_forwarded_ssl {
|
||||
default off;
|
||||
https on;
|
||||
}
|
||||
gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
log_format vhost '$host $remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent" '
|
||||
'"$upstream_addr"';
|
||||
# HTTP 1.1 support
|
||||
proxy_http_version 1.1;
|
||||
proxy_buffering off;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $proxy_connection;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto;
|
||||
proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl;
|
||||
proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
# Mitigate httpoxy attack (see README for details)
|
||||
proxy_set_header Proxy "";
|
||||
|
||||
# Request rate limiting per second, 2Mb zone @ 5 requests per second
|
||||
limit_req_zone $binary_remote_addr zone=one:2m rate=5r/s;
|
||||
# Connections per IP limited to 2
|
||||
limit_conn_zone $binary_remote_addr zone=two:2m;
|
||||
|
||||
server {
|
||||
server_name _; # This is just an invalid value which will never trigger on a real hostname.
|
||||
server_tokens off;
|
||||
listen 80;
|
||||
access_log /var/log/nginx/access.log vhost;
|
||||
return 503;
|
||||
}
|
||||
|
||||
server {
|
||||
server_name yourdomain.com;
|
||||
|
||||
# Connection timeouts
|
||||
client_body_timeout 10s;
|
||||
client_header_timeout 10s;
|
||||
|
||||
listen 80;
|
||||
access_log /var/log/nginx/access.log vhost;
|
||||
|
||||
# Do not HTTPS redirect LetsEncrypt ACME challenge
|
||||
location ^~ /.well-known/acme-challenge/ {
|
||||
limit_req zone=one;
|
||||
limit_conn two 1;
|
||||
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
location / {
|
||||
limit_req zone=one;
|
||||
limit_conn two 1;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user