initialise
This commit is contained in:
commit
a8ebc332c3
9
Dockerfile
Normal file
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"]
|
72
README.md
Normal file
72
README.md
Normal 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
69
docker-compose.yml
Normal 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
4
nest-cli.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
87
package.json
Normal file
87
package.json
Normal 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
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
1040
src/abi/TornadoPool.json
Normal file
File diff suppressed because it is too large
Load Diff
5
src/abi/index.ts
Normal file
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
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
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
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
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;
|
358
src/artifacts/factories/OffchainOracle__factory.ts
Normal file
358
src/artifacts/factories/OffchainOracle__factory.ts
Normal 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;
|
||||
}
|
||||
}
|
1061
src/artifacts/factories/TornadoPool__factory.ts
Normal file
1061
src/artifacts/factories/TornadoPool__factory.ts
Normal file
File diff suppressed because it is too large
Load Diff
8
src/artifacts/index.ts
Normal file
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
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,
|
||||
},
|
||||
}));
|
26
src/config/configuration.ts
Normal file
26
src/config/configuration.ts
Normal 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
4
src/config/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './configuration';
|
||||
|
||||
export * from './bull.config';
|
||||
export * from './txManager.config';
|
15
src/config/txManager.config.ts
Normal file
15
src/config/txManager.config.ts
Normal 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),
|
||||
},
|
||||
}));
|
12
src/constants/contracts.ts
Normal file
12
src/constants/contracts.ts
Normal 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
2
src/constants/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './variables';
|
||||
export * from './contracts';
|
55
src/constants/variables.ts
Normal file
55
src/constants/variables.ts
Normal 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
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();
|
45
src/modules/api/api.controller.ts
Normal file
45
src/modules/api/api.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
16
src/modules/api/api.module.ts
Normal file
16
src/modules/api/api.module.ts
Normal 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 {}
|
71
src/modules/api/api.service.ts
Normal file
71
src/modules/api/api.service.ts
Normal 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 };
|
74
src/modules/api/api.validator.ts
Normal file
74
src/modules/api/api.validator.ts
Normal 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 };
|
4
src/modules/api/dto/create-subscribe.dto.ts
Normal file
4
src/modules/api/dto/create-subscribe.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class CreateApiDto {
|
||||
error: boolean;
|
||||
status: string;
|
||||
}
|
1
src/modules/api/dto/index.ts
Normal file
1
src/modules/api/dto/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './create-subscribe.dto';
|
1
src/modules/api/index.ts
Normal file
1
src/modules/api/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ApiModule } from './api.module';
|
11
src/modules/api/set-headers.middleware.ts
Normal file
11
src/modules/api/set-headers.middleware.ts
Normal 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();
|
||||
}
|
||||
}
|
17
src/modules/api/types/index.ts
Normal file
17
src/modules/api/types/index.ts
Normal 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
2
src/modules/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './queue';
|
||||
export * from './api';
|
66
src/modules/queue/base.processor.ts
Normal file
66
src/modules/queue/base.processor.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
1
src/modules/queue/index.ts
Normal file
1
src/modules/queue/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './queue.module';
|
20
src/modules/queue/queue.module.ts
Normal file
20
src/modules/queue/queue.module.ts
Normal 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 {}
|
173
src/modules/queue/transaction.processor.ts
Normal file
173
src/modules/queue/transaction.processor.ts
Normal 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.');
|
||||
}
|
||||
}
|
50
src/services/gas-price.service.ts
Normal file
50
src/services/gas-price.service.ts
Normal 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
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';
|
36
src/services/offchain-price.service.ts
Normal file
36
src/services/offchain-price.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
56
src/services/provider.service.ts
Normal file
56
src/services/provider.service.ts
Normal 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
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
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
1
src/utilities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './crypto';
|
24
test/app.e2e-spec.ts
Normal file
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
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"
|
||||
}
|
||||
}
|
4
tsconfig.build.json
Normal file
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
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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user