feat: init commit

This commit is contained in:
nikdementev 2021-07-13 20:06:36 +03:00
commit 0ce449620a
No known key found for this signature in database
GPG Key ID: 769B05D57CF16FE2
45 changed files with 8696 additions and 0 deletions

24
.eslintrc.js Normal file

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

34
.gitignore vendored Normal file

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

4
.prettierrc Normal file

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all",
}

73
README.md Normal file

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

4
nest-cli.json Normal file

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

88
package.json Normal file

@ -0,0 +1,88 @@
{
"name": "new-relayer",
"version": "0.0.1",
"description": "Relayer for Tornado.cash privacy solution. https://tornado.cash",
"author": "tornado.cash",
"private": true,
"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": "node dist/main",
"lint": "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/platform-express": "^8.0.0",
"ajv": "^8.6.1",
"bull": "^3.22.11",
"class-validator": "^0.13.1",
"config": "^3.3.6",
"ethers": "^5.4.1",
"gas-price-oracle": "^0.3.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"tx-manager": "^0.3.1",
"uuid": "^8.3.2",
"web3-utils": "^1.4.0"
},
"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/config": "^0.0.39",
"@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"
}
}

391
src/abi/TornadoPool.json Normal file

@ -0,0 +1,391 @@
[
{
"inputs": [
{
"internalType": "contract IVerifier",
"name": "_verifier2",
"type": "address"
},
{
"internalType": "contract IVerifier",
"name": "_verifier16",
"type": "address"
},
{
"internalType": "bytes32",
"name": "_currentRoot",
"type": "bytes32"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes",
"name": "account",
"type": "bytes"
}
],
"name": "EncryptedAccount",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bytes32",
"name": "commitment",
"type": "bytes32"
},
{
"indexed": false,
"internalType": "uint256",
"name": "index",
"type": "uint256"
},
{
"indexed": false,
"internalType": "bytes",
"name": "encryptedOutput",
"type": "bytes"
}
],
"name": "NewCommitment",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bytes32",
"name": "nullifier",
"type": "bytes32"
}
],
"name": "NewNullifier",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": false,
"internalType": "bytes",
"name": "key",
"type": "bytes"
}
],
"name": "PublicKey",
"type": "event"
},
{
"inputs": [],
"name": "FIELD_SIZE",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MAX_EXT_AMOUNT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_extAmount",
"type": "uint256"
}
],
"name": "calculateExternalAmount",
"outputs": [
{
"internalType": "int256",
"name": "",
"type": "int256"
}
],
"stateMutability": "pure",
"type": "function"
},
{
"inputs": [],
"name": "currentCommitmentIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "currentRoot",
"outputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "_nullifierHash",
"type": "bytes32"
}
],
"name": "isSpent",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "nullifierHashes",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_pubKey",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "_account",
"type": "bytes"
}
],
"name": "register",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_proof",
"type": "bytes"
},
{
"internalType": "bytes32",
"name": "_root",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_newRoot",
"type": "bytes32"
},
{
"internalType": "bytes32[]",
"name": "_inputNullifiers",
"type": "bytes32[]"
},
{
"internalType": "bytes32[2]",
"name": "_outputCommitments",
"type": "bytes32[2]"
},
{
"internalType": "uint256",
"name": "_outPathIndices",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_extAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_fee",
"type": "uint256"
},
{
"components": [
{
"internalType": "address payable",
"name": "recipient",
"type": "address"
},
{
"internalType": "address payable",
"name": "relayer",
"type": "address"
},
{
"internalType": "bytes",
"name": "encryptedOutput1",
"type": "bytes"
},
{
"internalType": "bytes",
"name": "encryptedOutput2",
"type": "bytes"
}
],
"internalType": "struct TornadoPool.ExtData",
"name": "_extData",
"type": "tuple"
},
{
"internalType": "bytes32",
"name": "_extDataHash",
"type": "bytes32"
}
],
"name": "transaction",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [],
"name": "verifier16",
"outputs": [
{
"internalType": "contract IVerifier",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "verifier2",
"outputs": [
{
"internalType": "contract IVerifier",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "_proof",
"type": "bytes"
},
{
"internalType": "bytes32",
"name": "_root",
"type": "bytes32"
},
{
"internalType": "bytes32",
"name": "_newRoot",
"type": "bytes32"
},
{
"internalType": "bytes32[]",
"name": "_inputNullifiers",
"type": "bytes32[]"
},
{
"internalType": "bytes32[2]",
"name": "_outputCommitments",
"type": "bytes32[2]"
},
{
"internalType": "uint256",
"name": "_outPathIndices",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_extAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_fee",
"type": "uint256"
},
{
"internalType": "bytes32",
"name": "_extDataHash",
"type": "bytes32"
}
],
"name": "verifyProof",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
}
]

