Compare commits

..

No commits in common. "main" and "nova" have entirely different histories.
main ... nova

60 changed files with 12261 additions and 731 deletions

3
.dockerignore Normal file

@ -0,0 +1,3 @@
node_modules
.env
.git

30
.env.example Normal file

@ -0,0 +1,30 @@
# DNS settings
VIRTUAL_HOST=
LETSENCRYPT_HOST=
# server settings
PORT=8000
# for production
REDIS_URL=redis://redis/0
# for development
# REDIS_URL=redis://127.0.0.1:6379
CHAIN_ID=100
RPC_URL=https://rpc.gnosischain.com
# ORACLE_RPC_URL should always point to the mainnet
ORACLE_RPC_URL=https://rpc.payload.de
REWARD_ADDRESS=
PRIVATE_KEY=
CONFIRMATIONS=4
MAX_GAS_PRICE=100
# commission for service
# transfer fee is a fixed value in ether, 0.01 means 0.01 ether
TRANSFER_SERVICE_FEE=0.00000001
# withdrawal fee is a percentage of the amount, 0.05 means 0.05%
WITHDRAWAL_SERVICE_FEE=0.05

24
.eslintrc.js Normal file

@ -0,0 +1,24 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

95
.github/workflows/build.yml vendored Normal file

@ -0,0 +1,95 @@
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 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) 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 }} = $(grep '"version":' package.json | grep -o "[0-9.]*") ] || (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/nova-relayer
tag_with_ref: true
tags: latest,nova,candidate
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 }}) 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 Nova relayer node service to docker hub: `tornadocash/nova-relayer:v${{ steps.vars.outputs.version }}` and `tornadocash/nova-relayer:latest`.
Please update your Nova nodes ❗️
DO NOT TOUCH MAINNET AND SIDECHAINS 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 Nova relayer node service to docker hub: `tornadocash/nova-relayer:v${{ steps.vars.outputs.version }}` and `tornadocash/nova-relayer:latest`.
Please update your Nova nodes ❗️
DO NOT TOUCH MAINNET AND SIDECHAINS 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) because of ${{ github.actor }}
format: markdown
to: ${{ secrets.TELEGRAM_CHAT_ID }}
token: ${{ secrets.TELEGRAM_BOT_TOKEN }}

35
.gitignore vendored

@ -1,4 +1,35 @@
node_modules # compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.env .env
.env*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

1
.npmrc Normal file

@ -0,0 +1 @@
@tornado:registry=https://git.tornado.ws/api/packages/tornado-packages/npm/

5
.prettierrc Normal file

@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 140
}

20
.vscode/launch.json vendored Normal file

@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Nest Debug",
"runtimeExecutable": "yarn",
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
"console": "integratedTerminal",
"restart": true,
"protocol": "auto",
"port": 9229,
"autoAttachChildProcesses": true
}
]
}

9
Dockerfile Normal file

@ -0,0 +1,9 @@
FROM node:14
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn && yarn cache clean --force
COPY . .
EXPOSE 8000
ENTRYPOINT ["yarn"]

123
README.md

@ -1,60 +1,95 @@
# 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 Nova [![Build Status](https://github.com/tornadocash/tornado-pool-relayer/workflows/build/badge.svg)](https://github.com/tornadocash/tornado-pool-relayer/actions) [![Docker Image Version (latest semver)](https://img.shields.io/docker/v/tornadocash/nova-relayer?logo=docker&logoColor=%23FFFFFF&sort=semver)](https://hub.docker.com/repository/docker/tornadocash/nova-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.** ## Deploy with docker-compose (recommended)
**\*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).** _The following instructions are for Ubuntu 22.10, other operating systems may vary. These instructions include automated SSL configuration with LetsEncrypt._
## Deploy with script and docker-compose **PREREQUISITES**
_The following instructions are for Ubuntu 22.10, other operating systems may vary._ 1. Update core dependencies
#### Installation: - `sudo apt-get update`
Just run in terminal: 2. Install docker-compose
- `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`
5. Install node
- `sudo apt install nodejs npm`
**DEPLOYMENT**
1. Clone this repository
`git clone https://git.tornado.ws/tornadocash/nova-relayer && cd nova-relayer`
2. Copy `.env.example` to `.env` and setup environment variables in `.env` file
- set `CHAIN_ID` (100 for xdai, 1 for mainnet)
- set `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
- set `REWARD_ADDRESS` - eth address that is used to collect fees
- set `RPC_URL` rpc url for your node
- set `ORACLE_RPC_URL` - rpc url for mainnet node for fetching prices(always have to be on mainnet)
- set `WITHDRAWAL_SERVICE_FEE` - fee in % that is used for tornado withdrawals
- set `TRANSFER_SERVICE_FEE` - fee is a fixed value in ether for transfer
- set `CONFIRMATIONS` if needed - how many block confirmations to wait before processing an event. Not recommended to set less than 3
- set `MAX_GAS_PRICE` if needed - maximum value of gwei value for relayer's transaction
3. Build and deploy the docker container:
- `npm run build:docker`
- `docker-compose up -d`
## Run locally
1. `yarn`
2. `cp .env.example .env`
3. Modify `.env` as needed
4. `yarn start:dev`
5. Go to `http://127.0.0.1:8000`
6. In order to execute withdraw/transfer request, you can run following command
```bash ```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/transaction
``` ```
#### Configuring environments: Relayer should return a transaction hash
1. Go to `tornado-relayer` folder on the server home directory In that case you will need to add https termination yourself because browsers with default settings will prevent https
2. Check environment files: tornado.cash UI from submitting your request over http connection
By default each network is preconfigured the naming of `.env.<NETWORK>` ## Architecture
- `.env.eth` for Ethereum Mainnet - Abi: Json ABI for working with contracts
- `.env.bsc` for Binance Smart Chain - Artifacts: The generated file contains typed contract instances
- `.env.arb` for Arbitrum - Config:
- `.env.op` for Optimism 1. `bull.config.ts` bull service settings
- `.env.gnosis` for Gnosis (xdai) 2. `configuration.ts` global application configuration
- `.env.polygon` for Polygon (matic) 3. `txManager.config.ts` txManager service settings
- `.env.avax` for Avalanche C-Chain - Constants:
1. `contracts.ts` addresses of contracts and rps
2. `variables.ts` various variables to make things easier
- Modules:
1. `controller.ts` Controller file that will contain all the application routes
2. `module.ts` The module file essentially bundles all the controllers and providers of your application together.
3. `service.ts` The service will include methods that will perform a certain operation.
4. `main.ts` The entry file of the application will take in your module bundle and create an app instance using the NestFactory provided by Nest.
- Services:
1. `gas-price.ts` update gas prices
2. `offchain-price.ts` update the exchange rate
3. `provider.ts` add-on for working with ethers js
- Types: types for the application
- Utilities: helpers functions
3. Configure (fill) environment files for those networks on which the relayer will be deployed: Disclaimer:
- 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
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
#### Deployment:
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):
- `docker-compose --profile eth --profile bsc --profile arb up -d`
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:
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.

