Compare commits
53 Commits
main
...
sidechain-
Author | SHA1 | Date | |
---|---|---|---|
1705dd1ce2 | |||
1e99a136cc | |||
03c71f28bb | |||
d04480a62f | |||
5191207cc2 | |||
bb76455d6a | |||
c33a7f9ef2 | |||
0a7e97e62e | |||
fb2fbb89f3 | |||
bc6778bda2 | |||
64ccb5d4d0 | |||
b2ae3d3399 | |||
9b14e3ed89 | |||
4c1500bd96 | |||
|
a4916d1cb4 | ||
|
7350591ebd | ||
|
a453444318 | ||
|
bca85764bc | ||
|
38fd1e6a4c | ||
|
ecdcb545fc | ||
|
ed9f003d50 | ||
|
526f1889f9 | ||
|
e5dd4b465f | ||
|
92bc3bb0a8 | ||
|
7d8a6dc30f | ||
|
5b2815e6a0 | ||
|
220a71fe09 | ||
|
692ab64d49 | ||
|
6eb3d702a4 | ||
|
06848352f1 | ||
|
87d3528346 | ||
|
1d6c2575f9 | ||
|
96b6e722d8 | ||
|
4a28d4646e | ||
|
3d952f1906 | ||
|
07b091ff46 | ||
|
10b2fed839 | ||
|
c0320285b9 | ||
|
7ea0b9abd6 | ||
|
aa7d389766 | ||
|
e538236904 | ||
|
787c9663fe | ||
|
71b638e640 | ||
|
3d7a9b5f93 | ||
|
96d937ee0d | ||
|
9b49cab775 | ||
|
9747c1a397 | ||
|
3cf642fe68 | ||
|
5cfe527be8 | ||
|
e24af4239e | ||
|
a875bc77fa | ||
|
7286aeaff0 | ||
|
803a104a93 |
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.git
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
18
.env.example
Normal file
18
.env.example
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
NET_ID=56
|
||||||
|
HTTP_RPC_URL=https://bsc-dataseed1.ninicoin.io/
|
||||||
|
REDIS_URL=redis://127.0.0.1:6379
|
||||||
|
|
||||||
|
# DNS settings
|
||||||
|
VIRTUAL_HOST=example.duckdns.org
|
||||||
|
LETSENCRYPT_HOST=example.duckdns.org
|
||||||
|
APP_PORT=8000
|
||||||
|
|
||||||
|
# without 0x prefix
|
||||||
|
PRIVATE_KEY=
|
||||||
|
# 0.1 means 0.1%
|
||||||
|
RELAYER_FEE=0.1
|
||||||
|
REWARD_ACCOUNT=
|
||||||
|
CONFIRMATIONS=4
|
||||||
|
|
||||||
|
# in GWEI
|
||||||
|
MAX_GAS_PRICE=1000
|
45
.eslintrc.json
Normal file
45
.eslintrc.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2018
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
2,
|
||||||
|
{
|
||||||
|
"SwitchCase": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"linebreak-style": ["error", "unix"],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"avoidEscape": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"semi": ["error", "never"],
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"require-await": "error",
|
||||||
|
"comma-dangle": ["error", "only-multiline"],
|
||||||
|
"space-before-function-paren": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"anonymous": "always",
|
||||||
|
"named": "never",
|
||||||
|
"asyncArrow": "always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
98
.github/workflows/build.yml
vendored
Normal file
98
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
name: build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['*']
|
||||||
|
tags: ['v[0-9]+.[0-9]+.[0-9]+*']
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
- run: yarn install
|
||||||
|
- run: yarn test
|
||||||
|
- run: yarn lint
|
||||||
|
- name: Telegram Failure Notification
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
message: ❗ Build failed for [${{ github.repository }}](https://github.com/${{ github.repository }}/actions) for sidechains because of ${{ github.actor }}
|
||||||
|
format: markdown
|
||||||
|
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags')
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set vars
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
echo "::set-output name=version::$(echo ${GITHUB_REF#refs/tags/v})"
|
||||||
|
echo "::set-output name=repo_name::$(echo ${GITHUB_REPOSITORY#*/})"
|
||||||
|
|
||||||
|
- name: Check package.json version vs tag
|
||||||
|
run: |
|
||||||
|
[ ${{ steps.vars.outputs.version }} = $(sed -nE 's/^\s*"version": "([0-9]+.[0-9]+.[0-9]+(-beta.[0-9]+)?)",$/\1/p' package.json) ] || (echo "Git tag doesn't match version in package.json" && false)
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v1.1.0
|
||||||
|
with:
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
repository: tornadocash/relayer
|
||||||
|
tag_with_ref: true
|
||||||
|
tags: light
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Telegram Message Notify
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
message: 🚀 Published a [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}) for sidechains version ${{ steps.vars.outputs.version }} to docker hub
|
||||||
|
debug: true
|
||||||
|
format: markdown
|
||||||
|
|
||||||
|
- name: Telegram Relayer Channel Notification
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
with:
|
||||||
|
to: ${{ secrets.TELEGRAM_RELAYER_CHAT_ID }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
||||||
|
message: |
|
||||||
|
🚀 Published a new version of the sidechains relayer node service to docker hub: `tornadocash/relayer:v${{ steps.vars.outputs.version }}` and `tornadocash/relayer:light`.
|
||||||
|
|
||||||
|
❗️Please update your sidechains nodes ❗️
|
||||||
|
DO NOT TOUCH MAINNET AND NOVA RELAYERS.
|
||||||
|
debug: true
|
||||||
|
format: markdown
|
||||||
|
|
||||||
|
- name: Discord Relayer Channel Notification
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELAYER_WEBHOOK }}
|
||||||
|
uses: Ilshidur/action-discord@master
|
||||||
|
with:
|
||||||
|
args: |
|
||||||
|
🚀 Published a new version of the sidechains relayer node service to docker hub: `tornadocash/relayer:v${{ steps.vars.outputs.version }}` and `tornadocash/relayer:light`.
|
||||||
|
|
||||||
|
❗️Please update your sidechains nodes ❗️
|
||||||
|
DO NOT TOUCH MAINNET AND NOVA RELAYERS.
|
||||||
|
|
||||||
|
- name: Telegram Failure Notification
|
||||||
|
uses: appleboy/telegram-action@master
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
message: ❗ Failed to publish [${{ steps.vars.outputs.repo_name }}](https://github.com/${{ github.repository }}/actions) for sidechains because of ${{ github.actor }}
|
||||||
|
format: markdown
|
||||||
|
to: ${{ secrets.TELEGRAM_CHAT_ID }}
|
||||||
|
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,4 +1,9 @@
|
|||||||
node_modules
|
.vscode
|
||||||
|
node_modules/
|
||||||
.env
|
.env
|
||||||
.env*
|
.env.*
|
||||||
|
!.env.*.example
|
||||||
|
!.env.example
|
||||||
|
kovan.*
|
||||||
|
dump.rdb
|
||||||
|
.idea
|
||||||
|
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
|||||||
|
@tornado:registry=https://git.tornado.ws/api/packages/tornado-packages/npm/
|
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
keys/TreeUpdate.json
|
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 110,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
FROM node:16
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json yarn.lock ./
|
||||||
|
RUN yarn && yarn cache clean --force
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
ENTRYPOINT ["yarn"]
|
156
README.md
156
README.md
@ -1,60 +1,142 @@
|
|||||||
# 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) ![Static Badge](https://img.shields.io/badge/version-5.1.0-blue?logo=docker)
|
||||||
|
|
||||||
**\*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.**
|
***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.**
|
||||||
|
|
||||||
**\*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).**
|
***It is recommended to run your Relayer on a VPS instance ([Virtual Private Server](https://njal.la/)). 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
|
## Deploy with docker-compose
|
||||||
|
|
||||||
_The following instructions are for Ubuntu 22.10, other operating systems may vary._
|
__PREREQUISITES__
|
||||||
|
|
||||||
#### Installation:
|
1. Update core dependencies
|
||||||
|
|
||||||
Just run in terminal:
|
- `sudo apt-get update`
|
||||||
|
|
||||||
```bash
|
2. Install docker-compose
|
||||||
curl -s https://git.tornado.ws/tornadocash/tornado-relayer/raw/branch/main/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Configuring environments:
|
- `curl -SL https://github.com/docker/compose/releases/download/v2.16.0/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose`
|
||||||
|
|
||||||
|
3. Install Docker
|
||||||
|
|
||||||
|
- `curl -fsSL https://get.docker.com -o get-docker.sh && chmod +x get-docker.sh && ./get-docker.sh`
|
||||||
|
|
||||||
|
4. Install git
|
||||||
|
|
||||||
|
- `sudo apt-get install git-all`
|
||||||
|
|
||||||
|
5. Install nginx
|
||||||
|
|
||||||
|
- `sudo apt install nginx`
|
||||||
|
|
||||||
|
6. Stop apache2 instance (enabled by default)
|
||||||
|
|
||||||
|
- `sudo systemctl stop apache2`
|
||||||
|
|
||||||
|
__FIREWALL CONFIGURATION__
|
||||||
|
|
||||||
|
_* Warning: Failure to configure SSH as the first UFW rule, will lock you out of the instance_
|
||||||
|
|
||||||
|
1. Make sure UFW is installed by running `apt update` and `apt install ufw`
|
||||||
|
2. Allow SSH in the first position in UFW by running `ufw insert 1 allow ssh`*
|
||||||
|
3. Allow HTTP, and HTTPS by running `ufw allow https/tcp/http`
|
||||||
|
4. Finalize changes and enable firewall `ufw enable`
|
||||||
|
|
||||||
|
__NGINX REVERSE PROXY__
|
||||||
|
|
||||||
|
1. Copy the pre-modified nginx policy as your default policy
|
||||||
|
|
||||||
|
- `cp tornado.conf /etc/nginx/sites-available/default`
|
||||||
|
|
||||||
|
2. Append the default nginx configuration to include streams
|
||||||
|
|
||||||
|
- `echo "stream { map_hash_bucket_size 128; map_hash_max_size 128; include /etc/nginx/conf.d/streams/*.conf; }" >> /etc/nginx/nginx.conf`
|
||||||
|
|
||||||
|
3. Create the stream configuration
|
||||||
|
|
||||||
|
- `mkdir /etc/nginx/conf.d/streams && cp tornado-stream.conf /etc/nginx/conf.d/streams/tornado-stream.conf`
|
||||||
|
|
||||||
|
4. Start nginx to make sure the configuration is correct
|
||||||
|
|
||||||
|
- `sudo systemctl restart nginx`
|
||||||
|
|
||||||
|
5. Stop nginx
|
||||||
|
|
||||||
|
- `sudo systemctl stop nginx`
|
||||||
|
|
||||||
|
__DEPLOYMENT__
|
||||||
|
|
||||||
|
1. Clone the repository and enter the directory
|
||||||
|
|
||||||
|
- `git clone https://git.tornado.ws/tornadocash/classic-relayer -b sidechain-v5 && cd classic-relayer`
|
||||||
|
|
||||||
1. Go to `tornado-relayer` folder on the server home directory
|
|
||||||
2. Check environment files:
|
2. Check environment files:
|
||||||
|
|
||||||
By default each network is preconfigured the naming of `.env.<NETWORK>`
|
By default each network is preconfigured the naming of `.env.<NETWORK>`
|
||||||
|
|
||||||
- `.env.eth` for Ethereum Mainnet
|
- `.env.bsc` for Binance Smart Chain
|
||||||
- `.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
|
|
||||||
|
|
||||||
3. Configure (fill) environment files for those networks on which the relayer will be deployed:
|
- `.env.arb` for Arbitrum
|
||||||
|
|
||||||
- Set `PRIVATE_KEY` to your relayer address (remove the 0x from your private key) to each environment file
|
- `.env.op` for Optimism
|
||||||
- _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
|
|
||||||
|
|
||||||
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
|
- `.env.gnosis` for Gnosis (xdai)
|
||||||
|
|
||||||
#### Deployment:
|
- `.env.polygon` for Polygon (matic)
|
||||||
|
|
||||||
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):
|
- `.env.avax` for Avalanche C-Chain
|
||||||
|
|
||||||
- `docker-compose --profile eth --profile bsc --profile arb up -d`
|
3. Configure (fill) environment files for those networks on which the relayer will be deployed:
|
||||||
|
|
||||||
2. Visit your domain addresses and check each `/status` endpoint to ensure there is no errors in the `status` fields
|
- 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*)
|
||||||
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.
|
- 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
|
||||||
|
- 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
|
||||||
|
|
||||||
#### Disclaimer:
|
4. Build docker image for sidechain with simple `npm run build` command
|
||||||
|
|
||||||
|
5. Uncomment the `env_file` lines (remove `# `) for the associated network services in `docker-compose.yml` for chosen chains (networks)
|
||||||
|
|
||||||
|
6. Run docker-compose for the configured networks specified via `--profile <NETWORK_SYMBOL>`, for example (if you run relayer only Binance Smart Chain and Arbitrum):
|
||||||
|
|
||||||
|
- `docker-compose --profile bsc --profile arb up -d`
|
||||||
|
|
||||||
|
7. Visit your domain addresses and check each `/status` endpoint to ensure there is no errors in the `status` fields
|
||||||
|
|
||||||
|
## Run locally for one chain
|
||||||
|
|
||||||
|
1. `yarn`
|
||||||
|
2. `cp .env.example .env`
|
||||||
|
3. Modify `.env` as needed (described above)
|
||||||
|
4. `yarn start`
|
||||||
|
5. Go to `http://127.0.0.1:8000`
|
||||||
|
6. In order to execute withdraw request, you can run following command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/relay
|
||||||
|
```
|
||||||
|
|
||||||
|
Relayer should return a transaction hash
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
## Run own RPC nodes
|
||||||
|
|
||||||
|
It is strongly recommended that you use your own RPC nodes. Instruction on how to run full nodes can be found [here](https://github.com/feshchenkod/rpc-nodes).
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
1. TreeWatcher module keeps track of Account Tree changes and automatically caches the actual state in Redis and emits `treeUpdate` event to redis pub/sub channel
|
||||||
|
2. Server module is Express.js instance that accepts http requests
|
||||||
|
3. Controller contains handlers for the Server endpoints. It validates input data and adds a Job to Queue.
|
||||||
|
4. Queue module is used by Controller to put and get Job from queue (bull wrapper)
|
||||||
|
5. Status module contains handler to get a Job status. It's used by UI for pull updates
|
||||||
|
6. Validate contains validation logic for all endpoints
|
||||||
|
7. Worker is the main module that gets a Job from queue and processes it
|
||||||
|
|
||||||
|
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.
|
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.
|
||||||
|
151
abis/ovmGasPriceOracleABI.json
Normal file
151
abis/ovmGasPriceOracleABI.json
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "address", "name": "_owner", "type": "address" }],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"name": "DecimalsUpdated",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"name": "GasPriceUpdated",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"name": "L1BaseFeeUpdated",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"name": "OverheadUpdated",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{ "indexed": true, "internalType": "address", "name": "previousOwner", "type": "address" },
|
||||||
|
{ "indexed": true, "internalType": "address", "name": "newOwner", "type": "address" }
|
||||||
|
],
|
||||||
|
"name": "OwnershipTransferred",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [{ "indexed": false, "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"name": "ScalarUpdated",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "gasPrice",
|
||||||
|
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "bytes", "name": "_data", "type": "bytes" }],
|
||||||
|
"name": "getL1Fee",
|
||||||
|
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "bytes", "name": "_data", "type": "bytes" }],
|
||||||
|
"name": "getL1GasUsed",
|
||||||
|
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "l1BaseFee",
|
||||||
|
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "overhead",
|
||||||
|
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "owner",
|
||||||
|
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "renounceOwnership",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [],
|
||||||
|
"name": "scalar",
|
||||||
|
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "uint256", "name": "_decimals", "type": "uint256" }],
|
||||||
|
"name": "setDecimals",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "uint256", "name": "_gasPrice", "type": "uint256" }],
|
||||||
|
"name": "setGasPrice",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "uint256", "name": "_baseFee", "type": "uint256" }],
|
||||||
|
"name": "setL1BaseFee",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "uint256", "name": "_overhead", "type": "uint256" }],
|
||||||
|
"name": "setOverhead",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "uint256", "name": "_scalar", "type": "uint256" }],
|
||||||
|
"name": "setScalar",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }],
|
||||||
|
"name": "transferOwnership",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]
|
105
abis/proxyLightABI.json
Normal file
105
abis/proxyLightABI.json
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"internalType": "address",
|
||||||
|
"name": "sender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"internalType": "bytes",
|
||||||
|
"name": "encryptedNote",
|
||||||
|
"type": "bytes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "EncryptedNote",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "bytes[]",
|
||||||
|
"name": "_encryptedNotes",
|
||||||
|
"type": "bytes[]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "backupNotes",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "contract ITornadoInstance",
|
||||||
|
"name": "_tornado",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bytes32",
|
||||||
|
"name": "_commitment",
|
||||||
|
"type": "bytes32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"internalType": "bytes",
|
||||||
|
"name": "_encryptedNote",
|
||||||
|
"type": "bytes"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "deposit",
|
||||||
|
"outputs": [],
|
||||||
|
"stateMutability": "payable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"internalType": "contract ITornadoInstance",
|
||||||
|
"name": "_tornado",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [],
|
||||||
|
"stateMutability": "payable",
|
||||||
|
"type": "function"
|
||||||
|
}
|
||||||
|
]
|
1
app.js
Normal file
1
app.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('./src/index')
|
@ -1,12 +1,14 @@
|
|||||||
version: "2"
|
version: '2'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
redis:
|
redis:
|
||||||
image: redis
|
image: redis
|
||||||
restart: always
|
restart: always
|
||||||
command: [redis-server, --appendonly, "yes"]
|
command: [redis-server, --appendonly, 'yes']
|
||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:6379:6379'
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
image: nginx:alpine
|
||||||
@ -43,131 +45,11 @@ services:
|
|||||||
- nginx
|
- nginx
|
||||||
- dockergen
|
- dockergen
|
||||||
|
|
||||||
# ---------------------- ETH Mainnet ----------------------- #
|
|
||||||
|
|
||||||
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 (Binance Smart Chain) ----------------------- #
|
||||||
|
|
||||||
bsc-server:
|
bsc-server:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["bsc"]
|
profiles: ['bsc']
|
||||||
restart: always
|
restart: always
|
||||||
command: server
|
command: server
|
||||||
env_file: .env.bsc
|
env_file: .env.bsc
|
||||||
@ -179,7 +61,7 @@ services:
|
|||||||
|
|
||||||
bsc-healthWatcher:
|
bsc-healthWatcher:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["bsc"]
|
profiles: ['bsc']
|
||||||
restart: always
|
restart: always
|
||||||
command: healthWatcher
|
command: healthWatcher
|
||||||
env_file: .env.bsc
|
env_file: .env.bsc
|
||||||
@ -190,7 +72,7 @@ services:
|
|||||||
|
|
||||||
bsc-worker1:
|
bsc-worker1:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["bsc"]
|
profiles: ['bsc']
|
||||||
restart: always
|
restart: always
|
||||||
command: worker
|
command: worker
|
||||||
env_file: .env.bsc
|
env_file: .env.bsc
|
||||||
@ -205,7 +87,7 @@ services:
|
|||||||
|
|
||||||
polygon-server:
|
polygon-server:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["polygon"]
|
profiles: ['polygon']
|
||||||
restart: always
|
restart: always
|
||||||
command: server
|
command: server
|
||||||
env_file: .env.polygon
|
env_file: .env.polygon
|
||||||
@ -217,7 +99,7 @@ services:
|
|||||||
|
|
||||||
polygon-healthWatcher:
|
polygon-healthWatcher:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["polygon"]
|
profiles: ['polygon']
|
||||||
restart: always
|
restart: always
|
||||||
command: healthWatcher
|
command: healthWatcher
|
||||||
env_file: .env.polygon
|
env_file: .env.polygon
|
||||||
@ -228,7 +110,7 @@ services:
|
|||||||
|
|
||||||
polygon-worker1:
|
polygon-worker1:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["polygon"]
|
profiles: ['polygon']
|
||||||
restart: always
|
restart: always
|
||||||
command: worker
|
command: worker
|
||||||
env_file: .env.polygon
|
env_file: .env.polygon
|
||||||
@ -243,7 +125,7 @@ services:
|
|||||||
|
|
||||||
gnosis-server:
|
gnosis-server:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["gnosis"]
|
profiles: ['gnosis']
|
||||||
restart: always
|
restart: always
|
||||||
command: server
|
command: server
|
||||||
env_file: .env.gnosis
|
env_file: .env.gnosis
|
||||||
@ -255,7 +137,7 @@ services:
|
|||||||
|
|
||||||
gnosis-healthWatcher:
|
gnosis-healthWatcher:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["gnosis"]
|
profiles: ['gnosis']
|
||||||
restart: always
|
restart: always
|
||||||
command: healthWatcher
|
command: healthWatcher
|
||||||
env_file: .env.gnosis
|
env_file: .env.gnosis
|
||||||
@ -266,7 +148,7 @@ services:
|
|||||||
|
|
||||||
gnosis-worker1:
|
gnosis-worker1:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["gnosis"]
|
profiles: ['gnosis']
|
||||||
restart: always
|
restart: always
|
||||||
command: worker
|
command: worker
|
||||||
env_file: .env.gnosis
|
env_file: .env.gnosis
|
||||||
@ -281,7 +163,7 @@ services:
|
|||||||
|
|
||||||
avax-server:
|
avax-server:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["avax"]
|
profiles: ['avax']
|
||||||
restart: always
|
restart: always
|
||||||
command: server
|
command: server
|
||||||
env_file: .env.avax
|
env_file: .env.avax
|
||||||
@ -293,7 +175,7 @@ services:
|
|||||||
|
|
||||||
avax-healthWatcher:
|
avax-healthWatcher:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["avax"]
|
profiles: ['avax']
|
||||||
restart: always
|
restart: always
|
||||||
command: healthWatcher
|
command: healthWatcher
|
||||||
env_file: .env.avax
|
env_file: .env.avax
|
||||||
@ -304,7 +186,7 @@ services:
|
|||||||
|
|
||||||
avax-worker1:
|
avax-worker1:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["avax"]
|
profiles: ['avax']
|
||||||
restart: always
|
restart: always
|
||||||
command: worker
|
command: worker
|
||||||
env_file: .env.avax
|
env_file: .env.avax
|
||||||
@ -319,7 +201,7 @@ services:
|
|||||||
|
|
||||||
op-server:
|
op-server:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["op"]
|
profiles: ['op']
|
||||||
restart: always
|
restart: always
|
||||||
command: server
|
command: server
|
||||||
env_file: .env.op
|
env_file: .env.op
|
||||||
@ -331,7 +213,7 @@ services:
|
|||||||
|
|
||||||
op-healthWatcher:
|
op-healthWatcher:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["op"]
|
profiles: ['op']
|
||||||
restart: always
|
restart: always
|
||||||
command: healthWatcher
|
command: healthWatcher
|
||||||
env_file: .env.op
|
env_file: .env.op
|
||||||
@ -342,7 +224,7 @@ services:
|
|||||||
|
|
||||||
op-worker1:
|
op-worker1:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["op"]
|
profiles: ['op']
|
||||||
restart: always
|
restart: always
|
||||||
command: worker
|
command: worker
|
||||||
env_file: .env.op
|
env_file: .env.op
|
||||||
@ -357,7 +239,7 @@ services:
|
|||||||
|
|
||||||
arb-server:
|
arb-server:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["arb"]
|
profiles: ['arb']
|
||||||
restart: always
|
restart: always
|
||||||
command: server
|
command: server
|
||||||
env_file: .env.arb
|
env_file: .env.arb
|
||||||
@ -369,7 +251,7 @@ services:
|
|||||||
|
|
||||||
arb-healthWatcher:
|
arb-healthWatcher:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["arb"]
|
profiles: ['arb']
|
||||||
restart: always
|
restart: always
|
||||||
command: healthWatcher
|
command: healthWatcher
|
||||||
env_file: .env.arb
|
env_file: .env.arb
|
||||||
@ -380,7 +262,7 @@ services:
|
|||||||
|
|
||||||
arb-worker1:
|
arb-worker1:
|
||||||
image: tornadocash/relayer:sidechain-v5
|
image: tornadocash/relayer:sidechain-v5
|
||||||
profiles: ["arb"]
|
profiles: ['arb']
|
||||||
restart: always
|
restart: always
|
||||||
command: worker
|
command: worker
|
||||||
env_file: .env.arb
|
env_file: .env.arb
|
||||||
@ -391,78 +273,6 @@ services:
|
|||||||
|
|
||||||
# -------------------------------------------------- #
|
# -------------------------------------------------- #
|
||||||
|
|
||||||
# ---------------------- 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]
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
conf:
|
conf:
|
||||||
vhost:
|
vhost:
|
||||||
|
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;
|
|
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "relay",
|
||||||
|
"version": "5.2.2",
|
||||||
|
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
|
||||||
|
"scripts": {
|
||||||
|
"server": "node src/server.js",
|
||||||
|
"worker": "node src/worker",
|
||||||
|
"healthWatcher": "node src/healthWatcher",
|
||||||
|
"eslint": "eslint --ext .js --ignore-path .gitignore .",
|
||||||
|
"prettier:check": "npx prettier --check . --config .prettierrc",
|
||||||
|
"prettier:fix": "npx prettier --write . --config .prettierrc",
|
||||||
|
"lint": "yarn eslint && yarn prettier:check",
|
||||||
|
"test": "mocha",
|
||||||
|
"build": "docker build -t tornadocash/relayer:sidechain-v5 .",
|
||||||
|
"start": "docker-compose up -d redis && concurrently \"yarn server\" \"yarn worker\" \"yarn healthWatcher\""
|
||||||
|
},
|
||||||
|
"author": "tornado.cash",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tornado/tornado-config": "^1.0.8",
|
||||||
|
"@tornado/tornado-oracles": "^3.3.0",
|
||||||
|
"@tornado/tx-manager": "^0.4.9",
|
||||||
|
"ajv": "^6.12.5",
|
||||||
|
"bull": "^3.12.1",
|
||||||
|
"concurrently": "^8.2.0",
|
||||||
|
"dotenv": "^8.2.0",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"ioredis": "^4.14.1",
|
||||||
|
"uuid": "^8.3.0",
|
||||||
|
"web3": "^1.3.0",
|
||||||
|
"web3-utils": "^1.2.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"eslint": "^6.6.0",
|
||||||
|
"eslint-config-prettier": "^6.12.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.4",
|
||||||
|
"mocha": "^8.1.3",
|
||||||
|
"prettier": "^2.1.2"
|
||||||
|
}
|
||||||
|
}
|
27
src/config.js
Normal file
27
src/config.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
|
const tornConfig = require('@tornado/tornado-config')
|
||||||
|
|
||||||
|
const { networkConfig } = require('./constants')
|
||||||
|
|
||||||
|
const netId = Number(process.env.NET_ID) || 56
|
||||||
|
const instances = tornConfig.instances[netId]
|
||||||
|
const proxyLight = tornConfig.tornadoProxyLight.address
|
||||||
|
const { gasPrices, nativeCurrency } = networkConfig[`netId${netId}`]
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
netId,
|
||||||
|
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
|
||||||
|
httpRpcUrl: process.env.HTTP_RPC_URL,
|
||||||
|
oracleRpcUrl: process.env.ORACLE_RPC_URL || 'https://rpc.payload.de',
|
||||||
|
minerMerkleTreeHeight: 20,
|
||||||
|
privateKey: process.env.PRIVATE_KEY,
|
||||||
|
instances,
|
||||||
|
port: process.env.APP_PORT || 8000,
|
||||||
|
tornadoServiceFee: Number(process.env.RELAYER_FEE),
|
||||||
|
rewardAccount: process.env.REWARD_ACCOUNT,
|
||||||
|
gasPrices,
|
||||||
|
proxyLight,
|
||||||
|
nativeCurrency,
|
||||||
|
minimumBalance: netId === 137 || netId === 43114 ? '10000000000000000000' : '100000000000000000', // 10 or 0.1
|
||||||
|
}
|
78
src/constants.js
Normal file
78
src/constants.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
const jobType = Object.freeze({
|
||||||
|
TORNADO_WITHDRAW: 'TORNADO_WITHDRAW',
|
||||||
|
OP_TORNADO_WITHDRAW: 'OP_TORNADO_WITHDRAW',
|
||||||
|
ARB_TORNADO_WITHDRAW: 'ARB_TORNADO_WITHDRAW',
|
||||||
|
})
|
||||||
|
|
||||||
|
const status = Object.freeze({
|
||||||
|
QUEUED: 'QUEUED',
|
||||||
|
ACCEPTED: 'ACCEPTED',
|
||||||
|
SENT: 'SENT',
|
||||||
|
MINED: 'MINED',
|
||||||
|
RESUBMITTED: 'RESUBMITTED',
|
||||||
|
CONFIRMED: 'CONFIRMED',
|
||||||
|
FAILED: 'FAILED',
|
||||||
|
})
|
||||||
|
|
||||||
|
const networkConfig = {
|
||||||
|
netId56: {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 5,
|
||||||
|
fast: 5,
|
||||||
|
standard: 5,
|
||||||
|
low: 5,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'bnb',
|
||||||
|
},
|
||||||
|
netId10: {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 0.001,
|
||||||
|
fast: 0.001,
|
||||||
|
standard: 0.001,
|
||||||
|
low: 0.001,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'eth',
|
||||||
|
},
|
||||||
|
netId100: {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 6,
|
||||||
|
fast: 5,
|
||||||
|
standard: 4,
|
||||||
|
low: 1,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'xdai',
|
||||||
|
},
|
||||||
|
netId137: {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 100,
|
||||||
|
fast: 75,
|
||||||
|
standard: 50,
|
||||||
|
low: 30,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'matic',
|
||||||
|
},
|
||||||
|
netId42161: {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 4,
|
||||||
|
fast: 3,
|
||||||
|
standard: 2.52,
|
||||||
|
low: 2.29,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'eth',
|
||||||
|
},
|
||||||
|
netId43114: {
|
||||||
|
gasPrices: {
|
||||||
|
instant: 225,
|
||||||
|
fast: 35,
|
||||||
|
standard: 25,
|
||||||
|
low: 25,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'avax',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
jobType,
|
||||||
|
status,
|
||||||
|
networkConfig,
|
||||||
|
}
|
21
src/controller.js
Normal file
21
src/controller.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const { getTornadoWithdrawInputError } = require('./modules/validator')
|
||||||
|
const { postJob } = require('./queue')
|
||||||
|
const { jobType } = require('./constants')
|
||||||
|
|
||||||
|
async function tornadoWithdraw(req, res) {
|
||||||
|
const inputError = getTornadoWithdrawInputError(req.body)
|
||||||
|
if (inputError) {
|
||||||
|
console.log('Invalid input:', inputError)
|
||||||
|
return res.status(400).json({ error: inputError })
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = await postJob({
|
||||||
|
type: jobType.TORNADO_WITHDRAW,
|
||||||
|
request: req.body,
|
||||||
|
})
|
||||||
|
return res.json({ id })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
tornadoWithdraw,
|
||||||
|
}
|
27
src/healthWatcher.js
Normal file
27
src/healthWatcher.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
const Web3 = require('web3')
|
||||||
|
const { toBN, fromWei } = require('web3-utils')
|
||||||
|
const { setSafeInterval, logRelayerError } = require('./utils')
|
||||||
|
const { httpRpcUrl, privateKey, minimumBalance, nativeCurrency, instances } = require('./config')
|
||||||
|
|
||||||
|
const web3 = new Web3(httpRpcUrl)
|
||||||
|
const { redis } = require('./modules/redis')
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const { address } = web3.eth.accounts.privateKeyToAccount(privateKey)
|
||||||
|
const balance = await web3.eth.getBalance(address)
|
||||||
|
|
||||||
|
if (toBN(balance).lt(toBN(minimumBalance))) {
|
||||||
|
const currency = instances[nativeCurrency].symbol
|
||||||
|
throw new Error(`Not enough balance, less than ${fromWei(minimumBalance)} ${currency}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await redis.hset('health', { status: true, error: '' })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('healthWatcher', e.message)
|
||||||
|
await logRelayerError(redis, e)
|
||||||
|
await redis.hset('health', { status: false, error: e.message })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSafeInterval(main, 30 * 1000)
|
9
src/modules/redis.js
Normal file
9
src/modules/redis.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const { createClient } = require('ioredis')
|
||||||
|
const { redisUrl } = require('../config')
|
||||||
|
|
||||||
|
const redis = createClient(redisUrl)
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
redis,
|
||||||
|
redisUrl,
|
||||||
|
}
|
200
src/modules/validator.js
Normal file
200
src/modules/validator.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
const { isAddress, toChecksumAddress } = require('web3-utils')
|
||||||
|
const { getInstance } = require('../utils')
|
||||||
|
const { rewardAccount } = require('../config')
|
||||||
|
|
||||||
|
const Ajv = require('ajv')
|
||||||
|
const ajv = new Ajv({ format: 'fast' })
|
||||||
|
|
||||||
|
ajv.addKeyword('isAddress', {
|
||||||
|
validate: (schema, data) => {
|
||||||
|
try {
|
||||||
|
return isAddress(data)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errors: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
ajv.addKeyword('isKnownContract', {
|
||||||
|
validate: (schema, data) => {
|
||||||
|
try {
|
||||||
|
return !!getInstance(data)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errors: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
ajv.addKeyword('isFeeRecipient', {
|
||||||
|
validate: (schema, data) => {
|
||||||
|
try {
|
||||||
|
return toChecksumAddress(rewardAccount) === toChecksumAddress(data)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errors: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$', isAddress: true }
|
||||||
|
const proofType = { type: 'string', pattern: '^0x[a-fA-F0-9]{512}$' }
|
||||||
|
const encryptedAccountType = { type: 'string', pattern: '^0x[a-fA-F0-9]{392}$' }
|
||||||
|
const bytes32Type = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' }
|
||||||
|
const instanceType = { ...addressType, isKnownContract: true }
|
||||||
|
const relayerType = { ...addressType, isFeeRecipient: true }
|
||||||
|
|
||||||
|
const tornadoWithdrawSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
proof: proofType,
|
||||||
|
contract: instanceType,
|
||||||
|
args: {
|
||||||
|
type: 'array',
|
||||||
|
maxItems: 6,
|
||||||
|
minItems: 6,
|
||||||
|
items: [bytes32Type, bytes32Type, addressType, relayerType, bytes32Type, bytes32Type],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['proof', 'contract', 'args'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const miningRewardSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
proof: proofType,
|
||||||
|
args: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
rate: bytes32Type,
|
||||||
|
fee: bytes32Type,
|
||||||
|
instance: instanceType,
|
||||||
|
rewardNullifier: bytes32Type,
|
||||||
|
extDataHash: bytes32Type,
|
||||||
|
depositRoot: bytes32Type,
|
||||||
|
withdrawalRoot: bytes32Type,
|
||||||
|
extData: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
relayer: relayerType,
|
||||||
|
encryptedAccount: encryptedAccountType,
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['relayer', 'encryptedAccount'],
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
inputRoot: bytes32Type,
|
||||||
|
inputNullifierHash: bytes32Type,
|
||||||
|
outputRoot: bytes32Type,
|
||||||
|
outputPathIndices: bytes32Type,
|
||||||
|
outputCommitment: bytes32Type,
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: [
|
||||||
|
'inputRoot',
|
||||||
|
'inputNullifierHash',
|
||||||
|
'outputRoot',
|
||||||
|
'outputPathIndices',
|
||||||
|
'outputCommitment',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: [
|
||||||
|
'rate',
|
||||||
|
'fee',
|
||||||
|
'instance',
|
||||||
|
'rewardNullifier',
|
||||||
|
'extDataHash',
|
||||||
|
'depositRoot',
|
||||||
|
'withdrawalRoot',
|
||||||
|
'extData',
|
||||||
|
'account',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['proof', 'args'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const miningWithdrawSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
proof: proofType,
|
||||||
|
args: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
amount: bytes32Type,
|
||||||
|
extDataHash: bytes32Type,
|
||||||
|
extData: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fee: bytes32Type,
|
||||||
|
recipient: addressType,
|
||||||
|
relayer: relayerType,
|
||||||
|
encryptedAccount: encryptedAccountType,
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['fee', 'relayer', 'encryptedAccount', 'recipient'],
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
inputRoot: bytes32Type,
|
||||||
|
inputNullifierHash: bytes32Type,
|
||||||
|
outputRoot: bytes32Type,
|
||||||
|
outputPathIndices: bytes32Type,
|
||||||
|
outputCommitment: bytes32Type,
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: [
|
||||||
|
'inputRoot',
|
||||||
|
'inputNullifierHash',
|
||||||
|
'outputRoot',
|
||||||
|
'outputPathIndices',
|
||||||
|
'outputCommitment',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['amount', 'extDataHash', 'extData', 'account'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['proof', 'args'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema)
|
||||||
|
const validateMiningReward = ajv.compile(miningRewardSchema)
|
||||||
|
const validateMiningWithdraw = ajv.compile(miningWithdrawSchema)
|
||||||
|
|
||||||
|
function getInputError(validator, data) {
|
||||||
|
validator(data)
|
||||||
|
if (validator.errors) {
|
||||||
|
const error = validator.errors[0]
|
||||||
|
return `${error.dataPath} ${error.message}`
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTornadoWithdrawInputError(data) {
|
||||||
|
return getInputError(validateTornadoWithdraw, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMiningRewardInputError(data) {
|
||||||
|
return getInputError(validateMiningReward, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMiningWithdrawInputError(data) {
|
||||||
|
return getInputError(validateMiningWithdraw, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getTornadoWithdrawInputError,
|
||||||
|
getMiningRewardInputError,
|
||||||
|
getMiningWithdrawInputError,
|
||||||
|
}
|
57
src/queue.js
Normal file
57
src/queue.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
const { v4: uuid } = require('uuid')
|
||||||
|
const Queue = require('bull')
|
||||||
|
|
||||||
|
const { netId } = require('./config')
|
||||||
|
const { status } = require('./constants')
|
||||||
|
const { redis, redisUrl } = require('./modules/redis')
|
||||||
|
|
||||||
|
const queue = new Queue(`proofs_${netId}`, redisUrl, {
|
||||||
|
lockDuration: 300000, // Key expiration time for job locks.
|
||||||
|
lockRenewTime: 30000, // Interval on which to acquire the job lock
|
||||||
|
stalledInterval: 30000, // How often check for stalled jobs (use 0 for never checking).
|
||||||
|
maxStalledCount: 3, // Max amount of times a stalled job will be re-processed.
|
||||||
|
guardInterval: 5000, // Poll interval for delayed jobs and added jobs.
|
||||||
|
retryProcessDelay: 5000, // delay before processing next job in case of internal error.
|
||||||
|
drainDelay: 5, // A timeout for when the queue is in drained state (empty waiting for jobs).
|
||||||
|
})
|
||||||
|
|
||||||
|
async function postJob({ type, request }) {
|
||||||
|
const id = uuid()
|
||||||
|
|
||||||
|
const job = await queue.add(
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
status: status.QUEUED,
|
||||||
|
...request, // proof, args, ?contract
|
||||||
|
},
|
||||||
|
{
|
||||||
|
//removeOnComplete: true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await redis.set(`job:${id}`, job.id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJob(uuid) {
|
||||||
|
const id = await redis.get(`job:${uuid}`)
|
||||||
|
return queue.getJobFromId(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJobStatus(uuid) {
|
||||||
|
const job = await getJob(uuid)
|
||||||
|
if (!job) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...job.data,
|
||||||
|
failedReason: job.failedReason,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
postJob,
|
||||||
|
getJob,
|
||||||
|
getJobStatus,
|
||||||
|
queue,
|
||||||
|
}
|
29
src/router.js
Normal file
29
src/router.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const controller = require('./controller')
|
||||||
|
const status = require('./status')
|
||||||
|
const router = require('express').Router()
|
||||||
|
|
||||||
|
// Add CORS headers
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
res.header('X-Frame-Options', 'DENY')
|
||||||
|
res.header('Access-Control-Allow-Origin', '*')
|
||||||
|
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept')
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Log error to console but don't send it to the client to avoid leaking data
|
||||||
|
router.use((err, req, res, next) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.get('/', status.index)
|
||||||
|
router.get('/v1/status', status.status)
|
||||||
|
router.get('/v1/jobs/:id', status.getJob)
|
||||||
|
router.post('/v1/tornadoWithdraw', controller.tornadoWithdraw)
|
||||||
|
router.get('/status', status.status)
|
||||||
|
router.post('/relay', controller.tornadoWithdraw)
|
||||||
|
|
||||||
|
module.exports = router
|
16
src/server.js
Normal file
16
src/server.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const router = require('./router')
|
||||||
|
const { port, rewardAccount } = require('./config')
|
||||||
|
const { version } = require('../package.json')
|
||||||
|
const { isAddress } = require('web3-utils')
|
||||||
|
|
||||||
|
if (!isAddress(rewardAccount)) {
|
||||||
|
throw new Error('No REWARD_ACCOUNT specified')
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
app.use(express.json())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.listen(port)
|
||||||
|
console.log(`Relayer ${version} started on port ${port}`)
|
38
src/status.js
Normal file
38
src/status.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const queue = require('./queue')
|
||||||
|
const { netId, tornadoServiceFee, instances, rewardAccount } = require('./config')
|
||||||
|
const { version } = require('../package.json')
|
||||||
|
const { readRelayerErrors } = require('./utils')
|
||||||
|
const { redis } = require('./modules/redis')
|
||||||
|
|
||||||
|
async function status(req, res) {
|
||||||
|
const health = await redis.hgetall('health')
|
||||||
|
health.errorsLog = await readRelayerErrors(redis)
|
||||||
|
const { waiting: currentQueue } = await queue.queue.getJobCounts()
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
rewardAccount,
|
||||||
|
instances,
|
||||||
|
netId,
|
||||||
|
tornadoServiceFee,
|
||||||
|
version,
|
||||||
|
health,
|
||||||
|
currentQueue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function index(req, res) {
|
||||||
|
res.send(
|
||||||
|
'This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/v1/status>/status</a> for settings',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getJob(req, res) {
|
||||||
|
const status = await queue.getJobStatus(req.params.id)
|
||||||
|
return status ? res.json(status) : res.status(400).json({ error: "The job doesn't exist" })
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
status,
|
||||||
|
index,
|
||||||
|
getJob,
|
||||||
|
}
|
107
src/utils.js
Normal file
107
src/utils.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
const { instances } = require('./config')
|
||||||
|
const { toChecksumAddress, BN } = require('web3-utils')
|
||||||
|
|
||||||
|
const addressMap = new Map()
|
||||||
|
for (const [currency, { instanceAddress, symbol, decimals }] of Object.entries(instances)) {
|
||||||
|
Object.entries(instanceAddress).forEach(([amount, address]) =>
|
||||||
|
addressMap.set(address, {
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
symbol,
|
||||||
|
decimals,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInstance(address) {
|
||||||
|
address = toChecksumAddress(address)
|
||||||
|
const key = toChecksumAddress(address)
|
||||||
|
if (addressMap.has(key)) {
|
||||||
|
return addressMap.get(key)
|
||||||
|
} else {
|
||||||
|
throw new Error('Unknown contact address')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSafeInterval(func, interval) {
|
||||||
|
func()
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => {
|
||||||
|
setTimeout(() => setSafeInterval(func, interval), interval)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const logRelayerError = async (redis, e) => {
|
||||||
|
await redis.zadd('errors', 'INCR', 1, e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const readRelayerErrors = async redis => {
|
||||||
|
const set = await redis.zrevrange('errors', 0, -1, 'WITHSCORES')
|
||||||
|
const errors = []
|
||||||
|
while (set.length) {
|
||||||
|
const [message, score] = set.splice(0, 2)
|
||||||
|
errors.push({ message, score })
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearRelayerErrors = redis => {
|
||||||
|
redis.del('errors')
|
||||||
|
}
|
||||||
|
module.exports = {
|
||||||
|
getInstance,
|
||||||
|
setSafeInterval,
|
||||||
|
fromDecimals,
|
||||||
|
logRelayerError,
|
||||||
|
readRelayerErrors,
|
||||||
|
clearRelayerErrors,
|
||||||
|
}
|
163
src/worker.js
Normal file
163
src/worker.js
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
const Web3 = require('web3')
|
||||||
|
const { toBN, fromWei, isAddress, toChecksumAddress } = require('web3-utils')
|
||||||
|
const { redis } = require('./modules/redis')
|
||||||
|
const proxyLightABI = require('../abis/proxyLightABI.json')
|
||||||
|
const { queue } = require('./queue')
|
||||||
|
const { getInstance, logRelayerError, clearRelayerErrors } = require('./utils')
|
||||||
|
const { jobType, status } = require('./constants')
|
||||||
|
const { netId, gasPrices, privateKey, proxyLight, httpRpcUrl, tornadoServiceFee } = require('./config')
|
||||||
|
const { TxManager } = require('@tornado/tx-manager')
|
||||||
|
const { TornadoFeeOracleV5 } = require('@tornado/tornado-oracles')
|
||||||
|
|
||||||
|
let web3
|
||||||
|
let currentTx
|
||||||
|
let currentJob
|
||||||
|
let txManager
|
||||||
|
let tornadoProxyInstance
|
||||||
|
const feeOracle = new TornadoFeeOracleV5(netId, httpRpcUrl, gasPrices)
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
try {
|
||||||
|
web3 = new Web3(httpRpcUrl)
|
||||||
|
tornadoProxyInstance = new web3.eth.Contract(proxyLightABI, proxyLight)
|
||||||
|
clearRelayerErrors(redis)
|
||||||
|
const { CONFIRMATIONS, MAX_GAS_PRICE } = process.env
|
||||||
|
|
||||||
|
const gasPriceOracleConfig = {
|
||||||
|
chainId: netId,
|
||||||
|
defaultRpc: httpRpcUrl,
|
||||||
|
defaultFallbackGasPrices: gasPrices,
|
||||||
|
minPriority: 0.05,
|
||||||
|
percentile: 5,
|
||||||
|
blocksCount: 20,
|
||||||
|
}
|
||||||
|
txManager = new TxManager({
|
||||||
|
privateKey,
|
||||||
|
rpcUrl: httpRpcUrl,
|
||||||
|
config: { CONFIRMATIONS, MAX_GAS_PRICE, THROW_ON_REVERT: false },
|
||||||
|
gasPriceOracleConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
queue.process(processJob)
|
||||||
|
console.log('Worker started')
|
||||||
|
} catch (e) {
|
||||||
|
logRelayerError(redis, e)
|
||||||
|
console.error('error on start worker', e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkTornadoFee({ data }, tx) {
|
||||||
|
const userProvidedFee = toBN(data.args[4])
|
||||||
|
const { amount, decimals, currency } = getInstance(data.contract)
|
||||||
|
|
||||||
|
const relayerDesiredFee = await feeOracle.calculateWithdrawalFeeViaRelayer({
|
||||||
|
tx,
|
||||||
|
txType: 'relayer_withdrawal',
|
||||||
|
relayerFeePercent: tornadoServiceFee,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
decimals,
|
||||||
|
gasLimit: tx.gasLimit,
|
||||||
|
gasPrice: tx.gasPrice,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'user-provided fee, desired fee',
|
||||||
|
fromWei(userProvidedFee.toString()),
|
||||||
|
fromWei(toBN(relayerDesiredFee).toString()),
|
||||||
|
)
|
||||||
|
if (userProvidedFee.lt(toBN(relayerDesiredFee))) {
|
||||||
|
throw new Error('Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTxObject({ data }) {
|
||||||
|
const calldata = tornadoProxyInstance.methods.withdraw(data.contract, data.proof, ...data.args).encodeABI()
|
||||||
|
|
||||||
|
const incompleteTx = {
|
||||||
|
value: data.args[5],
|
||||||
|
to: tornadoProxyInstance._address,
|
||||||
|
data: calldata,
|
||||||
|
}
|
||||||
|
const { gasLimit, gasPrice } = await feeOracle.getGasParams({
|
||||||
|
tx: incompleteTx,
|
||||||
|
txType: 'relayer_withdrawal',
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...incompleteTx, gasLimit, gasPrice }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkRecipient({ data }) {
|
||||||
|
const recipient = data.args[2]
|
||||||
|
if (!isAddress(recipient)) throw new Error('Recipient address is invalid')
|
||||||
|
|
||||||
|
const addressCode = await web3.eth.getCode(toChecksumAddress(recipient))
|
||||||
|
if (addressCode !== '0x') throw new Error('Recipient cannot be a smart-contract, only EOA')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processJob(job) {
|
||||||
|
try {
|
||||||
|
if (!jobType[job.data.type]) {
|
||||||
|
throw new Error(`Unknown job type: ${job.data.type}`)
|
||||||
|
}
|
||||||
|
currentJob = job
|
||||||
|
await updateStatus(status.ACCEPTED)
|
||||||
|
console.log(`Start processing a new ${job.data.type} job #${job.id}`)
|
||||||
|
await submitTx(job)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('processJob', e.message)
|
||||||
|
await updateStatus(status.FAILED)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTx(job) {
|
||||||
|
await checkRecipient(job)
|
||||||
|
const tx = await getTxObject(job)
|
||||||
|
await checkTornadoFee(job, tx)
|
||||||
|
currentTx = await txManager.createTx(tx)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const receipt = await currentTx
|
||||||
|
.send()
|
||||||
|
.on('transactionHash', txHash => {
|
||||||
|
updateTxHash(txHash)
|
||||||
|
updateStatus(status.SENT)
|
||||||
|
})
|
||||||
|
.on('mined', receipt => {
|
||||||
|
console.log('Mined in block', receipt.blockNumber)
|
||||||
|
updateStatus(status.MINED)
|
||||||
|
})
|
||||||
|
.on('confirmations', updateConfirmations)
|
||||||
|
|
||||||
|
if (receipt.status === 1) {
|
||||||
|
await updateStatus(status.CONFIRMED)
|
||||||
|
} else {
|
||||||
|
throw new Error('Submitted transaction failed')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// todo this could result in duplicated error logs
|
||||||
|
// todo handle a case where account tree is still not up to date (wait and retry)?
|
||||||
|
throw new Error(`Revert by smart contract ${e.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTxHash(txHash) {
|
||||||
|
console.log(`A new successfully sent tx ${txHash}`)
|
||||||
|
currentJob.data.txHash = txHash
|
||||||
|
await currentJob.update(currentJob.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateConfirmations(confirmations) {
|
||||||
|
console.log(`Confirmations count ${confirmations}`)
|
||||||
|
currentJob.data.confirmations = confirmations
|
||||||
|
await currentJob.update(currentJob.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateStatus(status) {
|
||||||
|
console.log(`Job status updated ${status}`)
|
||||||
|
currentJob.data.status = status
|
||||||
|
await currentJob.update(currentJob.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
start()
|
54
test/validator.js
Normal file
54
test/validator.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
require('chai').should()
|
||||||
|
|
||||||
|
const { getTornadoWithdrawInputError } = require('../src/modules/validator')
|
||||||
|
|
||||||
|
describe('Validator', () => {
|
||||||
|
describe('#getTornadoWithdrawInputError', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
getTornadoWithdrawInputError(withdrawData)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw for incorrect proof', () => {
|
||||||
|
const malformedData = { ...withdrawData }
|
||||||
|
malformedData.proof = '0xbeef'
|
||||||
|
getTornadoWithdrawInputError(malformedData).should.be.equal(
|
||||||
|
'.proof should match pattern "^0x[a-fA-F0-9]{512}$"',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should throw if unknown contract', () => {
|
||||||
|
const malformedData = { ...withdrawData }
|
||||||
|
malformedData.contract = '0xf17f52151ebef6c7334fad080c5704d77216b732'
|
||||||
|
getTornadoWithdrawInputError(malformedData).should.be.equal(
|
||||||
|
'.contract should pass "isKnownContract" keyword validation',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
it('should throw something is missing', () => {
|
||||||
|
const malformedData = { ...withdrawData }
|
||||||
|
delete malformedData.proof
|
||||||
|
getTornadoWithdrawInputError(malformedData).should.be.equal(" should have required property 'proof'")
|
||||||
|
malformedData.proof = withdrawData.proof
|
||||||
|
|
||||||
|
delete malformedData.args
|
||||||
|
getTornadoWithdrawInputError(malformedData).should.be.equal(" should have required property 'args'")
|
||||||
|
malformedData.args = withdrawData.args
|
||||||
|
|
||||||
|
delete malformedData.contract
|
||||||
|
getTornadoWithdrawInputError(malformedData).should.be.equal(" should have required property 'contract'")
|
||||||
|
malformedData.contract = withdrawData.contract
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const withdrawData = {
|
||||||
|
proof:
|
||||||
|
'0x0f8cb4c2ca9cbb23a5f21475773e19e39d3470436d7296f25c8730d19d88fcef2986ec694ad094f4c5fff79a4e5043bd553df20b23108bc023ec3670718143c20cc49c6d9798e1ae831fd32a878b96ff8897728f9b7963f0d5a4b5574426ac6203b2456d360b8e825d8f5731970bf1fc1b95b9713e3b24203667ecdd5939c2e40dec48f9e51d9cc8dc2f7f3916f0e9e31519c7df2bea8c51a195eb0f57beea4924cb846deaa78cdcbe361a6c310638af6f6157317bc27d74746bfaa2e1f8d2e9088fd10fa62100740874cdffdd6feb15c95c5a303f6bc226d5e51619c5b825471a17ddfeb05b250c0802261f7d05cf29a39a72c13e200e5bc721b0e4c50d55e6',
|
||||||
|
args: [
|
||||||
|
'0x1579d41e5290ab5bcec9a7df16705e49b5c0b869095299196c19c5e14462c9e3',
|
||||||
|
'0x0cf7f49c5b35c48b9e1d43713e0b46a75977e3d10521e9ac1e4c3cd5e3da1c5d',
|
||||||
|
'0xbd4369dc854c5d5b79fe25492e3a3cfcb5d02da5',
|
||||||
|
'0x0000000000000000000000000000000000000000',
|
||||||
|
'0x000000000000000000000000000000000000000000000000058d15e176280000',
|
||||||
|
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||||
|
],
|
||||||
|
contract: '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user