5
src/abi/index.ts Normal file

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

18
src/app.module.ts Normal file

@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { baseConfig } from './config';
import { QueueModule } from './modules';
import { CommunicationsModule } from './communication';
@Module({
imports: [
ConfigModule.forRoot({
load: [baseConfig],
isGlobal: true,
}),
QueueModule,
CommunicationsModule,
],
})
export class AppModule {}

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

@ -0,0 +1,554 @@
/* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
import {
ethers,
EventFilter,
Signer,
BigNumber,
BigNumberish,
PopulatedTransaction,
BaseContract,
ContractTransaction,
Overrides,
PayableOverrides,
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 TornadoPoolInterface extends ethers.utils.Interface {
functions: {
"FIELD_SIZE()": FunctionFragment;
"MAX_EXT_AMOUNT()": FunctionFragment;
"calculateExternalAmount(uint256)": FunctionFragment;
"currentCommitmentIndex()": FunctionFragment;
"currentRoot()": FunctionFragment;
"isSpent(bytes32)": FunctionFragment;
"nullifierHashes(bytes32)": FunctionFragment;
"register(bytes,bytes)": FunctionFragment;
"transaction(bytes,bytes32,bytes32,bytes32[],bytes32[2],uint256,uint256,uint256,tuple,bytes32)": FunctionFragment;
"verifier16()": FunctionFragment;
"verifier2()": FunctionFragment;
"verifyProof(bytes,bytes32,bytes32,bytes32[],bytes32[2],uint256,uint256,uint256,bytes32)": FunctionFragment;
};
encodeFunctionData(
functionFragment: "FIELD_SIZE",
values?: undefined
): string;
encodeFunctionData(
functionFragment: "MAX_EXT_AMOUNT",
values?: undefined
): string;
encodeFunctionData(
functionFragment: "calculateExternalAmount",
values: [BigNumberish]
): string;
encodeFunctionData(
functionFragment: "currentCommitmentIndex",
values?: undefined
): string;
encodeFunctionData(
functionFragment: "currentRoot",
values?: undefined
): string;
encodeFunctionData(functionFragment: "isSpent", values: [BytesLike]): string;
encodeFunctionData(
functionFragment: "nullifierHashes",
values: [BytesLike]
): string;
encodeFunctionData(
functionFragment: "register",
values: [BytesLike, BytesLike]
): string;
encodeFunctionData(
functionFragment: "transaction",
values: [
BytesLike,
BytesLike,
BytesLike,
BytesLike[],
[BytesLike, BytesLike],
BigNumberish,
BigNumberish,
BigNumberish,
{
recipient: string;
relayer: string;
encryptedOutput1: BytesLike;
encryptedOutput2: BytesLike;
},
BytesLike
]
): string;
encodeFunctionData(
functionFragment: "verifier16",
values?: undefined
): string;
encodeFunctionData(functionFragment: "verifier2", values?: undefined): string;
encodeFunctionData(
functionFragment: "verifyProof",
values: [
BytesLike,
BytesLike,
BytesLike,
BytesLike[],
[BytesLike, BytesLike],
BigNumberish,
BigNumberish,
BigNumberish,
BytesLike
]
): string;
decodeFunctionResult(functionFragment: "FIELD_SIZE", data: BytesLike): Result;
decodeFunctionResult(
functionFragment: "MAX_EXT_AMOUNT",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "calculateExternalAmount",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "currentCommitmentIndex",
data: BytesLike
): Result;
decodeFunctionResult(
functionFragment: "currentRoot",
data: BytesLike
): Result;
decodeFunctionResult(functionFragment: "isSpent", data: BytesLike): Result;
decodeFunctionResult(
functionFragment: "nullifierHashes",
data: BytesLike
): Result;
decodeFunctionResult(functionFragment: "register", data: BytesLike): Result;
decodeFunctionResult(
functionFragment: "transaction",
data: BytesLike
): Result;
decodeFunctionResult(functionFragment: "verifier16", data: BytesLike): Result;
decodeFunctionResult(functionFragment: "verifier2", data: BytesLike): Result;
decodeFunctionResult(
functionFragment: "verifyProof",
data: BytesLike
): Result;
events: {
"EncryptedAccount(address,bytes)": EventFragment;
"NewCommitment(bytes32,uint256,bytes)": EventFragment;
"NewNullifier(bytes32)": EventFragment;
"PublicKey(address,bytes)": EventFragment;
};
getEvent(nameOrSignatureOrTopic: "EncryptedAccount"): EventFragment;
getEvent(nameOrSignatureOrTopic: "NewCommitment"): EventFragment;
getEvent(nameOrSignatureOrTopic: "NewNullifier"): EventFragment;
getEvent(nameOrSignatureOrTopic: "PublicKey"): EventFragment;
}
export class TornadoPool 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: TornadoPoolInterface;
functions: {
FIELD_SIZE(overrides?: CallOverrides): Promise<[BigNumber]>;
MAX_EXT_AMOUNT(overrides?: CallOverrides): Promise<[BigNumber]>;
calculateExternalAmount(
_extAmount: BigNumberish,
overrides?: CallOverrides
): Promise<[BigNumber]>;
currentCommitmentIndex(overrides?: CallOverrides): Promise<[BigNumber]>;
currentRoot(overrides?: CallOverrides): Promise<[string]>;
isSpent(
_nullifierHash: BytesLike,
overrides?: CallOverrides
): Promise<[boolean]>;
nullifierHashes(
arg0: BytesLike,
overrides?: CallOverrides
): Promise<[boolean]>;
register(
_pubKey: BytesLike,
_account: BytesLike,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
transaction(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extData: {
recipient: string;
relayer: string;
encryptedOutput1: BytesLike;
encryptedOutput2: BytesLike;
},
_extDataHash: BytesLike,
overrides?: PayableOverrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
verifier16(overrides?: CallOverrides): Promise<[string]>;
verifier2(overrides?: CallOverrides): Promise<[string]>;
verifyProof(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extDataHash: BytesLike,
overrides?: CallOverrides
): Promise<[boolean]>;
};
FIELD_SIZE(overrides?: CallOverrides): Promise<BigNumber>;
MAX_EXT_AMOUNT(overrides?: CallOverrides): Promise<BigNumber>;
calculateExternalAmount(
_extAmount: BigNumberish,
overrides?: CallOverrides
): Promise<BigNumber>;
currentCommitmentIndex(overrides?: CallOverrides): Promise<BigNumber>;
currentRoot(overrides?: CallOverrides): Promise<string>;
isSpent(
_nullifierHash: BytesLike,
overrides?: CallOverrides
): Promise<boolean>;
nullifierHashes(arg0: BytesLike, overrides?: CallOverrides): Promise<boolean>;
register(
_pubKey: BytesLike,
_account: BytesLike,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
transaction(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extData: {
recipient: string;
relayer: string;
encryptedOutput1: BytesLike;
encryptedOutput2: BytesLike;
},
_extDataHash: BytesLike,
overrides?: PayableOverrides & { from?: string | Promise<string> }
): Promise<ContractTransaction>;
verifier16(overrides?: CallOverrides): Promise<string>;
verifier2(overrides?: CallOverrides): Promise<string>;
verifyProof(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extDataHash: BytesLike,
overrides?: CallOverrides
): Promise<boolean>;
callStatic: {
FIELD_SIZE(overrides?: CallOverrides): Promise<BigNumber>;
MAX_EXT_AMOUNT(overrides?: CallOverrides): Promise<BigNumber>;
calculateExternalAmount(
_extAmount: BigNumberish,
overrides?: CallOverrides
): Promise<BigNumber>;
currentCommitmentIndex(overrides?: CallOverrides): Promise<BigNumber>;
currentRoot(overrides?: CallOverrides): Promise<string>;
isSpent(
_nullifierHash: BytesLike,
overrides?: CallOverrides
): Promise<boolean>;
nullifierHashes(
arg0: BytesLike,
overrides?: CallOverrides
): Promise<boolean>;
register(
_pubKey: BytesLike,
_account: BytesLike,
overrides?: CallOverrides
): Promise<void>;
transaction(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extData: {
recipient: string;
relayer: string;
encryptedOutput1: BytesLike;
encryptedOutput2: BytesLike;
},
_extDataHash: BytesLike,
overrides?: CallOverrides
): Promise<void>;
verifier16(overrides?: CallOverrides): Promise<string>;
verifier2(overrides?: CallOverrides): Promise<string>;
verifyProof(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extDataHash: BytesLike,
overrides?: CallOverrides
): Promise<boolean>;
};
filters: {
EncryptedAccount(
owner?: string | null,
account?: null
): TypedEventFilter<[string, string], { owner: string; account: string }>;
NewCommitment(
commitment?: null,
index?: null,
encryptedOutput?: null
): TypedEventFilter<
[string, BigNumber, string],
{ commitment: string; index: BigNumber; encryptedOutput: string }
>;
NewNullifier(
nullifier?: null
): TypedEventFilter<[string], { nullifier: string }>;
PublicKey(
owner?: string | null,
key?: null
): TypedEventFilter<[string, string], { owner: string; key: string }>;
};
estimateGas: {
FIELD_SIZE(overrides?: CallOverrides): Promise<BigNumber>;
MAX_EXT_AMOUNT(overrides?: CallOverrides): Promise<BigNumber>;
calculateExternalAmount(
_extAmount: BigNumberish,
overrides?: CallOverrides
): Promise<BigNumber>;
currentCommitmentIndex(overrides?: CallOverrides): Promise<BigNumber>;
currentRoot(overrides?: CallOverrides): Promise<BigNumber>;
isSpent(
_nullifierHash: BytesLike,
overrides?: CallOverrides
): Promise<BigNumber>;
nullifierHashes(
arg0: BytesLike,
overrides?: CallOverrides
): Promise<BigNumber>;
register(
_pubKey: BytesLike,
_account: BytesLike,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
transaction(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extData: {
recipient: string;
relayer: string;
encryptedOutput1: BytesLike;
encryptedOutput2: BytesLike;
},
_extDataHash: BytesLike,
overrides?: PayableOverrides & { from?: string | Promise<string> }
): Promise<BigNumber>;
verifier16(overrides?: CallOverrides): Promise<BigNumber>;
verifier2(overrides?: CallOverrides): Promise<BigNumber>;
verifyProof(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extDataHash: BytesLike,
overrides?: CallOverrides
): Promise<BigNumber>;
};
populateTransaction: {
FIELD_SIZE(overrides?: CallOverrides): Promise<PopulatedTransaction>;
MAX_EXT_AMOUNT(overrides?: CallOverrides): Promise<PopulatedTransaction>;
calculateExternalAmount(
_extAmount: BigNumberish,
overrides?: CallOverrides
): Promise<PopulatedTransaction>;
currentCommitmentIndex(
overrides?: CallOverrides
): Promise<PopulatedTransaction>;
currentRoot(overrides?: CallOverrides): Promise<PopulatedTransaction>;
isSpent(
_nullifierHash: BytesLike,
overrides?: CallOverrides
): Promise<PopulatedTransaction>;
nullifierHashes(
arg0: BytesLike,
overrides?: CallOverrides
): Promise<PopulatedTransaction>;
register(
_pubKey: BytesLike,
_account: BytesLike,
overrides?: Overrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
transaction(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extData: {
recipient: string;
relayer: string;
encryptedOutput1: BytesLike;
encryptedOutput2: BytesLike;
},
_extDataHash: BytesLike,
overrides?: PayableOverrides & { from?: string | Promise<string> }
): Promise<PopulatedTransaction>;
verifier16(overrides?: CallOverrides): Promise<PopulatedTransaction>;
verifier2(overrides?: CallOverrides): Promise<PopulatedTransaction>;
verifyProof(
_proof: BytesLike,
_root: BytesLike,
_newRoot: BytesLike,
_inputNullifiers: BytesLike[],
_outputCommitments: [BytesLike, BytesLike],
_outPathIndices: BigNumberish,
_extAmount: BigNumberish,
_fee: BigNumberish,
_extDataHash: BytesLike,
overrides?: CallOverrides
): Promise<PopulatedTransaction>;
};
}

36
src/artifacts/commons.ts Normal file

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

@ -0,0 +1,412 @@
/* Autogenerated file. Do not edit manually. */
/* tslint:disable */
/* eslint-disable */
import { Contract, Signer, utils } from "ethers";
import { Provider } from "@ethersproject/providers";
import type { TornadoPool, TornadoPoolInterface } from "../TornadoPool";
const _abi = [
{
inputs: [
{
internalType: "contract IVerifier",
name: "_verifier2",
type: "address",
},
{
internalType: "contract IVerifier",
name: "_verifier16",
type: "address",
},
{
internalType: "bytes32",
name: "_currentRoot",
type: "bytes32",
},
],
stateMutability: "nonpayable",
type: "constructor",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "owner",
type: "address",
},
{
indexed: false,
internalType: "bytes",
name: "account",
type: "bytes",
},
],
name: "EncryptedAccount",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "bytes32",
name: "commitment",
type: "bytes32",
},
{
indexed: false,
internalType: "uint256",
name: "index",
type: "uint256",
},
{
indexed: false,
internalType: "bytes",
name: "encryptedOutput",
type: "bytes",
},
],
name: "NewCommitment",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: false,
internalType: "bytes32",
name: "nullifier",
type: "bytes32",
},
],
name: "NewNullifier",
type: "event",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "owner",
type: "address",
},
{
indexed: false,
internalType: "bytes",
name: "key",
type: "bytes",
},
],
name: "PublicKey",
type: "event",
},
{
inputs: [],
name: "FIELD_SIZE",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "MAX_EXT_AMOUNT",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "uint256",
name: "_extAmount",
type: "uint256",
},
],
name: "calculateExternalAmount",
outputs: [
{
internalType: "int256",
name: "",
type: "int256",
},
],
stateMutability: "pure",
type: "function",
},
{
inputs: [],
name: "currentCommitmentIndex",
outputs: [
{
internalType: "uint256",
name: "",
type: "uint256",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "currentRoot",
outputs: [
{
internalType: "bytes32",
name: "",
type: "bytes32",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes32",
name: "_nullifierHash",
type: "bytes32",
},
],
name: "isSpent",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes32",
name: "",
type: "bytes32",
},
],
name: "nullifierHashes",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes",
name: "_pubKey",
type: "bytes",
},
{
internalType: "bytes",
name: "_account",
type: "bytes",
},
],
name: "register",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [
{
internalType: "bytes",
name: "_proof",
type: "bytes",
},
{
internalType: "bytes32",
name: "_root",
type: "bytes32",
},
{
internalType: "bytes32",
name: "_newRoot",
type: "bytes32",
},
{
internalType: "bytes32[]",
name: "_inputNullifiers",
type: "bytes32[]",
},
{
internalType: "bytes32[2]",
name: "_outputCommitments",
type: "bytes32[2]",
},
{
internalType: "uint256",
name: "_outPathIndices",
type: "uint256",
},
{
internalType: "uint256",
name: "_extAmount",
type: "uint256",
},
{
internalType: "uint256",
name: "_fee",
type: "uint256",
},
{
components: [
{
internalType: "address payable",
name: "recipient",
type: "address",
},
{
internalType: "address payable",
name: "relayer",
type: "address",
},
{
internalType: "bytes",
name: "encryptedOutput1",
type: "bytes",
},
{
internalType: "bytes",
name: "encryptedOutput2",
type: "bytes",
},
],
internalType: "struct TornadoPool.ExtData",
name: "_extData",
type: "tuple",
},
{
internalType: "bytes32",
name: "_extDataHash",
type: "bytes32",
},
],
name: "transaction",
outputs: [],
stateMutability: "payable",
type: "function",
},
{
inputs: [],
name: "verifier16",
outputs: [
{
internalType: "contract IVerifier",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "verifier2",
outputs: [
{
internalType: "contract IVerifier",
name: "",
type: "address",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
internalType: "bytes",
name: "_proof",
type: "bytes",
},
{
internalType: "bytes32",
name: "_root",
type: "bytes32",
},
{
internalType: "bytes32",
name: "_newRoot",
type: "bytes32",
},
{
internalType: "bytes32[]",
name: "_inputNullifiers",
type: "bytes32[]",
},
{
internalType: "bytes32[2]",
name: "_outputCommitments",
type: "bytes32[2]",
},
{
internalType: "uint256",
name: "_outPathIndices",
type: "uint256",
},
{
internalType: "uint256",
name: "_extAmount",
type: "uint256",
},
{
internalType: "uint256",
name: "_fee",
type: "uint256",
},
{
internalType: "bytes32",
name: "_extDataHash",
type: "bytes32",
},
],
name: "verifyProof",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "view",
type: "function",
},
];
export class TornadoPool__factory {
static readonly abi = _abi;
static createInterface(): TornadoPoolInterface {
return new utils.Interface(_abi) as TornadoPoolInterface;
}
static connect(
address: string,
signerOrProvider: Signer | Provider
): TornadoPool {
return new Contract(address, _abi, signerOrProvider) as TornadoPool;
}
}

6
src/artifacts/index.ts Normal file

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

@ -0,0 +1,18 @@
export const baseConfig = () => ({
port: parseInt(process.env.PORT, 10) || 8080,
bull: {
redis: {
host: 'localhost',
port: 6379,
},
settings: {
lockDuration: 300000,
lockRenewTime: 30000,
stalledInterval: 30000,
maxStalledCount: 3,
guardInterval: 5000,
retryProcessDelay: 5000,
drainDelay: 5,
},
},
});

1
src/config/index.ts Normal file

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

@ -0,0 +1,16 @@
import { ChainId } from '@/types';
export const CONTRACT_NETWORKS: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]: '0x8Bfac9EF3d73cE08C7CEC339C0fE3B2e57814c1E',
[ChainId.GOERLI]: '0x20a2D506cf52453D681F9E8E814A3437c6242B9e',
[ChainId.OPTIMISM]: '0xc436071dE853A4421c57ddD0CDDC116C735aa8b5',
};
export const RPC_LIST: { [chainId in ChainId]: string } = {
[ChainId.MAINNET]:
'https://mainnet.infura.io/v3/eb6a84e726614079948e0b1efce5baa5',
[ChainId.GOERLI]:
'https://eth-goerli.alchemyapi.io/v2/hlSj0EqPUuLGyyTExs6UqFKnXDrc_eOh',
[ChainId.OPTIMISM]:
'https://optimism-kovan.infura.io/v3/8f786b96d16046b78e0287fa61c6fcf8',
};