@ -1,10 +1,22 @@
version: "2" version: '3'
services: services:
server:
image: tornadocash/relayer:nova
restart: always
command: start:prod
env_file: .env
environment:
REDIS_URL: redis://redis/0
nginx_proxy_read_timeout: 600
depends_on: [redis]
links:
- redis
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
@ -19,7 +31,7 @@ services:
- conf:/etc/nginx/conf.d - conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d - vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html - html:/usr/share/nginx/html
- certs:/etc/nginx/certs - certs:/etc/nginx/certs:ro
logging: logging:
driver: none driver: none
@ -28,9 +40,11 @@ services:
container_name: dockergen container_name: dockergen
restart: always restart: always
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes_from:
- nginx
volumes: volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
letsencrypt: letsencrypt:
@ -39,429 +53,13 @@ services:
restart: always restart: always
environment: environment:
NGINX_DOCKER_GEN_CONTAINER: dockergen NGINX_DOCKER_GEN_CONTAINER: dockergen
volumes_from: NGINX_PROXY_CONTAINER: nginx
- nginx volumes:
- dockergen - conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
# ---------------------- ETH Mainnet ----------------------- # - html:/usr/share/nginx/html
- certs:/etc/nginx/certs:rw
eth-server: - /var/run/docker.sock:/var/run/docker.sock:ro
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]
volumes: volumes:
conf: conf:

@ -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;

4
nest-cli.json Normal file

@ -0,0 +1,4 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

88
package.json Normal file

