initialise

This commit is contained in:
gozzy 2022-08-21 13:10:04 +00:00
commit a8ebc332c3
49 changed files with 12036 additions and 0 deletions

9
Dockerfile Normal file
View 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"]

72
README.md Normal file
View File

@ -0,0 +1,72 @@
# 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)
## Deploy with docker-compose
docker-compose.yml contains a stack that will automatically provision SSL certificates for your domain name and will add a https redirect to port 80.
1. Download [docker-compose.yml](/docker-compose.yml) and [.env.example](/.env.example)
```
wget https://raw.githubusercontent.com/tornadocash/tornado-pool-relayer/master/docker-compose.yml
wget https://raw.githubusercontent.com/tornadocash/tornado-pool-relayer/master/.env.example -O .env
```
2. Setup environment variables
- 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. Run `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
curl -X POST -H 'content-type:application/json' --data '<input data>' http://127.0.0.1:8000/transaction
```
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
## Architecture
- Abi: Json ABI for working with contracts
- Artifacts: The generated file contains typed contract instances
- Config:
1. `bull.config.ts` bull service settings
2. `configuration.ts` global application configuration
3. `txManager.config.ts` txManager service settings
- 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
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.

69
docker-compose.yml Normal file
View File

@ -0,0 +1,69 @@
version: '3'
services:
server:
image: tornadocash/nova-relayer
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:
image: redis
restart: always
command: [redis-server, --appendonly, 'yes']
volumes:
- redis:/data
nginx:
image: nginx:alpine
container_name: nginx
restart: always
ports:
- 80:80
- 443:443
volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:ro
logging:
driver: none
dockergen:
image: poma/docker-gen
container_name: dockergen
restart: always
command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
volumes:
- 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
letsencrypt:
image: jrcs/letsencrypt-nginx-proxy-companion
container_name: letsencrypt
restart: always
environment:
NGINX_DOCKER_GEN_CONTAINER: dockergen
NGINX_PROXY_CONTAINER: nginx
volumes:
- conf:/etc/nginx/conf.d
- vhost:/etc/nginx/vhost.d
- html:/usr/share/nginx/html
- certs:/etc/nginx/certs:rw
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
conf:
vhost:
html:
certs:
redis:

4
nest-cli.json Normal file
View File

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

87
package.json Normal file
View File

@ -0,0 +1,87 @@
{
"name": "pool-relayer",
"version": "0.0.5",
"description": "Relayer for Tornado.cash Nova privacy solution. https://tornado.cash",
"author": "tornado.cash",
"license": "MIT",
"scripts": {
"compile": "typechain --target ethers-v5 --out-dir ./src/artifacts './src/abi/*.json'",
"prebuild": "rimraf dist",
"build": "nest build",
"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",
"ajv": "^8.6.1",
"bull": "^3.22.11",
"class-validator": "^0.13.1",
"ethers": "^5.4.6",
"gas-price-oracle": "^0.4.7",
"redis": "^3.1.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"tx-manager": "^0.4.8",
"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
View 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
View File

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

22
src/app.module.ts Normal file
View 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
View 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
View 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;

View File

@ -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
View 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
View 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,
},
}));

View File

@ -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
View File

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

View File

@ -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: process.env.CONFIRMATIONS,
MAX_GAS_PRICE: process.env.MAX_GAS_PRICE,
},
gasPriceOracleConfig: {
chainId: Number(process.env.CHAIN_ID),
},
}));

View File

@ -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.xdaichain.com/tornado',
};
export const OFF_CHAIN_ORACLE = '0x07D91f5fb9Bf7798734C3f606dB065549F6893bb';

2
src/constants/index.ts Normal file
View File

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

View File

@ -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
View 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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

@ -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
View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,173 @@
import { BigNumber } from 'ethers';
import { TxManager } from '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.');
}
}

View File

@ -0,0 +1,50 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { BigNumber } from 'ethers';
import { GasPriceOracle } from 'gas-price-oracle';
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();
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
View File

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

View File

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

View File

@ -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
View 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
View 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
View File

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

24
test/app.e2e-spec.ts Normal file
View 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
View 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"
}
}

4
tsconfig.build.json Normal file
View File

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

22
tsconfig.json Normal file
View 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/*"
]
}
}
}

6212
yarn.lock Normal file

File diff suppressed because it is too large Load Diff