2
src/constants/index.ts Normal file

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

@ -0,0 +1,19 @@
import { BigNumber } from 'ethers';
const numbers = {
ZERO: 0,
ONE: 1,
TWO: 2,
SECOND: 1000,
ETH_DECIMALS: 18,
MERKLE_TREE_HEIGHT: 32,
};
const BG_ZERO = BigNumber.from(numbers.ZERO);
const FIELD_SIZE = BigNumber.from(
'21888242871839275222246405745257275088548364400416034343698204186575808495617',
);
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export { numbers, FIELD_SIZE, BG_ZERO, ZERO_ADDRESS };

10
src/contracts/index.ts Normal file

@ -0,0 +1,10 @@
import { ChainId } from '@/types';
import { CONTRACT_NETWORKS } from '@/constants';
import { getProviderWithSigner } from '@/services';
import { TornadoPool__factory as TornadoPoolFactory } from '@/artifacts';
export function getTornadoPool(chainId: ChainId) {
const provider = getProviderWithSigner(chainId);
return TornadoPoolFactory.connect(CONTRACT_NETWORKS[chainId], provider);
}

15
src/main.ts Normal file

@ -0,0 +1,15 @@
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
const configService = app.get(ConfigService);
await app.listen(configService.get('port'));
}
bootstrap()
.then((result) => console.log('result', result))
.catch((e) => console.log('error', e.message));