@ -0,0 +1,88 @@
{
"name": "nova-relayer",
"version": "0.0.6",
"description": "Relayer for Tornado.cash Nova privacy solution. https://nova.tornado.ws",
"author": "tornado.cash",
"license": "MIT",
"scripts": {
"compile": "typechain --target ethers-v5 --out-dir ./src/artifacts './src/abi/*.json'",
"prebuild": "rimraf dist",
"build": "nest build",
"build:docker": "docker build -t tornadocash/relayer:nova .",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "yarn prebuild; yarn build; node dist/src/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@flashbots/ethers-provider-bundle": "^0.3.2",
"@nestjs/bull": "^0.4.0",
"@nestjs/common": "^8.0.0",
"@nestjs/config": "^1.0.0",
"@nestjs/core": "^8.0.0",
"@nestjs/microservices": "^8.0.2",
"@nestjs/platform-express": "^8.0.0",
"@tornado/gas-price-oracle": "^0.5.3",
"@tornado/tx-manager": "^0.4.9",
"ajv": "^8.6.1",
"bull": "^3.22.11",
"class-validator": "^0.13.1",
"ethers": "^5.4.6",
"redis": "^3.1.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@typechain/ethers-v5": "^7.0.1",
"@types/bull": "^3.15.2",
"@types/express": "^4.17.13",
"@types/jest": "^26.0.24",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@types/uuid": "^8.3.1",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"jest": "^27.0.6",
"prettier": "^2.3.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typechain": "^5.1.1",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

175
src/abi/OffchainOracle.json Normal file

@ -0,0 +1,175 @@
[
{
"inputs": [
{ "internalType": "contract MultiWrapper", "name": "_multiWrapper", "type": "address" },
{ "internalType": "contract IOracle[]", "name": "existingOracles", "type": "address[]" },
{ "internalType": "enum OffchainOracle.OracleType[]", "name": "oracleTypes", "type": "uint8[]" },
{ "internalType": "contract IERC20[]", "name": "existingConnectors", "type": "address[]" },
{ "internalType": "contract IERC20", "name": "wBase", "type": "address" }
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "contract IERC20", "name": "connector", "type": "address" }],
"name": "ConnectorAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "contract IERC20", "name": "connector", "type": "address" }],
"name": "ConnectorRemoved",
"type": "event"
},
{
"anonymous": false,
"inputs": [{ "indexed": false, "internalType": "contract MultiWrapper", "name": "multiWrapper", "type": "address" }],
"name": "MultiWrapperUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{
"indexed": false,
"internalType": "enum OffchainOracle.OracleType",
"name": "oracleType",
"type": "uint8"
}
],
"name": "OracleAdded",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": false, "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{
"indexed": false,
"internalType": "enum OffchainOracle.OracleType",
"name": "oracleType",
"type": "uint8"
}
],
"name": "OracleRemoved",
"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"
},
{
"inputs": [{ "internalType": "contract IERC20", "name": "connector", "type": "address" }],
"name": "addConnector",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{ "internalType": "enum OffchainOracle.OracleType", "name": "oracleKind", "type": "uint8" }
],
"name": "addOracle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "connectors",
"outputs": [{ "internalType": "contract IERC20[]", "name": "allConnectors", "type": "address[]" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IERC20", "name": "srcToken", "type": "address" },
{ "internalType": "contract IERC20", "name": "dstToken", "type": "address" },
{ "internalType": "bool", "name": "useWrappers", "type": "bool" }
],
"name": "getRate",
"outputs": [{ "internalType": "uint256", "name": "weightedRate", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IERC20", "name": "srcToken", "type": "address" },
{ "internalType": "bool", "name": "useSrcWrappers", "type": "bool" }
],
"name": "getRateToEth",
"outputs": [{ "internalType": "uint256", "name": "weightedRate", "type": "uint256" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "multiWrapper",
"outputs": [{ "internalType": "contract MultiWrapper", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "oracles",
"outputs": [
{ "internalType": "contract IOracle[]", "name": "allOracles", "type": "address[]" },
{ "internalType": "enum OffchainOracle.OracleType[]", "name": "oracleTypes", "type": "uint8[]" }
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [{ "internalType": "contract IERC20", "name": "connector", "type": "address" }],
"name": "removeConnector",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{ "internalType": "contract IOracle", "name": "oracle", "type": "address" },
{ "internalType": "enum OffchainOracle.OracleType", "name": "oracleKind", "type": "uint8" }
],
"name": "removeOracle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "contract MultiWrapper", "name": "_multiWrapper", "type": "address" }],
"name": "setMultiWrapper",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [{ "internalType": "address", "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

1040
src/abi/TornadoPool.json Normal file

File diff suppressed because it is too large Load Diff

5
src/abi/index.ts Normal file

@ -0,0 +1,5 @@
import TORNADO_POOL from './TornadoPool.json';
export const abi = {
TORNADO_POOL,
};

22
src/app.module.ts Normal file

@ -0,0 +1,22 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { baseConfig } from '@/config';
import { QueueModule, ApiModule } from '@/modules';
import { setHeadersMiddleware } from '@/modules/api/set-headers.middleware';
@Module({
imports: [
ConfigModule.forRoot({
load: [baseConfig],
isGlobal: true,
}),
ApiModule,
QueueModule,
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(setHeadersMiddleware).forRoutes('/');
}
}

523
src/artifacts/OffchainOracle.d.ts vendored Normal file

@ -0,0 +1,523 @@
/* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
import {
ethers,
EventFilter,
Signer,
BigNumber,
BigNumberish,
PopulatedTransaction,
BaseContract,
ContractTransaction,
Overrides,
CallOverrides,
} from "ethers";
import { BytesLike } from "@ethersproject/bytes";
import { Listener, Provider } from "@ethersproject/providers";
import { FunctionFragment, EventFragment, Result } from "@ethersproject/abi";
import { TypedEventFilter, TypedEvent, TypedListener } from "./commons";
interface OffchainOracleInterface extends ethers.utils.Interface {
functions: {
"addConnector(address)": FunctionFragment;
"addOracle(address,uint8)": FunctionFragment;
"connectors()": FunctionFragment;
"getRate(address,address,bool)": FunctionFragment;
"getRateToEth(address,bool)": FunctionFragment;
"multiWrapper()": FunctionFragment;
"oracles()": FunctionFragment;
"owner()": FunctionFragment;
"removeConnector(address)": FunctionFragment;
"removeOracle(address,uint8)": FunctionFragment;
"renounceOwnership()": FunctionFragment;
"setMultiWrapper(address)": FunctionFragment;
"transferOwnership(address)": FunctionFragment;
};
encodeFunctionData(
functionFragment: "addConnector",
values: [string]
): string;
encodeFunctionData(
functionFragment: "addOracle",
values: [string, BigNumberish]
): string;
encodeFunctionData(
functionFragment: "connectors",
values?: undefined
): string;
encodeFunctionData(
functionFragment: "getRate",
values: [string, string, boolean]
): string;
encodeFunctionData(
functionFragment: "getRateToEth",
values: [string, boolean]
): string;
encodeFunctionData(
functionFragment: "multiWrapper",
values?: undefined
): string;
encodeFunctionData(functionFragment: "oracles", values?: undefined): string;
encodeFunctionData(functionFragment: "owner", values?: undefined): string;
encodeFunctionData(
functionFragment: "removeConnector",
values: [string]
): string;
encodeFunctionData(
functionFragment: "removeOracle",
values: [string, BigNumberish]
): string;
encodeFunctionData(
functionFragment: "renounceOwnership",
values?: undefined
): string;
encodeFunctionData(
functionFragment: "setMultiWrapper",
values: [string]
): string;
encodeFunctionData(
functionFragment: "transferOwnership",
values: [string]
): string;
decodeFunctionResult(
functionFragment: "addConnector",
data: BytesLike
): Result;
decodeFunctionResult(functionFragment: "addOracle", data: BytesLike): Result;
decodeFunctionResult(functionFragment: "connectors", data: BytesLike): Result;
decodeFunctionResult(functionFragment: "getRate", data: BytesLike): Result;
decodeFunctionResult(
functionFragment: "getRateToEth",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "multiWrapper",
data: BytesLike
): Result;
decodeFunctionResult(functionFragment: "oracles", data: BytesLike): Result;
decodeFunctionResult(functionFragment: "owner", data: BytesLike): Result;
decodeFunctionResult(
functionFragment: "removeConnector",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "removeOracle",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "renounceOwnership",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "setMultiWrapper",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "transferOwnership",
data: BytesLike
): Result;
events: {
"ConnectorAdded(address)": EventFragment;
"ConnectorRemoved(address)": EventFragment;
"MultiWrapperUpdated(address)": EventFragment;
"OracleAdded(address,uint8)": EventFragment;
"OracleRemoved(address,uint8)": EventFragment;
"OwnershipTransferred(address,address)": EventFragment;
};
getEvent(nameOrSignatureOrTopic: "ConnectorAdded"): EventFragment;
getEvent(nameOrSignatureOrTopic: "ConnectorRemoved"): EventFragment;
getEvent(nameOrSignatureOrTopic: "MultiWrapperUpdated"): EventFragment;
getEvent(nameOrSignatureOrTopic: "OracleAdded"): EventFragment;
getEvent(nameOrSignatureOrTopic: "OracleRemoved"): EventFragment;
getEvent(nameOrSignatureOrTopic: "OwnershipTransferred"): EventFragment;
}
export class OffchainOracle extends BaseContract {
connect(signerOrProvider: Signer | Provider | string): this;
attach(addressOrName: string): this;
deployed(): Promise<this>;
listeners<EventArgsArray extends Array<any>, EventArgsObject>(
eventFilter?: TypedEventFilter<EventArgsArray, EventArgsObject>
): Array<TypedListener<EventArgsArray, EventArgsObject>>;
off<EventArgsArray extends Array<any>, EventArgsObject>(
eventFilter: TypedEventFilter<EventArgsArray, EventArgsObject>,
listener: TypedListener<EventArgsArray, EventArgsObject>
): this;
on<EventArgsArray extends Array<any>, EventArgsObject>(
eventFilter: TypedEventFilter<EventArgsArray, EventArgsObject>,
listener: TypedListener<EventArgsArray, EventArgsObject>
): this;
once<EventArgsArray extends Array<any>, EventArgsObject>(
eventFilter: TypedEventFilter<EventArgsArray, EventArgsObject>,
listener: TypedListener<EventArgsArray, EventArgsObject>
): this;
removeListener<EventArgsArray extends Array<any>, EventArgsObject>(
eventFilter: TypedEventFilter<EventArgsArray, EventArgsObject>,
listener: TypedListener<EventArgsArray, EventArgsObject>
): this;
removeAllListeners<EventArgsArray extends Array<any>, EventArgsObject>(
eventFilter: TypedEventFilter<EventArgsArray, EventArgsObject>
): this;
listeners(eventName?: string): Array<Listener>;
off(eventName: string, listener: Listener): this;
on(eventName: string, listener: Listener): this;
once(eventName: string, listener: Listener): this;
removeListener(eventName: string, listener: Listener): this;
removeAllListeners(eventName?: string): this;
queryFilter<EventArgsArray extends Array<any>, EventArgsObject>(
event: TypedEventFilter<EventArgsArray, EventArgsObject>,
fromBlockOrBlockhash?: string | number | undefined,
toBlock?: string | number | undefined
): Promise<Array<TypedEvent<EventArgsArray & EventArgsObject>>>;
interface: OffchainOracleInterface;
functions: {
addConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
addOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
connectors(
overrides?: CallOverrides
): Promise<[string[]] & { allConnectors: string[] }>;
getRate(
srcToken: string,
dstToken: string,
useWrappers: boolean,
overrides?: CallOverrides
): Promise<[BigNumber] & { weightedRate: BigNumber }>;
getRateToEth(
srcToken: string,
useSrcWrappers: boolean,
overrides?: CallOverrides
): Promise<[BigNumber] & { weightedRate: BigNumber }>;
multiWrapper(overrides?: CallOverrides): Promise<[string]>;
oracles(
overrides?: CallOverrides
): Promise<
[string[], number[]] & { allOracles: string[]; oracleTypes: number[] }
>;
owner(overrides?: CallOverrides): Promise<[string]>;
removeConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
removeOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
renounceOwnership(
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
setMultiWrapper(
_multiWrapper: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
transferOwnership(
newOwner: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
};
addConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
addOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
connectors(overrides?: CallOverrides): Promise<string[]>;
getRate(
srcToken: string,
dstToken: string,
useWrappers: boolean,
overrides?: CallOverrides
): Promise<BigNumber>;
getRateToEth(
srcToken: string,
useSrcWrappers: boolean,
overrides?: CallOverrides
): Promise<BigNumber>;
multiWrapper(overrides?: CallOverrides): Promise<string>;
oracles(
overrides?: CallOverrides
): Promise<
[string[], number[]] & { allOracles: string[]; oracleTypes: number[] }
>;
owner(overrides?: CallOverrides): Promise<string>;
removeConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
removeOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
renounceOwnership(
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
setMultiWrapper(
_multiWrapper: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
transferOwnership(
newOwner: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
callStatic: {
addConnector(connector: string, overrides?: CallOverrides): Promise<void>;
addOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: CallOverrides
): Promise<void>;
connectors(overrides?: CallOverrides): Promise<string[]>;
getRate(
srcToken: string,
dstToken: string,
useWrappers: boolean,
overrides?: CallOverrides
): Promise<BigNumber>;
getRateToEth(
srcToken: string,
useSrcWrappers: boolean,
overrides?: CallOverrides
): Promise<BigNumber>;
multiWrapper(overrides?: CallOverrides): Promise<string>;
oracles(
overrides?: CallOverrides
): Promise<
[string[], number[]] & { allOracles: string[]; oracleTypes: number[] }
>;
owner(overrides?: CallOverrides): Promise<string>;
removeConnector(
connector: string,
overrides?: CallOverrides
): Promise<void>;
removeOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: CallOverrides
): Promise<void>;
renounceOwnership(overrides?: CallOverrides): Promise<void>;
setMultiWrapper(
_multiWrapper: string,
overrides?: CallOverrides
): Promise<void>;
transferOwnership(
newOwner: string,
overrides?: CallOverrides
): Promise<void>;
};
filters: {
ConnectorAdded(
connector?: null
): TypedEventFilter<[string], { connector: string }>;
ConnectorRemoved(
connector?: null
): TypedEventFilter<[string], { connector: string }>;
MultiWrapperUpdated(
multiWrapper?: null
): TypedEventFilter<[string], { multiWrapper: string }>;
OracleAdded(
oracle?: null,
oracleType?: null
): TypedEventFilter<
[string, number],
{ oracle: string; oracleType: number }
>;
OracleRemoved(
oracle?: null,
oracleType?: null
): TypedEventFilter<
[string, number],
{ oracle: string; oracleType: number }
>;
OwnershipTransferred(
previousOwner?: string | null,
newOwner?: string | null
): TypedEventFilter<
[string, string],
{ previousOwner: string; newOwner: string }
>;
};
estimateGas: {
addConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
addOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
connectors(overrides?: CallOverrides): Promise<BigNumber>;
getRate(
srcToken: string,
dstToken: string,
useWrappers: boolean,
overrides?: CallOverrides
): Promise<BigNumber>;
getRateToEth(
srcToken: string,
useSrcWrappers: boolean,
overrides?: CallOverrides
): Promise<BigNumber>;
multiWrapper(overrides?: CallOverrides): Promise<BigNumber>;
oracles(overrides?: CallOverrides): Promise<BigNumber>;
owner(overrides?: CallOverrides): Promise<BigNumber>;
removeConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
removeOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
renounceOwnership(
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
setMultiWrapper(
_multiWrapper: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
transferOwnership(
newOwner: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
};
populateTransaction: {
addConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
addOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
connectors(overrides?: CallOverrides): Promise<PopulatedTransaction>;
getRate(
srcToken: string,
dstToken: string,
useWrappers: boolean,
overrides?: CallOverrides
): Promise<PopulatedTransaction>;
getRateToEth(
srcToken: string,
useSrcWrappers: boolean,
overrides?: CallOverrides
): Promise<PopulatedTransaction>;
multiWrapper(overrides?: CallOverrides): Promise<PopulatedTransaction>;
oracles(overrides?: CallOverrides): Promise<PopulatedTransaction>;
owner(overrides?: CallOverrides): Promise<PopulatedTransaction>;
removeConnector(
connector: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
removeOracle(
oracle: string,
oracleKind: BigNumberish,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
renounceOwnership(
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
setMultiWrapper(
_multiWrapper: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
transferOwnership(
newOwner: string,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
};
}

1430
src/artifacts/TornadoPool.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

36
src/artifacts/commons.ts Normal file

@ -0,0 +1,36 @@
/* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
import { EventFilter, Event } from "ethers";
import { Result } from "@ethersproject/abi";
export interface TypedEventFilter<_EventArgsArray, _EventArgsObject>
extends EventFilter {}
export interface TypedEvent<EventArgs extends Result> extends Event {
args: EventArgs;
}
export type TypedListener<
EventArgsArray extends Array<any>,
EventArgsObject
> = (
...listenerArg: [
...EventArgsArray,
TypedEvent<EventArgsArray & EventArgsObject>
]
) => void;
export type MinEthersFactory<C, ARGS> = {
deploy(...a: ARGS[]): Promise<C>;
};
export type GetContractTypeFromFactory<F> = F extends MinEthersFactory<
infer C,
any
>
? C
: never;
export type GetARGsTypeFromFactory<F> = F extends MinEthersFactory<any, any>
? Parameters<F["deploy"]>
: never;

@ -0,0 +1,358 @@
/* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
import { Contract, Signer, utils } from "ethers";
import { Provider } from "@ethersproject/providers";
import type {
OffchainOracle,
OffchainOracleInterface,
} from "../OffchainOracle";
const _abi = [
{
inputs: [
{
internalType: "contract MultiWrapper",
name: "_multiWrapper",
type: "address",
},
{
internalType: "contract IOracle[]",
name: "existingOracles",
type: "address[]",
},
{
internalType: "enum OffchainOracle.OracleType[]",
name: "oracleTypes",
type: "uint8[]",
},
{
internalType: "contract IERC20[]",
name: "existingConnectors",
type: "address[]",
},
{
internalType: "contract IERC20",
name: "wBase",
type: "address",
},
],
stateMutability: "nonpayable",
type: "constructor",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "contract IERC20",
name: "connector",
type: "address",
},
],
name: "ConnectorAdded",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "contract IERC20",
name: "connector",
type: "address",
},
],
name: "ConnectorRemoved",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "contract MultiWrapper",
name: "multiWrapper",
type: "address",
},
],
name: "MultiWrapperUpdated",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "contract IOracle",
name: "oracle",
type: "address",
},
{
indexed: false,
internalType: "enum OffchainOracle.OracleType",
name: "oracleType",
type: "uint8",
},
],
name: "OracleAdded",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "contract IOracle",
name: "oracle",
type: "address",
},
{
indexed: false,
internalType: "enum OffchainOracle.OracleType",
name: "oracleType",
type: "uint8",
},
],
name: "OracleRemoved",
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",
},
{
inputs: [
{
internalType: "contract IERC20",
name: "connector",
type: "address",
},
],
name: "addConnector",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "contract IOracle",
name: "oracle",
type: "address",
},
{
internalType: "enum OffchainOracle.OracleType",
name: "oracleKind",
type: "uint8",
},
],
name: "addOracle",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "connectors",
outputs: [
{
internalType: "contract IERC20[]",
name: "allConnectors",
type: "address[]",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "contract IERC20",
name: "srcToken",
type: "address",
},
{
internalType: "contract IERC20",
name: "dstToken",
type: "address",
},
{
internalType: "bool",
name: "useWrappers",
type: "bool",
},
],
name: "getRate",
outputs: [
{
internalType: "uint256",
name: "weightedRate",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "contract IERC20",
name: "srcToken",
type: "address",
},
{
internalType: "bool",
name: "useSrcWrappers",
type: "bool",
},
],
name: "getRateToEth",
outputs: [
{
internalType: "uint256",
name: "weightedRate",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "multiWrapper",
outputs: [
{
internalType: "contract MultiWrapper",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "oracles",
outputs: [
{
internalType: "contract IOracle[]",
name: "allOracles",
type: "address[]",
},
{
internalType: "enum OffchainOracle.OracleType[]",
name: "oracleTypes",
type: "uint8[]",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "owner",
outputs: [
{
internalType: "address",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "contract IERC20",
name: "connector",
type: "address",
},
],
name: "removeConnector",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "contract IOracle",
name: "oracle",
type: "address",
},
{
internalType: "enum OffchainOracle.OracleType",
name: "oracleKind",
type: "uint8",
},
],
name: "removeOracle",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "renounceOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "contract MultiWrapper",
name: "_multiWrapper",
type: "address",
},
],
name: "setMultiWrapper",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "address",
name: "newOwner",
type: "address",
},
],
name: "transferOwnership",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
];
export class OffchainOracle__factory {
static readonly abi = _abi;
static createInterface(): OffchainOracleInterface {
return new utils.Interface(_abi) as OffchainOracleInterface;
}
static connect(
address: string,
signerOrProvider: Signer | Provider
): OffchainOracle {
return new Contract(address, _abi, signerOrProvider) as OffchainOracle;
}
}

File diff suppressed because it is too large Load Diff

8
src/artifacts/index.ts Normal file

@ -0,0 +1,8 @@
/* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
export type { OffchainOracle } from "./OffchainOracle";
export type { TornadoPool } from "./TornadoPool";
export { OffchainOracle__factory } from "./factories/OffchainOracle__factory";
export { TornadoPool__factory } from "./factories/TornadoPool__factory";

14
src/config/bull.config.ts Normal file

@ -0,0 +1,14 @@
import { registerAs } from '@nestjs/config';
export default registerAs('bull', () => ({
redis: process.env.REDIS_URL || 'localhost',
settings: {
lockDuration: 300000,
lockRenewTime: 30000,
stalledInterval: 30000,
maxStalledCount: 3,
guardInterval: 5000,
retryProcessDelay: 5000,
drainDelay: 5,
},
}));

@ -0,0 +1,26 @@
import { Wallet } from 'ethers';
import { ChainId } from '@/types';
import { toWei } from '@/utilities';
import { NETWORKS_INFO, RPC_LIST } from '@/constants';
import { version } from '../../package.json';
export const baseConfig = () => ({
base: {
version,
port: process.env.PORT,
chainId: Number(process.env.CHAIN_ID),
serviceFee: {
transfer: toWei(process.env.TRANSFER_SERVICE_FEE).toString(),
withdrawal: Number(process.env.WITHDRAWAL_SERVICE_FEE),
},
rpcUrl: process.env.RPC_URL || RPC_LIST[process.env.CHAIN_ID],
oracleRpcUrl: process.env.ORACLE_RPC_URL || RPC_LIST[ChainId.MAINNET],
rewardAddress: process.env.REWARD_ADDRESS,
address: new Wallet(process.env.PRIVATE_KEY).address,
gasLimit: NETWORKS_INFO[process.env.CHAIN_ID].gasLimit,
minimumBalance: NETWORKS_INFO[process.env.CHAIN_ID].minimumBalance,
},
});

4
src/config/index.ts Normal file

@ -0,0 +1,4 @@
export * from './configuration';
export * from './bull.config';
export * from './txManager.config';

@ -0,0 +1,15 @@
import { registerAs } from '@nestjs/config';
import { RPC_LIST } from '@/constants';
export default registerAs('txManager', () => ({
privateKey: process.env.PRIVATE_KEY,
rpcUrl: process.env.RPC_URL || RPC_LIST[process.env.CHAIN_ID],
config: {
THROW_ON_REVERT: false,
CONFIRMATIONS: Number(process.env.CONFIRMATIONS),
MAX_GAS_PRICE: Number(process.env.MAX_GAS_PRICE),
},
gasPriceOracleConfig: {
chainId: Number(process.env.CHAIN_ID),
},
}));

@ -0,0 +1,12 @@
import { ChainId } from '@/types';
export const CONTRACT_NETWORKS: { [chainId in ChainId]: string } = {
[ChainId.XDAI]: '0xD692Fd2D0b2Fbd2e52CFa5B5b9424bC981C30696', // ETH
};
export const RPC_LIST: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]: 'https://api.mycryptoapi.com/eth',
[ChainId.XDAI]: 'https://rpc.gnosischain.com/tornado',
};
export const OFF_CHAIN_ORACLE = '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb';

2
src/constants/index.ts Normal file

@ -0,0 +1,2 @@
export * from './variables';
export * from './contracts';

@ -0,0 +1,55 @@
import { BigNumber } from 'ethers';
import { ChainId } from '@/types';
const NETWORKS_INFO: { [chainId in ChainId] } = {
[ChainId.XDAI]: {
symbol: 'xDAI',
gasLimit: BigNumber.from(2000000),
minimumBalance: '0.5',
},
};
const numbers = {
ZERO: 0,
ONE: 1,
TWO: 2,
TEN: 10,
ONE_HUNDRED: 100,
SECOND: 1000,
ETH_DECIMALS: 18,
MERKLE_TREE_HEIGHT: 23,
};
export const jobStatus = {
QUEUED: 'QUEUED',
ACCEPTED: 'ACCEPTED',
CONFIRMED: 'CONFIRMED',
FAILED: 'FAILED',
MINED: 'MINED',
SENT: 'SENT',
};
const BG_ZERO = BigNumber.from(numbers.ZERO);
const FIELD_SIZE = BigNumber.from('21888242871839275222246405745257275088548364400416034343698204186575808495617');
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f';
export { numbers, NETWORKS_INFO, DAI_ADDRESS, FIELD_SIZE, BG_ZERO, ZERO_ADDRESS };
export const CONTRACT_ERRORS = [
'Invalid merkle root',
'Input is already spent',
'Incorrect external data hash',
'Invalid fee',
'Invalid ext amount',
'Invalid public amount',
'Invalid transaction proof',
"Can't withdraw to zero address",
];
export const SERVICE_ERRORS = {
GAS_PRICE: 'Could not get gas price',
TOKEN_RATES: 'Could not get token rates',
GAS_SPIKE: 'Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.',
};

18
src/main.ts Normal file

@ -0,0 +1,18 @@
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
try {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { cors: true });
const configService = app.get(ConfigService);
await app.listen(configService.get('base.port'));
} catch (err) {
console.log('err', err.message);
}
}
bootstrap();

@ -0,0 +1,45 @@
import { Body, Controller, Get, HttpStatus, Param, Post, Res } from '@nestjs/common';
import { Response } from 'express';
import { ApiService } from './api.service';
import { validateTransactionRequest } from './api.validator';
@Controller()
export class ApiController {
constructor(private readonly service: ApiService) {}
@Get('/status')
async status(@Res() res: Response): Promise<Response<Status>> {
return res.json(await this.service.status());
}
@Get('/')
root(@Res() res: Response): Response<string> {
return res.send(this.service.root());
}
@Get('/job/:jobId')
async getJob(@Res() res: Response, @Param('jobId') jobId: string) {
const job = await this.service.getJob(jobId);
if (!job) {
return res.status(HttpStatus.BAD_REQUEST).json({ error: "The job doesn't exist" });
}
return res.json(job);
}
@Post('/transaction')
async transaction(@Res() res: Response, @Body() { body }: any) {
const params = JSON.parse(body);
const inputError = validateTransactionRequest(params);
if (inputError) {
console.log('Invalid input:', inputError);
return res.status(HttpStatus.BAD_REQUEST).json({ error: inputError });
}
const jobId = await this.service.transaction(params);
return res.send(jobId);
}
}

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ApiService } from './api.service';
import { ApiController } from './api.controller';
import { QueueModule } from '@/modules';
import { ProviderService } from '@/services';
@Module({
imports: [ConfigModule, QueueModule],
providers: [ApiService, ProviderService],
controllers: [ApiController],
exports: [],
})
export class ApiModule {}

@ -0,0 +1,71 @@
import { Queue } from 'bull';
import { v4 as uuid } from 'uuid';
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { ProviderService } from '@/services';
import { ConfigService } from '@nestjs/config';
import { jobStatus, NETWORKS_INFO } from '@/constants';
import { Transaction } from '@/types';
@Injectable()
class ApiService {
constructor(
private configService: ConfigService,
private providerService: ProviderService,
@InjectQueue('transaction') private transactionQueue: Queue,
) {}
async status(): Promise<Status> {
const { rewardAddress, version, chainId, serviceFee } = this.configService.get('base');
const health = await this.healthCheck();
return {
health,
version,
chainId,
serviceFee,
rewardAddress,
};
}
root(): string {
return `This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> for settings`;
}
async transaction(data: any): Promise<string> {
const jobId = uuid();
await this.transactionQueue.add({ ...data, status: jobStatus.QUEUED }, { jobId });
return jobId;
}
async getJob(id: string): Promise<Transaction | null> {
const job = await this.transactionQueue.getJob(id);
if (!job) {
return null;
}
return {
...job.data,
failedReason: job.failedReason,
};
}
private async healthCheck(): Promise<Health> {
const status = await this.providerService.checkSenderBalance();
const { chainId, minimumBalance } = this.configService.get('base');
return {
status,
error: status ? '' : `Not enough balance, less than ${minimumBalance} ${NETWORKS_INFO[chainId].symbol}`,
};
}
}
export { ApiService };

@ -0,0 +1,74 @@
import Ajv, { ValidateFunction } from 'ajv';
import { isAddress } from '@/utilities';
const ajv = new Ajv();
ajv.addKeyword({
keyword: 'isAddress',
validate: (schema: any, address: string) => {
return isAddress(address);
},
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 bytes32Type = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' };
const externalAmountType = { type: 'string', pattern: '^(0x[a-fA-F0-9]{64}|-0x[a-fA-F0-9]{63})$' };
const encryptedOutputType = { type: 'string', pattern: '^0x[a-fA-F0-9]{312}$' };
const arrayType = { type: 'array', items: bytes32Type };
const booleanType = { type: 'boolean' };
const transactionSchema = {
type: 'object',
properties: {
extData: {
type: 'object',
properties: {
encryptedOutput1: encryptedOutputType,
encryptedOutput2: encryptedOutputType,
extAmount: externalAmountType,
fee: bytes32Type,
recipient: addressType,
relayer: addressType,
isL1Withdrawal: booleanType,
l1Fee: bytes32Type,
},
},
args: {
type: 'object',
properties: {
extDataHash: bytes32Type,
inputNullifiers: arrayType,
outputCommitments: arrayType,
proof: proofType,
publicAmount: bytes32Type,
root: bytes32Type,
},
},
},
additionalProperties: false,
required: ['extData', 'args'],
};
const validateTornadoTransaction = ajv.compile(transactionSchema);
function getInputError(validator: ValidateFunction, data: typeof transactionSchema) {
validator(data);
if (validator.errors) {
const [error] = validator.errors;
return error.message;
}
return null;
}
function validateTransactionRequest(data: typeof transactionSchema) {
return getInputError(validateTornadoTransaction, data);
}
export { validateTransactionRequest };

@ -0,0 +1,4 @@
export class CreateApiDto {
error: boolean;
status: string;
}

@ -0,0 +1 @@
export * from './create-subscribe.dto';

1
src/modules/api/index.ts Normal file

@ -0,0 +1 @@
export { ApiModule } from './api.module';

@ -0,0 +1,11 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class setHeadersMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
}
}

@ -0,0 +1,17 @@
type Health = {
status: boolean;
error: string;
};
type ServiceFee = {
transfer: string;
withdrawal: number;
};
type Status = {
health: Health;
chainId: number;
version: string;
rewardAddress: string;
serviceFee: ServiceFee;
};

2
src/modules/index.ts Normal file

@ -0,0 +1,2 @@
export * from './queue';
export * from './api';

@ -0,0 +1,66 @@
import {
Processor,
OnQueueActive,
OnQueueFailed,
OnQueueRemoved,
OnQueueResumed,
OnQueueStalled,
OnQueueProgress,
OnQueueCompleted,
} from '@nestjs/bull';
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { Job, Queue } from 'bull';
@Injectable()
@Processor()
// eslint-disable-next-line @typescript-eslint/ban-types
export class BaseProcessor<T = object> implements OnModuleDestroy {
public queueName: string;
public queue: Queue<T>;
@OnQueueActive()
async onQueueActive(job: Job<T>) {
return this.updateTask(job);
}
@OnQueueFailed()
async onQueueFailed(job: Job<T>) {
return this.updateTask(job);
}
@OnQueueCompleted()
async onQueueCompleted(job: Job<T>) {
return this.updateTask(job);
}
@OnQueueProgress()
async onQueueProgress(job: Job<T>) {
return this.updateTask(job);
}
@OnQueueRemoved()
async onQueueRemoved(job: Job<T>) {
return this.updateTask(job);
}
@OnQueueResumed()
async onQueueResumed(job: Job<T>) {
return this.updateTask(job);
}
@OnQueueStalled()
async onQueueStalled(job: Job<T>) {
return this.updateTask(job);
}
protected async updateTask(job: Job<T>) {
const currentJob = await this.queue.getJob(job.id);
await currentJob.update(job.data);
}
async onModuleDestroy() {
if (this.queue) {
await this.queue.close();
}
}
}

@ -0,0 +1 @@
export * from './queue.module';

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { GasPriceService, ProviderService, OffchainPriceService } from '@/services';
import { TransactionProcessor } from './transaction.processor';
import bullConfig from '@/config/bull.config';
@Module({
imports: [
BullModule.registerQueueAsync({
name: 'transaction',
useFactory: bullConfig,
}),
],
providers: [GasPriceService, ProviderService, TransactionProcessor, OffchainPriceService],
exports: [BullModule],
})
export class QueueModule {}

@ -0,0 +1,173 @@
import { BigNumber } from 'ethers';
import { TxManager } from '@tornado/tx-manager';
import { Job, Queue, DoneCallback } from 'bull';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectQueue, Process, Processor, OnQueueActive, OnQueueCompleted, OnQueueFailed } from '@nestjs/bull';
import { Transaction } from '@/types';
import { getToIntegerMultiplier, toWei } from '@/utilities';
import { CONTRACT_ERRORS, SERVICE_ERRORS, jobStatus } from '@/constants';
import { GasPriceService, ProviderService, OffchainPriceService } from '@/services';
import txMangerConfig from '@/config/txManager.config';
import { BaseProcessor } from './base.processor';
@Injectable()
@Processor('transaction')
export class TransactionProcessor extends BaseProcessor<Transaction> {
constructor(
@InjectQueue('transaction') public transactionQueue: Queue,
private configService: ConfigService,
private gasPriceService: GasPriceService,
private providerService: ProviderService,
private offChainPriceService: OffchainPriceService,
) {
super();
this.queueName = 'transaction';
this.queue = transactionQueue;
}
@Process()
async processTransactions(job: Job<Transaction>, cb: DoneCallback) {
try {
const { extData } = job.data;
await this.checkFee({ fee: extData.fee, externalAmount: extData.extAmount });
const txHash = await this.submitTx(job);
cb(null, txHash);
} catch (err) {
cb(err);
}
}
@OnQueueActive()
async onActive(job: Job) {
job.data.status = jobStatus.ACCEPTED;
await this.updateTask(job);
}
@OnQueueCompleted()
async onCompleted(job: Job) {
job.data.status = jobStatus.CONFIRMED;
await this.updateTask(job);
}
@OnQueueFailed()
async onFailed(job: Job) {
job.data.status = jobStatus.FAILED;
await this.updateTask(job);
}
async submitTx(job: Job<Transaction>) {
try {
const txManager = new TxManager(txMangerConfig());
const prepareTx = await this.prepareTransaction(job.data);
const tx = await txManager.createTx(prepareTx);
const receipt = await tx
.send()
.on('transactionHash', async (txHash: string) => {
job.data.txHash = txHash;
job.data.status = jobStatus.SENT;
await this.updateTask(job);
})
.on('mined', async () => {
job.data.status = jobStatus.MINED;
await this.updateTask(job);
})
.on('confirmations', async (confirmations) => {
job.data.confirmations = confirmations;
await this.updateTask(job);
});
if (BigNumber.from(receipt.status).eq(1)) {
return receipt.transactionHash;
} else {
throw new Error('Submitted transaction failed');
}
} catch (err) {
return this.handleError(err);
}
}
async prepareTransaction({ extData, args }) {
const contract = this.providerService.getTornadoPool();
const data = contract.interface.encodeFunctionData('transact', [args, extData]);
const gasLimit = this.configService.get<BigNumber>('base.gasLimit');
const { fast } = await this.gasPriceService.getGasPrice();
return {
data,
gasLimit,
to: contract.address,
gasPrice: fast,
value: BigNumber.from(0)._hex,
};
}
getServiceFee(externalAmount) {
const amount = BigNumber.from(externalAmount);
const { serviceFee } = this.configService.get('base');
// for withdrawals the amount is negative
if (amount.isNegative()) {
const oneEther = getToIntegerMultiplier();
const share = Number(serviceFee.withdrawal) / 100;
return amount.mul(toWei(share.toString())).div(oneEther);
}
return serviceFee.transfer;
}
async checkFee({ fee, externalAmount }) {
try {
const { gasLimit } = this.configService.get('base');
const { fast } = await this.gasPriceService.getGasPrice();
const operationFee = BigNumber.from(fast).mul(gasLimit);
const feePercent = this.getServiceFee(externalAmount);
const ethPrice = await this.offChainPriceService.getDaiEthPrice();
const expense = operationFee.mul(ethPrice).div(toWei('1'));
const desiredFee = expense.add(feePercent);
if (BigNumber.from(fee).lt(desiredFee)) {
throw new Error(SERVICE_ERRORS.GAS_SPIKE);
}
} catch (err) {
this.handleError(err);
}
}
handleError({ message }: Error) {
const contractError = CONTRACT_ERRORS.find((knownError) => message.includes(knownError));
if (contractError) {
throw new Error(`Revert by smart contract: ${contractError}`);
}
const serviceError = Object.values(SERVICE_ERRORS).find((knownError) => message.includes(knownError));
if (serviceError) {
throw new Error(`Relayer internal error: ${serviceError}`);
}
console.log('handleError:', message);
throw new Error('Relayer did not send your transaction. Please choose a different relayer.');
}
}

@ -0,0 +1,51 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BigNumber } from 'ethers';
import { GasPriceOracle } from '@tornado/gas-price-oracle';
import type { GasPrice } from '@tornado/gas-price-oracle/lib/services';
import { toWei } from '@/utilities';
import { SERVICE_ERRORS } from '@/constants';
const bump = (gas: BigNumber, percent: number) => gas.mul(percent).div(100).toHexString();
const gweiToWei = (value: number) => toWei(String(value), 'gwei');
const percentBump = {
INSTANT: 150,
FAST: 130,
STANDARD: 85,
LOW: 50,
};
@Injectable()
export class GasPriceService {
private readonly chainId: number;
private readonly rpcUrl: string;
constructor(private configService: ConfigService) {
this.chainId = this.configService.get<number>('base.chainId');
this.rpcUrl = this.configService.get('base.rpcUrl');
}
async getGasPrice() {
try {
const instance = new GasPriceOracle({
chainId: this.chainId,
defaultRpc: this.rpcUrl,
});
const result = (await instance.gasPrices({ isLegacy: true })) as GasPrice;
return {
instant: bump(gweiToWei(result.instant), percentBump.INSTANT),
fast: bump(gweiToWei(result.instant), percentBump.FAST),
standard: bump(gweiToWei(result.standard), percentBump.STANDARD),
low: bump(gweiToWei(result.low), percentBump.LOW),
};
} catch (err) {
console.log('getGasPrice has error:', err.message);
throw new Error(SERVICE_ERRORS.GAS_PRICE);
}
}
}

4
src/services/index.ts Normal file

@ -0,0 +1,4 @@
export * from './provider.service';
export * from './gas-price.service';
export * from './offchain-price.service';

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BigNumber } from 'ethers';
import { ChainId } from '@/types';
import { toWei } from '@/utilities';
import { ProviderService } from '@/services';
import { DAI_ADDRESS, SERVICE_ERRORS } from '@/constants';
@Injectable()
export class OffchainPriceService {
private readonly chainId: number;
private readonly rpcUrl: string;
constructor(private configService: ConfigService, private providerService: ProviderService) {
this.chainId = ChainId.MAINNET;
this.rpcUrl = this.configService.get('base.oracleRpcUrl');
}
async getDaiEthPrice() {
try {
const contract = this.providerService.getOffChainOracle();
const rate = await contract.callStatic.getRateToEth(DAI_ADDRESS, false);
const numerator = BigNumber.from(toWei('1'));
const denominator = BigNumber.from(toWei('1'));
// price = rate * "token decimals" / "eth decimals" (dai = eth decimals)
return BigNumber.from(rate).mul(numerator).div(denominator);
} catch (err) {
console.log('getDaiEthPrice has error:', err.message);
throw new Error(SERVICE_ERRORS.TOKEN_RATES);
}
}
}

@ -0,0 +1,56 @@
import { ethers } from 'ethers';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ChainId } from '@/types';
import { CONTRACT_NETWORKS, OFF_CHAIN_ORACLE } from '@/constants';
import { TornadoPool__factory as TornadoPool, OffchainOracle__factory as OffchainOracle } from '@/artifacts';
@Injectable()
export class ProviderService {
private readonly chainId: number;
private readonly rpcUrl: string;
private readonly providers: Map<ChainId, ethers.providers.StaticJsonRpcProvider> = new Map();
constructor(private configService: ConfigService) {
this.chainId = this.configService.get<number>('base.chainId');
this.rpcUrl = this.configService.get('base.rpcUrl');
}
get provider() {
return this.getProvider(this.chainId, this.rpcUrl);
}
getProvider(chainId: ChainId, rpcUrl: string) {
if (!this.providers.has(chainId)) {
this.providers.set(chainId, new ethers.providers.StaticJsonRpcProvider(rpcUrl, chainId));
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.providers.get(chainId)!;
}
getTornadoPool() {
return TornadoPool.connect(CONTRACT_NETWORKS[this.chainId], this.provider);
}
getOffChainOracle() {
const oracleRpcUrl = this.configService.get('base.oracleRpcUrl');
const provider = this.getProvider(ChainId.MAINNET, oracleRpcUrl);
return OffchainOracle.connect(OFF_CHAIN_ORACLE, provider);
}
async checkSenderBalance() {
try {
const balance = await this.getBalance(this.configService.get<string>('base.address'));
return balance.gt(ethers.utils.parseEther(this.configService.get('base.minimumBalance')));
} catch {
return false;
}
}
async getBalance(address: string) {
return await this.provider.getBalance(address);
}
}

37
src/types/index.ts Normal file

@ -0,0 +1,37 @@
import { BigNumberish } from 'ethers';
import { BytesLike } from '@ethersproject/bytes';
const MAINNET_CHAIN_ID = 1;
const XDAI_CHAIN_ID = 100;
export enum ChainId {
MAINNET = MAINNET_CHAIN_ID,
XDAI = XDAI_CHAIN_ID,
}
export type ExtData = {
recipient: string;
relayer: string;
fee: BigNumberish;
extAmount: BigNumberish;
encryptedOutput1: BytesLike;
encryptedOutput2: BytesLike;
};
export type ArgsProof = {
proof: BytesLike;
root: BytesLike;
inputNullifiers: string[];
outputCommitments: BytesLike[];
publicAmount: string;
extDataHash: string;
};
export interface Transaction {
extData: ExtData;
args: ArgsProof;
status: string;
txHash?: string;
confirmations?: number;
failedReason?: string;
}

31
src/utilities/crypto.ts Normal file

@ -0,0 +1,31 @@
import { BigNumber, utils, BigNumberish } from 'ethers';
import { numbers } from '@/constants';
export function isAddress(value: string): boolean {
return utils.isAddress(value);
}
export function toChecksumAddress(value: string): string {
return utils.getAddress(value);
}
export function toWei(value: string, uintName = 'ether') {
return utils.parseUnits(String(value), uintName);
}
export function hexToNumber(hex: string) {
return BigNumber.from(hex).toNumber();
}
export function numberToHex(value: number) {
return utils.hexlify(value);
}
export function fromWei(balance: BigNumberish) {
return utils.formatUnits(balance, numbers.ETH_DECIMALS);
}
export function getToIntegerMultiplier(): BigNumber {
return toWei('1', 'ether');
}

1
src/utilities/index.ts Normal file

@ -0,0 +1 @@
export * from './crypto';

24
test/app.e2e-spec.ts Normal file

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> for settings');
});
});

12
test/jest-e2e.json Normal file

@ -0,0 +1,12 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": "../",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
}
}

@ -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;
}

@ -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;
}
}

4
tsconfig.build.json Normal file

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

22
tsconfig.json Normal file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"skipLibCheck": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"resolveJsonModule": true,
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"paths": {
"@/*": [
"./src/*"
]
}
}
}

6225
yarn.lock Normal file

File diff suppressed because it is too large Load Diff