1
src/modules/index.ts Normal file

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

@ -0,0 +1,73 @@
import {
Processor,
OnQueueActive,
OnQueueFailed,
OnQueueRemoved,
OnQueueResumed,
OnQueueStalled,
OnQueueProgress,
OnQueueCompleted,
} from '@nestjs/bull';
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { Job, Queue } from 'bull';
import { v4 as uuid } from 'uuid';
@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);
}
async updateTask(job: Job<T>) {
const currentJob = await this.queue.getJob(job.id);
await currentJob.update(job.data);
}
private async createTask({ request }) {
const id = uuid();
await this.queue.add({ ...request, id });
return id;
}
async onModuleDestroy() {
if (this.queue) {
await this.queue.close();
}
}
}

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

@ -0,0 +1,24 @@
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import config from 'config';
import { AdvancedSettings } from 'bull';
import { RedisOptions } from 'ioredis';
import { WithdrawalProcessor } from './withdrawal.processor';
const redis = config.get<RedisOptions>('bull.redis');
const settings = config.get<AdvancedSettings>('bull.settings');
@Module({
imports: [
BullModule.registerQueue({
redis,
settings,
name: 'withdrawal',
}),
],
providers: [WithdrawalProcessor],
exports: [BullModule],
})
export class QueueModule {}

@ -0,0 +1,132 @@
import { InjectQueue, Process, Processor } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Job, Queue } from 'bull';
import { BigNumber } from 'ethers';
import { TxManager } from 'tx-manager';
import { getGasPrice } from '@/services';
import { toChecksumAddress, toWei } from '@/utilities';
import { BaseProcessor } from './base.processor';
export interface Withdrawal {
args: string[];
txHash: string;
status: string;
contract: string;
confirmations: number;
}
@Injectable()
@Processor('job')
export class WithdrawalProcessor extends BaseProcessor<Withdrawal> {
constructor(
private configService: ConfigService,
@InjectQueue('withdrawal') public withdrawalQueue: Queue,
) {
super();
this.queueName = 'withdrawal';
this.queue = withdrawalQueue;
}
@Process()
async processWithdrawals(job: Job<Withdrawal>) {
try {
await job.isActive();
const { args, contract } = job.data;
await this.checkFee({ contract, fee: args[4] });
} catch (err) {
await job.moveToFailed(err, true);
}
}
async submitTx(job: Job<Withdrawal>) {
const txManager = new TxManager({
privateKey: '',
rpcUrl: '',
config: { CONFIRMATIONS: '', MAX_GAS_PRICE: '', THROW_ON_REVERT: false },
});
const tx = await txManager.createTx(await getTxObject(job));
try {
const receipt = await tx
.send()
.on('transactionHash', async (txHash: string) => {
job.data.txHash = txHash;
job.data.status = 'SENT';
await job.update(job.data);
})
.on('mined', async () => {
job.data.status = 'MINED';
await job.update(job.data);
})
.on('confirmations', async (confirmations) => {
job.data.confirmations = confirmations;
await job.update(job.data);
});
if (receipt.status === 1) {
await job.isCompleted();
job.data.status = 'SENT';
await job.update(job.data);
} else {
throw new Error('Submitted transaction failed');
}
} catch (e) {
throw new Error(`Revert by smart contract ${e.message}`);
}
}
getInstance(address) {
const id = this.configService.get('network.id');
const instances = this.configService.get(`instances.${id}`);
for (const currency of Object.keys(instances)) {
const { instanceAddress, decimals } = instances[currency];
for (const amount of Object.keys(instanceAddress)) {
const contract = instances[currency].instanceAddress[amount];
if (toChecksumAddress(contract) === toChecksumAddress(address)) {
return { currency, amount, decimals };
}
}
}
return null;
}
async checkFee({ fee, contract }) {
const { amount } = this.getInstance(contract);
const gasLimit = this.configService.get<number>('gasLimits');
const { fast } = await getGasPrice(1);
const expense = BigNumber.from(toWei(fast.toString(), 'gwei')).mul(
gasLimit,
);
const serviceFee = this.configService.get<number>('fee');
const feePercent = BigNumber.from(toWei(amount))
.mul(toWei(serviceFee.toString()))
.div(100);
const desiredFee = expense.add(feePercent);
if (fee.lt(desiredFee)) {
throw new Error(
'Provided fee is not enough. Probably it is a Gas Price spike, try to resubmit.',
);
}
}
}

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

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

@ -0,0 +1 @@
export { StatusModule } from './stat.module';

@ -0,0 +1,18 @@
import { Controller, Get } from '@nestjs/common';
import { StatusService } from './stat.service';
@Controller()
export class StatusController {
constructor(private readonly service: StatusService) {}
@Get('/status')
async status(): Promise<Health> {
return await this.service.status();
}
@Get('/')
async main(): Promise<string> {
return this.service.main();
}
}

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { StatusService } from './stat.service';
import { StatusController } from './stat.controller';
@Module({
imports: [ConfigModule],
providers: [StatusService],
controllers: [StatusController],
})
export class StatusModule {}

@ -0,0 +1,26 @@
import { Queue } from 'bull';
import { InjectQueue } from '@nestjs/bull';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
class StatusService {
constructor(
private configService: ConfigService,
@InjectQueue('withdrawal') private withdrawalQueue: Queue,
) {}
async status(): Promise<Health> {
return {
status: '',
error: false,
};
}
main(): string {
return `This is <a href=https://tornado.cash>tornado.cash</a> Relayer service. Check the <a href=/status>/status</a> for settings`;
}
}
export { StatusService };

@ -0,0 +1,89 @@
import Ajv, { ValidateFunction } from 'ajv';
import { isAddress, toChecksumAddress } from '@/utilities';
const ajv = new Ajv();
ajv.addKeyword({
keyword: 'isAddress',
validate: (schema: any, address: string) => {
return isAddress(address);
},
errors: true,
});
ajv.addKeyword({
keyword: 'isKnownContract',
validate: (schema: any, address: string) => {
try {
return address !== null;
} catch (e) {
return false;
}
},
errors: true,
});
ajv.addKeyword({
keyword: 'isFeeRecipient',
validate: (schema: any, address: string) => {
try {
return toChecksumAddress('') === toChecksumAddress(address);
} catch (e) {
return false;
}
},
errors: true,
});
const addressType = {
type: 'string',
pattern: '^0x[a-fA-F0-9]{40}$',
isAddress: true,
};
const proofType = { type: 'string', pattern: '^0x[a-fA-F0-9]{512}$' };
const bytes32Type = { type: 'string', pattern: '^0x[a-fA-F0-9]{64}$' };
const instanceType = { ...addressType, isKnownContract: true };
const relayerType = { ...addressType, isFeeRecipient: true };
const tornadoWithdrawSchema = {
type: 'object',
properties: {
proof: proofType,
contract: instanceType,
args: {
type: 'array',
maxItems: 6,
minItems: 6,
items: [
bytes32Type,
bytes32Type,
addressType,
relayerType,
bytes32Type,
bytes32Type,
],
},
},
additionalProperties: false,
required: ['proof', 'contract', 'args'],
};
const validateTornadoWithdraw = ajv.compile(tornadoWithdrawSchema);
function getInputError(
validator: ValidateFunction,
data: typeof tornadoWithdrawSchema,
) {
validator(data);
if (validator.errors) {
const [error] = validator.errors;
return error.message;
}
return null;
}
function validateWithdrawRequest(data: typeof tornadoWithdrawSchema) {
return getInputError(validateTornadoWithdraw, data);
}
export { validateWithdrawRequest };

@ -0,0 +1,4 @@
type Health = {
status: string;
error: boolean;
};

26
src/services/ether.ts Normal file

@ -0,0 +1,26 @@
import { ethers } from 'ethers';
import { ChainId } from '@/types';
import { RPC_LIST } from '@/constants';
interface Options {
url: string;
}
export class Provider {
public provider: ethers.providers.JsonRpcProvider;
constructor(options: Options) {
this.provider = new ethers.providers.JsonRpcProvider(options.url);
}
}
export function getProvider(chainId: ChainId): Provider {
return new Provider({ url: RPC_LIST[chainId] });
}
export function getProviderWithSigner(
chainId: ChainId,
): ethers.providers.BaseProvider {
return ethers.providers.getDefaultProvider(RPC_LIST[chainId]);
}

91
src/services/flashbot.ts Normal file

@ -0,0 +1,91 @@
import { Wallet, PopulatedTransaction } from 'ethers';
import {
FlashbotsBundleProvider,
FlashbotsBundleResolution,
} from '@flashbots/ethers-provider-bundle';
import { ChainId } from '@/types';
import { numbers } from '@/constants';
import { getProviderWithSigner } from '@/services';
const authSigner = Wallet.createRandom();
const FLASH_BOT_RPC: { [key in ChainId]: { name: string; url: string } } = {
[ChainId.GOERLI]: {
url: 'https://relay-goerli.flashbots.net/',
name: 'goerli',
},
[ChainId.MAINNET]: {
url: '',
name: '',
},
};
async function sendFlashBotTransaction(
transaction: PopulatedTransaction,
chainId: ChainId,
) {
const provider = getProviderWithSigner(chainId);
const { url, name } = FLASH_BOT_RPC[chainId];
const flashBotsProvider = await FlashbotsBundleProvider.create(
provider,
authSigner,
url,
name,
);
const nonce = await provider.getTransactionCount(authSigner.address);
const mergedTx = {
...transaction,
nonce,
from: authSigner.address,
};
const signedTransaction = await authSigner.signTransaction(mergedTx);
const TIME_10_BLOCK = 130;
const blockNumber = await provider.getBlockNumber();
const minTimestamp = (await provider.getBlock(blockNumber)).timestamp;
const maxTimestamp = minTimestamp + TIME_10_BLOCK;
const targetBlockNumber = blockNumber + numbers.TWO;
const simulation = await flashBotsProvider.simulate(
[signedTransaction],
targetBlockNumber,
);
if ('error' in simulation) {
console.log(`Simulation Error: ${simulation.error.message}`);
} else {
console.log(
`Simulation Success: ${JSON.stringify(simulation, null, numbers.TWO)}`,
);
}
const bundleSubmission = await flashBotsProvider.sendBundle(
[{ signedTransaction }],
targetBlockNumber,
{
minTimestamp,
maxTimestamp,
},
);
if ('error' in bundleSubmission) {
throw new Error(bundleSubmission.error.message);
}
const waitResponse = await bundleSubmission.wait();
const bundleSubmissionSimulation = await bundleSubmission.simulate();
console.log({
bundleSubmissionSimulation,
waitResponse: FlashbotsBundleResolution[waitResponse],
});
}
export { sendFlashBotTransaction };

3
src/services/index.ts Normal file

@ -0,0 +1,3 @@
export * from './ether';
export * from './oracle';
export * from './flashbot';

29
src/services/oracle.ts Normal file

@ -0,0 +1,29 @@
import { GasPriceOracle } from 'gas-price-oracle';
import { GasPrice } from 'gas-price-oracle/lib/types';
import { ChainId } from '@/types';
import { RPC_LIST, numbers } from '@/constants';
const SECONDS = 10;
const TEN_SECOND = SECONDS * numbers.SECOND;
const OPTIMISM_GAS_PRICE = {
fast: 0.015,
low: 0.015,
instant: 0.015,
standard: 0.015,
};
const getGasPrice = async (chainId: ChainId): Promise<GasPrice> => {
if (chainId === ChainId.OPTIMISM) {
return OPTIMISM_GAS_PRICE;
}
const instance = new GasPriceOracle({
timeout: TEN_SECOND,
defaultRpc: RPC_LIST[ChainId.MAINNET],
});
return await instance.gasPrices();
};
export { getGasPrice };

9
src/types/index.ts Normal file

@ -0,0 +1,9 @@
const MAINNET_CHAIN_ID = 1
const GOERLI_CHAIN_ID = 5
const OPTIMISM_CHAIN_ID = 69
export enum ChainId {
MAINNET = MAINNET_CHAIN_ID,
GOERLI = GOERLI_CHAIN_ID,
OPTIMISM = OPTIMISM_CHAIN_ID,
}

37
src/utilities/crypto.ts Normal file

@ -0,0 +1,37 @@
import { BigNumber, utils, BigNumberish } from 'ethers';
import {
toChecksumAddress as checksumAddress,
isAddress as checkAddress,
} from 'web3-utils';
import { numbers } from '@/constants';
// eslint-disable-next-line
export function isAddress(value: any): boolean {
try {
return checkAddress(value);
} catch {
return false;
}
}
// eslint-disable-next-line
export function toChecksumAddress(value: any): string {
return checksumAddress(value);
}
export function toWei(value: string, uintName = 'wei') {
return utils.parseUnits(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);
}

2
src/utilities/index.ts Normal file

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

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

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json Normal file

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

4
tsconfig.build.json Normal file

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

21
tsconfig.json Normal file

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

6325
yarn.lock Normal file

File diff suppressed because it is too large Load Diff