Added basic service

This commit is contained in:
Tornado Contrib 2024-04-24 17:26:02 +00:00
parent ef1c99bf57
commit e29be4a1e6
Signed by: tornadocontrib
GPG Key ID: 60B4DF1A076C64B1
41 changed files with 10291 additions and 436 deletions

@ -1,11 +1,15 @@
MERKLE_TREE_HEIGHT=20
# in wei
ETH_AMOUNT=100000000000000000
# check config.js
TOKEN_AMOUNT=100000000000000000
ERC20_TOKEN=
PRIVATE_KEY=
#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105
#TOKEN_AMOUNT=1000000
RPC_URL=https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
ETHRPC_URL=https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
GRAPH_URL=https://api.thegraph.com
ETHGRAPH_URL=https://api.thegraph.com
DISABLE_GRAPH=true
RELAYER=torn-city.eth
WALLET_WITHDRAWAL=true
TOR_PORT=9150
TOKEN=0x77777FeDdddFfC19Ff86DB637967013e6C6A116C
VIEW_ONLY=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
MNEMONIC="test test test test test test test test test test test junk"
MNEMONIC_INDEX=0
PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
NON_INTERACTIVE=true
LOCAL_RPC=true

@ -5,7 +5,11 @@ module.exports = {
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
"prettier",
"plugin:prettier/recommended",
],
"overrides": [
{
@ -26,9 +30,18 @@ module.exports = {
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
"@typescript-eslint",
"prettier"
],
"rules": {
"prettier/prettier": [
"error",
{
singleQuote: true,
printWidth: 120
}
],
"import/order": ["error"],
"indent": [
"error",
2
@ -44,6 +57,7 @@ module.exports = {
"semi": [
"error",
"always"
]
],
"@typescript-eslint/no-unused-vars": ["warn"]
}
}

2
.gitattributes vendored Normal file

@ -0,0 +1,2 @@
dist/* linguist-vendored
static/* linguist-vendored

565
README.md

@ -1,18 +1,38 @@
# Tornado-CLI
<div class="hero" align="center">
Command line tool to interact with [Tornado Cash](https://tornado.ws).
<img src="./logo2.png">
### How to install tornado cli
# Tornado CLI
Download and install [node.js](https://nodejs.org/en/download/).
Modern Toolsets for [Privacy Pools](https://www.forbes.com/sites/tomerniv/2023/09/07/privacy-pools-bridging-the-gap-between-blockchain-and-regulatory-compliance) on Ethereum
You also need to install C++ build tools in order to do 'npm install', for more information please checkout https://github.com/nodejs/node-gyp#on-unix.
[![Telegram Badge](https://img.shields.io/badge/Join%20Group-telegram?style=flat&logo=telegram&color=blue&link=https%3A%2F%2Ft.me%2Ftornadocli)](https://t.me/tornadocli) [![Element Badge](https://img.shields.io/badge/Join%20Element%20Chat-Element?style=flat&logo=element&color=green&link=https%3A%2F%2Felement.tornadocash.social%2F)](https://element.tornadocash.social) [![Discourse Badge](https://img.shields.io/badge/Discourse-Discourse?style=flat&logo=Discourse&color=black&link=https%3A%2F%2Fforum.tornado.ws%2F)](https://forum.tornado.ws/)
- For Windows: https://stackoverflow.com/a/64224475
</div>
- For MacOS: Install XCode Command Line Tools
### About Tornado CLI
- For Linux: Install make & gcc, for ubuntu `$ sudo apt-get install -y build-essentials`
Tornado CLI is a complete rewrite of old tornado-cli command line tool with the following changes
+ Rewritten to [TypeScript](https://www.typescriptlang.org/)
+ Built on top of modern tech stacks like [Ethers.js V6](https://docs.ethers.org/v6/) or [TypeChain](https://github.com/dethcrypto/TypeChain)
+ Creates resource heavy Merkle Trees on a separate thread using Web Workers / Worker Threads
+ Resilient API requests made by [cross-fetch](https://www.npmjs.com/package/cross-fetch) and retries, especially for Tor Users
+ Modular design
### How to install Tornado CLI
Download and install the latest [Node.js](https://nodejs.org/en/download/) LTS version (Current: 20.x).
You also need to install [Yarn](https://yarnpkg.com/) 1.x package manager using following command
```bash
$ npm i -g yarn
```
If you have git installed on your system, clone the master branch.
@ -26,68 +46,120 @@ After downloading or cloning the repository, you must install necessary librarie
```bash
$ cd tornado-cli
$ npm install
$ yarn
```
If you want to use Tor connection to conceal ip address, install [Tor Browser](https://www.torproject.org/download/) and add `--tor 9150` for `cli.js` if you connect tor with browser. (For non tor-browser tor service you can use the default 9050 port).
If you want to use Tor connection to conceal ip address, install [Tor Browser](https://www.torproject.org/download/) and add `--tor-port 9150` for `yarn start` if you connect tor with browser. (For non tor-browser tor service you can use the default 9050 port).
Note that you should reset your tor connection by restarting the browser every time when you deposit & withdraw otherwise you will have the same exit node used for connection.
### Goerli, Mainnet, Binance Smart Chain, Gnosis Chain, Polygon Network, Arbitrum, Avalanche
### Configuration
1. `node cli.js --help`
2. If you want to use secure, anonymous tor connection add `--tor <torPort>` behind the command.
Commands like `yarn deposit`, `yarn depositInvoice` or `yarn send` would require either a valid view-only wallet address or mnemonic or a private key to perform actions ( Because, if you want to deposit to pools or send the token, you would need your wallet right? ).
You can apply those values with two options
1. Refer the `VIEW_ONLY` or `MNEMONIC` or `PRIVATE_KEY` value from the `.env.example` file and create a new `.env` file with the appropriate value.
2. Supply command-line options `--view-only` or `--mnemonic` or `--private-key` during the commands.
### How to start
1. `yarn start --help`
2. If you want to use a secure, anonymous tor connection add `--tor-port <torPort>` behind the command or add `TOR_PORT` at the `.env` file.
3. Add `PRIVATE_KEY` to `.env` file (optional, only if you want to use it for many operations) - open `.env.example` file, add private key after `PRIVATE_KEY=` and rename file to `.env`.
#### To deposit:
```bash
$ node cli.js deposit <currency> <amount> --rpc <rpc url> --tor <torPort> --private-key <private key>
$ yarn deposit <netId> <currency> <amount>
```
Note that `--tor <torPort>` is optional, and use `--private-key <private key>` only if you didn't add it to `.env` file.
For RPC nodes please refer to the list of public RPC nodes below.
RPC nodes are now selected automatically however if you want to change you can use the `--rpc-url` option with the RPC URL following behind (Find any from https://chainlist.org)
##### Example:
```bash
$ node cli.js deposit ETH 0.1 --rpc https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607 --tor 9150
$ yarn deposit 1 ETH 0.1
Your note: tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652
Tornado ETH balance is 8.9
Sender account ETH balance is 1004873.470619891361352542
Submitting deposit transaction
Tornado ETH balance is 9
Sender account ETH balance is 1004873.361652048361352542
====================================================================
_____ _ ____ _ ___
|_ _|__ _ __ _ __ __ _ __| | ___ / ___| | |_ _|
| |/ _ \| '__| '_ \ / _` |/ _` |/ _ \ | | | | | |
| | (_) | | | | | | (_| | (_| | (_) | | |___| |___ | |
|_|\___/|_| |_| |_|\__,_|\__,_|\___/ \____|_____|___|
====================================================================
┌───────────────────────────────────────────────────────┐
│ Program Options │
├──────────┬────────────────────────────────────────────┤
│ IP │ │
├──────────┼────────────────────────────────────────────┤
│ Is Tor │ false │
└──────────┴────────────────────────────────────────────┘
Confirm? [Y/n]
Y
New deposit: {
"currency": "eth",
"amount": "0.1",
"netId": 1,
"nullifier": "447406849888912231527060205543641504804080944127170669822752873679919469946",
"secret": "174744806548157587591107992863852449674497869575837897594110402641101509504",
"note": "tornado-eth-0.1-1-0x7a558fc1c1169a22661ce7256ff1a525b494916832c6ea10ba8209652a39fd80d3c62ebb20ad83c0777ceb1fd7a463eb0e952fcad9223a6f37cc1cede662",
"noteHex": "0x7a558fc1c1169a22661ce7256ff1a525b494916832c6ea10ba8209652a39fd80d3c62ebb20ad83c0777ceb1fd7a463eb0e952fcad9223a6f37cc1cede662",
"invoice": "tornadoInvoice-eth-0.1-1-0x0ad52c6472894d3521e40b27af6c590d0567c11819e9bacd4a872f3cc7056a54",
"commitmentHex": "0x0ad52c6472894d3521e40b27af6c590d0567c11819e9bacd4a872f3cc7056a54",
"nullifierHex": "0x2eb4ea29bbf999c40c7ceef35dc5917fbc6ed591fa7eb50b0aea94d3c1254d67"
}
┌────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Send Transaction? │
├──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────┤
│ to │ 0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ from │ │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ data │ 0x13d98d1300000000000000000000000012d66f...0000000000000000000000000000000000000000 │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ gasLimit │ 1238242 │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ maxFeePerGas │ 10000000000 │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ maxPriorityFeePerGas │ 1000000 │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ value │ 100000000000000000 │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ type │ 2 │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ nonce │ │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
│ chainId │ 1 │
└──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────┘
Confirm? [Y/n]
Y
Sent transaction 0x0ad52c6472894d3521e40b27af6c590d0567c11819e9bacd4a872f3cc7056a54
```
#### To withdraw:
```bash
$ node cli.js withdraw <note> <recipient> --rpc <rpc url> --relayer <relayer url> --tor <torPort> --private-key <private key>
$ yarn withdraw <note> <recipient>
```
Note that `--relayer <relayer url>`, `--tor <torPort>` and `--rpc <rpc url>` are optional parameters, and use `--private-key <private key>` only if you withdraw without relayer.
You can don't provide RPC link and withdrawal will be made via default RPC for the chain to which note belongs.
Note that `--relayer <relayer url>`, `--tor-port <torPort>` and `--rpc-url <rpc url>` are optional parameters, and use `--wallet-withdrawal --private-key <private key>` only if you withdraw without the relayer.
If you want to use Tornado Cash relayer for your first withdrawal to your new ethereum account, please refer to the list of relayers below.
If you don't need relayer while doing withdrawals, you must provide your withdrawal account's private key - either as parameter, or by adding it to `.env` file.
The CLI will select the relayer from the Relayer Registry contract by scoring for more information about how the relayers are being selected you could refer here https://docs.tornado.ws/general/guides/relayer.html.
##### Example:
```bash
$ node cli.js withdraw tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607 --relayer https://goerli-relay.example.org --tor 9150
Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
Getting current state from tornado contract
Generating SNARK proof
Proof time: 9117.051ms
Sending withdraw transaction through relay
Transaction submitted through the relay. View transaction on etherscan https://goerli.etherscan.io/tx/0xcb21ae8cad723818c6bc7273e83e00c8393fcdbe74802ce5d562acad691a2a7b
Transaction mined in block 17036120
Done
$ yarn withdraw tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16
```
### (Optional) Creating Deposit Notes & Invoices offline
@ -96,10 +168,10 @@ One of the main features of tornado-cli is that it supports creating deposit not
After the private-key like notes are backed up somewhere safe, you can copy the created deposit invoices and use them to create new deposit transaction on online environment.
#### To create deposit notes with `createNote` command.
#### To create deposit notes with `create (createDeposit)` command.
```bash
$ node cli.js createNote <currency> <amount> <chainId>
$ yarn createDeposit <chainId> <currency> <amount>
```
To find out chainId value for your network, refer to https://chainlist.org/.
@ -107,11 +179,39 @@ To find out chainId value for your network, refer to https://chainlist.org/.
##### Example:
```bash
$ node cli.js createNote ETH 0.1 5
Your note: tornado-eth-0.1-5-0x1d9771a7b9f8b6c03d33116208ce8db1aa559d33e65d22dd2ff78375fc6b635f930536d2432b4bde0178c72cfc79d6b27023c5d9de60985f186b34c18c00
Your invoice for deposit: tornadoInvoice-eth-0.1-5-0x1b680c7dda0c2dd1b85f0fe126d49b16ed594b3cd6d5114db5f4593877a6b84f
Backed up deposit note as ./backup-tornado-eth-0.1-5-0x1d9771a7.txt
Backed up invoice as ./backup-tornadoInvoice-eth-0.1-5-0x1b680c7d.txt
$ yarn createDeposit 1 ETH 0.1
====================================================================
_____ _ ____ _ ___
|_ _|__ _ __ _ __ __ _ __| | ___ / ___| | |_ _|
| |/ _ \| '__| '_ \ / _` |/ _` |/ _ \ | | | | | |
| | (_) | | | | | | (_| | (_| | (_) | | |___| |___ | |
|_|\___/|_| |_| |_|\__,_|\__,_|\___/ \____|_____|___|
====================================================================
New deposit: {
"currency": "eth",
"amount": "0.1",
"netId": 1,
"nullifier": "211996166335523441594778881807923807770971048532637197153927747977918013739",
"secret": "443763519478082043322725320022481467938478224697448315688237911974763852521",
"note": "tornado-eth-0.1-1-0x2be1c7f1d7cc77e96e394b108ddc2bf2b25b8c2158ebb92b6cc347d74efc77e9723c6b6ac0654e00588f7ec8177e8a7dc47bc3b00fe75c7d094dc24729fb",
"noteHex": "0x2be1c7f1d7cc77e96e394b108ddc2bf2b25b8c2158ebb92b6cc347d74efc77e9723c6b6ac0654e00588f7ec8177e8a7dc47bc3b00fe75c7d094dc24729fb",
"invoice": "tornadoInvoice-eth-0.1-1-0x24d26c7d0381dc34941b6fe9e0d622c7efadc0bfdc9d3f7e8dcb1e490e6ce9ea",
"commitmentHex": "0x24d26c7d0381dc34941b6fe9e0d622c7efadc0bfdc9d3f7e8dcb1e490e6ce9ea",
"nullifierHex": "0x0e3ec1a269e2e143bc8b2dfca40975bc8f5af2754917cd1f839e499162b28324"
}
Transaction Data: {
"to": "0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b",
"value": "100000000000000000",
"data": "0x13d98d1300000000000000000000000012d66f87a04a9e220743712ce6d9bb1b5616b8fc24d26c7d0381dc34941b6fe9e0d622c7efadc0bfdc9d3f7e8dcb1e490e6ce9ea00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000"
}
Done in 4.33s.
```
#### To create corresponding deposit transaction with `depositInvoice` command.
@ -119,374 +219,23 @@ Backed up invoice as ./backup-tornadoInvoice-eth-0.1-5-0x1b680c7d.txt
Creating deposit transaction with `depositInvoice` only requires valid deposit note created by `createNote` command, so that the deposit note could be stored without exposed anywhere.
```bash
$ node cli.js depositInvoice <invoice> --rpc <rpc url> --tor <tor port>
$ node cli.js depositInvoice <invoice>
```
Parameter `--rpc <rpc url>` is optional, if you don't provide it, default RPC (corresponding to note chain) will be used.
Parameter `--rpc-url <rpc url>` is optional, if you don't provide it, default RPC (corresponding to note chain) will be used.
##### Example:
```bash
node cli.js depositInvoice tornadoInvoice-eth-0.1-5-0x1b680c7dda0c2dd1b85f0fe126d49b16ed594b3cd6d5114db5f4593877a6b84f --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --tor 9150
Using tor network
Your remote IP address is xx.xx.xx.xx from xx.
Creating ETH 0.1 deposit for Goerli network.
Using supplied invoice for deposit
Tornado contract balance is xxx.x ETH
Sender account balance is x.xxxxxxx ETH
Submitting deposit transaction
Submitting transaction to the remote node
View transaction on block explorer https://goerli.etherscan.io/tx/0x6ded443caed8d6f2666841149532c64bee149a9a8e1070ed4c91a12dd1837747
Tornado contract balance is xxx.x ETH
Sender account balance is x.xxxxxxx ETH
yarn depositInvoice tornadoInvoice-eth-0.1-1-0x24d26c7d0381dc34941b6fe9e0d622c7efadc0bfdc9d3f7e8dcb1e490e6ce9ea
```
### List of rpc & relayers for withdrawal
```json
{
"netId1": {
"rpcUrls": {
"publicRpc1": {
"name": "1RPC",
"url": "https://1rpc.io/eth"
},
"Chainnodes": {
"name": "Chainnodes",
"url": "https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607"
}
},
"relayers": {
"0xproxy.eth": {
"url": "0xproxy.eth",
"name": "0xproxy.eth",
"cachedUrl": "http://mainnet.0x0relayer.xyz/"
},
"0xtornadocash.eth": {
"url": "0xtornadocash.eth",
"name": "0xtornadocash.eth",
"cachedUrl": "http://mainnet-tornado-0xtornadocash-eth.crypto-bot.exchange/"
},
"available-reliable-relayer.eth": {
"url": "available-reliable-relayer.eth",
"name": "available-reliable-relayer.eth",
"cachedUrl": "http://mainnet-tornado-arr-eth.crypto-bot.exchange/"
},
"bitah.eth": {
"url": "bitah.eth",
"name": "bitah.eth",
"cachedUrl": "http://tornado.bitah.link/"
},
"cheap-relayer.eth": {
"url": "cheap-relayer.eth",
"name": "cheap-relayer.eth",
"cachedUrl": "http://mainnet-tornado.cheap-relayer.xyz/"
},
"em3tornado.eth": {
"url": "em3tornado.eth",
"name": "em3tornado.eth",
"cachedUrl": "http://em3torn.com/"
},
"lowcost.eth": {
"url": "lowcost.eth",
"name": "lowcost.eth",
"cachedUrl": "http://mainnet-tornado.low-fee.xyz/"
},
"relayer007.eth": {
"url": "relayer007.eth",
"name": "relayer007.eth",
"cachedUrl": "http://torn.relayersdao.finance/"
},
"reltor.eth": {
"url": "reltor.eth",
"name": "reltor.eth",
"cachedUrl": "http://reltor.su/"
},
"shadow-out.eth": {
"url": "shadow-out.eth",
"name": "shadow-out.eth",
"cachedUrl": "http://torn-relayer.shadowninjas.xyz/"
},
"thornadope.eth": {
"url": "thornadope.eth",
"name": "thornadope.eth",
"cachedUrl": "http://thornadope.xyz/"
},
"torn-eth.eth": {
"url": "torn-eth.eth",
"name": "torn-eth.eth",
"cachedUrl": "http://mainnet-tornado.50swap.com/"
},
"torn-relayers.eth": {
"url": "torn-relayers.eth",
"name": "torn-relayers.eth",
"cachedUrl": "http://mainnet.tornrelayers.com/"
},
"torn-secure.eth": {
"url": "torn-secure.eth",
"name": "torn-secure.eth",
"cachedUrl": "http://mainnet-tornado.secure-relays.site/"
},
"torn69.eth": {
"url": "torn69.eth",
"name": "torn69.eth",
"cachedUrl": "http://m2.torn69.gq/"
},
"tornado-crypto-bot-exchange.eth": {
"url": "tornado-crypto-bot-exchange.eth",
"name": "tornado-crypto-bot-exchange.eth",
"cachedUrl": "http://tornado.crypto-bot.exchange/"
},
"torndao.eth": {
"url": "torndao.eth",
"name": "torndao.eth",
"cachedUrl": "http://eth-tornado.zkany.com/"
},
"tornrelayers.eth": {
"url": "tornrelayers.eth",
"name": "tornrelayers.eth",
"cachedUrl": "http://mainnet-tornado-tornrelayer-eth.crypto-bot.exchange/"
}
}
},
"netId56": {
"rpcUrls": {
"publicRpc1": {
"name": "BSC Public RPC 1",
"url": "https://1rpc.io/bnb"
},
"publicRpc2": {
"name": "BSC Public RPC 2",
"url": "https://bsc-dataseed1.defibit.io"
},
"publicRpc3": {
"name": "BSC Public RPC 3",
"url": "https://bsc-dataseed1.ninicoin.io"
}
},
"relayers": {
"0xproxy.eth": {
"url": "0xproxy.eth",
"name": "0xproxy.eth",
"cachedUrl": "http://bsc.0x0relayer.xyz/"
},
"bitah.eth": {
"url": "bitah.eth",
"name": "bitah.eth",
"cachedUrl": "http://bsc-tornado.bitah.link/"
},
"cheap-relayer.eth": {
"url": "cheap-relayer.eth",
"name": "cheap-relayer.eth",
"cachedUrl": "http://bsc-tornado.cheap-relayer.xyz/"
},
"em3tornado.eth": {
"url": "em3tornado.eth",
"name": "em3tornado.eth",
"cachedUrl": "http://bsc.em3torn.com/"
},
"lowcost.eth": {
"url": "lowcost.eth",
"name": "lowcost.eth",
"cachedUrl": "http://bsc-tornado.low-fee.xyz/"
},
"relayer007.eth": {
"url": "relayer007.eth",
"name": "relayer007.eth",
"cachedUrl": "http://bsc.relayersdao.finance/"
},
"reltor.eth": {
"url": "reltor.eth",
"name": "reltor.eth",
"cachedUrl": "http://bsc.reltor.su/"
},
"thornadope.eth": {
"url": "thornadope.eth",
"name": "thornadope.eth",
"cachedUrl": "http://tornado-bsc.thornadope.xyz/"
},
"torn-relay.eth": {
"url": "torn-relay.eth",
"name": "torn-relay.eth",
"cachedUrl": "http://bsc.torn-relay.com/"
},
"torn-relayers.eth": {
"url": "torn-relayers.eth",
"name": "torn-relayers.eth",
"cachedUrl": "http://bsc.tornrelayers.com/"
},
"torn-secure.eth": {
"url": "torn-secure.eth",
"name": "torn-secure.eth",
"cachedUrl": "http://bsc-v0.secure-relays.site/"
},
"torn69.eth": {
"url": "torn69.eth",
"name": "torn69.eth",
"cachedUrl": "http://bsc.0x111111.xyz/"
},
"tornado-crypto-bot-exchange.eth": {
"url": "tornado-crypto-bot-exchange.eth",
"name": "tornado-crypto-bot-exchange.eth",
"cachedUrl": "http://tornado-bsc.crypto-bot.exchange/"
},
"torndao.eth": {
"url": "torndao.eth",
"name": "torndao.eth",
"cachedUrl": "http://bsc-tornado.zkany.com/"
}
}
},
"netId100": {
"rpcUrls": {
"publicRpc": {
"name": "Gnosis Chain RPC",
"url": "https://rpc.gnosischain.com"
}
},
"relayers": {
"torndao.eth": {
"url": "torndao.eth",
"name": "torndao.eth",
"cachedUrl": "http://gnosis-tornado.zkany.com/"
}
}
},
"netId137": {
"rpcUrls": {
"publicRpc1": {
"name": "1RPC",
"url": "https://1rpc.io/matic"
},
"Chainnodes": {
"name": "Chainnodes",
"url": "https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607"
}
},
"relayers": {
"cheap-relayer.eth": {
"url": "cheap-relayer.eth",
"name": "cheap-relayer.eth",
"cachedUrl": "http://polygon-tornado.cheap-relayer.xyz/"
},
"lowcost.eth": {
"url": "lowcost.eth",
"name": "lowcost.eth",
"cachedUrl": "http://polygon-tornado.low-fee.xyz/"
},
"relayer007.eth": {
"url": "relayer007.eth",
"name": "relayer007.eth",
"cachedUrl": "http://matic.relayersdao.finance/"
},
"reltor.eth": {
"url": "reltor.eth",
"name": "reltor.eth",
"cachedUrl": "http://polygon.reltor.su/"
},
"thornadope.eth": {
"url": "thornadope.eth",
"name": "thornadope.eth",
"cachedUrl": "http://tornado-polygon.thornadope.xyz/"
},
"torn-secure.eth": {
"url": "torn-secure.eth",
"name": "torn-secure.eth",
"cachedUrl": "http://poly-v0.secure-relays.site/"
},
"tornado-crypto-bot-exchange.eth": {
"url": "tornado-crypto-bot-exchange.eth",
"name": "tornado-crypto-bot-exchange.eth",
"cachedUrl": "http://tornado-polygon.crypto-bot.exchange/"
},
"torndao.eth": {
"url": "torndao.eth",
"name": "torndao.eth",
"cachedUrl": "http://polygon-tornado.zkany.com/"
}
}
},
"netId42161": {
"rpcUrls": {
"publicRpc1": {
"name": "Arbitrum Public RPC",
"url": "https://arb1.arbitrum.io/rpc"
},
"publicRpc2": {
"name": "ChainnodesRPC",
"url": "https://arbitrum-one.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607"
}
},
"relayers": {
"tornado-crypto-bot-exchange.eth": {
"url": "tornado-crypto-bot-exchange.eth",
"name": "tornado-crypto-bot-exchange.eth",
"cachedUrl": "http://tornado-arbitrum.crypto-bot.exchange/"
},
"torndao.eth": {
"url": "torndao.eth",
"name": "torndao.eth",
"cachedUrl": "http://arbitrum-tornado.zkany.com/"
}
}
},
"netId43114": {
"rpcUrls": {
"publicRpc": {
"name": "1RPC",
"url": "https://1rpc.io/avax/c"
}
},
"relayers": {
"cheap-relayer.eth": {
"url": "cheap-relayer.eth",
"name": "cheap-relayer.eth",
"cachedUrl": "http://avalanche-tornado.cheap-relayer.xyz/"
},
"lowcost.eth": {
"url": "lowcost.eth",
"name": "lowcost.eth",
"cachedUrl": "http://avalanche-tornado.low-fee.xyz/"
},
"thornadope.eth": {
"url": "thornadope.eth",
"name": "thornadope.eth",
"cachedUrl": "http://tornado-avalanche.thornadope.xyz/"
},
"tornado-crypto-bot-exchange.eth": {
"url": "tornado-crypto-bot-exchange.eth",
"name": "tornado-crypto-bot-exchange.eth",
"cachedUrl": "http://tornado-avalanche.crypto-bot.exchange/"
}
}
},
"netId10": {
"rpcUrls": {
"publicRpc1": {
"name": "1RPC",
"url": "https://1rpc.io/op"
},
"Chainnodes": {
"name": "Chainnodes",
"url": "https://optimism-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607"
}
},
"relayers": {
"tornado-crypto-bot-exchange.eth": {
"url": "tornado-crypto-bot-exchange.eth",
"name": "tornado-crypto-bot-exchange.eth",
"cachedUrl": "http://tornado-optimism.crypto-bot.exchange/"
}
}
},
"netId5": {
"rpcUrls": {
"Chainnodes": {
"name": "Chainnodes RPC",
"url": "https://goerli.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607"
}
},
"relayers": {}
}
}
```
Refer https://chainlist.org for a full list of available public RPC URLs.
Note that most of the RPC would censor sanctionded pool contracts.
So either you can use the default RPC or find yourself a suitable one.
For the list of avaiable relayers, use the `yarn relayers 1` command.

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -1,19 +1,31 @@
{
"name": "tornado-cli",
"version": "1.0.0",
"description": "",
"version": "1.0.1-alpha",
"description": "Modern Toolsets for Privacy Pools on Ethereum",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"bin": {
"tornado": "./dist/index.js"
"tornado-cli": "./dist/cli.js"
},
"scripts": {
"typechain": "typechain --target ethers-v6 --out-dir src/typechain src/abi/*.json",
"types": "tsc --declaration --emitDeclarationOnly",
"lint": "eslint src/**/*.ts --ext .ts --ignore-pattern src/typechain --fix",
"build": "rollup -c",
"start": "ts-node src/index.ts"
"lint": "eslint src/**/*.ts --ext .ts --ignore-pattern src/typechain",
"build": "yarn types && rollup -c",
"start": "ts-node src/cli.ts",
"startHelp": "ts-node src/cli.ts help",
"createDeposit": "ts-node src/cli.ts create",
"deposit": "ts-node src/cli.ts deposit",
"depositInvoice": "ts-node src/cli.ts depositInvoice",
"withdraw": "ts-node src/cli.ts withdraw",
"compliance": "ts-node src/cli.ts compliance",
"syncEvents": "ts-node src/cli.ts syncEvents",
"relayers": "ts-node src/cli.ts relayers",
"send": "ts-node src/cli.ts send",
"balance": "ts-node src/cli.ts balance",
"sign": "ts-node src/cli.ts sign",
"broadcast": "ts-node src/cli.ts broadcast"
},
"author": "",
"license": "MIT",
@ -26,6 +38,7 @@
".eslintrc.js",
".gitattributes",
".gitignore",
".npmrc",
"rollup.config.mjs",
"tsconfig.json",
"yarn.lock"

@ -2,19 +2,25 @@ import esbuild from 'rollup-plugin-esbuild';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import pkgJson from './package.json' assert { type: 'json' };
const external = Object.keys(pkgJson.dependencies).concat(...[
'@tornado/websnark/src/utils',
'@tornado/websnark/src/groth16',
]);
const config = [
{
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
file: pkgJson.main,
format: "cjs",
esModule: false,
banner: '#!/usr/bin/env node\n'
},
],
external: [],
external,
plugins: [
esbuild({
include: /\.[jt]sx?$/,
@ -26,6 +32,96 @@ const config = [
commonjs(),
json()
],
},
{
input: 'src/index.ts',
output: [
{
file: pkgJson.module,
format: "esm",
},
],
external,
plugins: [
esbuild({
include: /\.[jt]sx?$/,
minify: false,
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
json()
],
},
{
input: 'src/cli.ts',
output: [
{
file: pkgJson.bin[pkgJson.name],
format: "cjs",
esModule: false,
banner: '#!/usr/bin/env node\n'
},
],
plugins: [
esbuild({
include: /\.[jt]sx?$/,
minify: false,
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
commonjs(),
json()
],
},
{
input: 'src/merkleTreeWorker.ts',
output: [
{
file: 'static/merkleTreeWorker.js',
format: "cjs",
esModule: false,
},
],
treeshake: 'smallest',
plugins: [
esbuild({
include: /\.[jt]sx?$/,
minify: false,
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
commonjs(),
json()
],
},
{
input: 'src/merkleTreeWorker.ts',
output: [
{
file: 'static/merkleTreeWorker.umd.js',
format: "umd",
esModule: false
},
],
treeshake: 'smallest',
external: ['web-worker'],
plugins: [
esbuild({
include: /\.[jt]sx?$/,
minify: false,
sourceMap: true,
target: 'es2016',
}),
nodeResolve(),
commonjs(),
json(),
replace({
'process.browser': 'true'
})
],
}
]

12
scripts/figlet-font.ts Normal file

@ -0,0 +1,12 @@
import fs from 'fs'
let figletStandard = fs.readFileSync('./node_modules/figlet/importable-fonts/Standard.js', { encoding: 'utf8' }) as string;
figletStandard = figletStandard.replace('export default `', '')
figletStandard = figletStandard.replace(' `', '')
fs.writeFileSync('./src/fonts/figletStandard.ts', `
export const figletStandard: string = \`${figletStandard}\`;
export default figletStandard;
`)

15
src/cli.ts Normal file

@ -0,0 +1,15 @@
import figlet from 'figlet';
import Standard from './fonts/figletStandard';
import { tornadoProgram } from './program';
figlet.parseFont('Standard', Standard);
console.log(`
====================================================================
${figlet.textSync('Tornado CLI', { font: 'Standard' })}
====================================================================\n
`);
tornadoProgram().parse();

2238
src/fonts/figletStandard.ts Normal file

File diff suppressed because it is too large Load Diff

@ -1,3 +1,2 @@
import { ethers } from 'ethers';
export const provider = new ethers.JsonRpcProvider('http://localhost:8545');
export * from './services';
export * from './typechain';

69
src/merkleTreeWorker.ts Normal file

@ -0,0 +1,69 @@
/* eslint-disable @typescript-eslint/no-explicit-any, prettier/prettier */
import workerThreads from 'worker_threads';
import { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree';
import { mimc, isNode } from './services';
interface WorkData {
merkleTreeHeight: number;
edge?: TreeEdge;
elements: Element[];
zeroElement: string;
}
async function nodePostWork() {
const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = workerThreads.workerData as WorkData;
if (edge) {
const merkleTree = new PartialMerkleTree(merkleTreeHeight, edge, elements, {
zeroElement,
hashFunction,
});
(workerThreads.parentPort as workerThreads.MessagePort).postMessage(merkleTree.toString());
return;
}
const merkleTree = new MerkleTree(merkleTreeHeight, elements, {
zeroElement,
hashFunction,
});
(workerThreads.parentPort as workerThreads.MessagePort).postMessage(merkleTree.toString());
}
if (isNode && workerThreads) {
nodePostWork();
} else if (!isNode && typeof addEventListener === 'function' && typeof postMessage === 'function') {
addEventListener('message', async (e: any) => {
let data;
if (e.data) {
data = e.data;
} else {
data = e;
}
const { hash: hashFunction } = await mimc.getHash();
const { merkleTreeHeight, edge, elements, zeroElement } = data as WorkData;
if (edge) {
const merkleTree = new PartialMerkleTree(merkleTreeHeight, edge, elements, {
zeroElement,
hashFunction,
});
postMessage(merkleTree.toString());
return;
}
const merkleTree = new MerkleTree(merkleTreeHeight, elements, {
zeroElement,
hashFunction,
});
postMessage(merkleTree.toString());
});
} else {
throw new Error('This browser / environment doesn\'t support workers!');
}

1751
src/program.ts Normal file

File diff suppressed because it is too large Load Diff

339
src/services/batch.ts Normal file

@ -0,0 +1,339 @@
import type { Provider, BlockTag, Block, TransactionResponse, BaseContract, ContractEventName, EventLog } from 'ethers';
import { chunk, sleep } from './utils';
export interface BatchBlockServiceConstructor {
provider: Provider;
onProgress?: BatchBlockOnProgress;
concurrencySize?: number;
batchSize?: number;
shouldRetry?: boolean;
retryMax?: number;
retryOn?: number;
}
export type BatchBlockOnProgress = ({
percentage,
currentIndex,
totalIndex,
}: {
percentage: number;
currentIndex?: number;
totalIndex?: number;
}) => void;
/**
* Fetch blocks from web3 provider on batches
*/
export class BatchBlockService {
provider: Provider;
onProgress?: BatchBlockOnProgress;
concurrencySize: number;
batchSize: number;
shouldRetry: boolean;
retryMax: number;
retryOn: number;
constructor({
provider,
onProgress,
concurrencySize = 10,
batchSize = 10,
shouldRetry = true,
retryMax = 5,
retryOn = 500,
}: BatchBlockServiceConstructor) {
this.provider = provider;
this.onProgress = onProgress;
this.concurrencySize = concurrencySize;
this.batchSize = batchSize;
this.shouldRetry = shouldRetry;
this.retryMax = retryMax;
this.retryOn = retryOn;
}
async getBlock(blockTag: BlockTag): Promise<Block> {
const blockObject = await this.provider.getBlock(blockTag);
// if the provider returns null (which they have corrupted block data for one of their nodes) throw and retry
if (!blockObject) {
const errMsg = `No block for ${blockTag}`;
throw new Error(errMsg);
}
return blockObject;
}
createBatchRequest(batchArray: BlockTag[][]): Promise<Block[]>[] {
return batchArray.map(async (blocks: BlockTag[], index: number) => {
// send batch requests on milliseconds to avoid including them on a single batch request
await sleep(20 * index);
return (async () => {
let retries = 0;
let err;
// eslint-disable-next-line no-unmodified-loop-condition
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
try {
return await Promise.all(blocks.map((b) => this.getBlock(b)));
} catch (e) {
retries++;
err = e;
// retry on 0.5 seconds
await sleep(this.retryOn);
}
}
throw err;
})();
});
}
async getBatchBlocks(blocks: BlockTag[]): Promise<Block[]> {
let blockCount = 0;
const results: Block[] = [];
for (const chunks of chunk(blocks, this.concurrencySize * this.batchSize)) {
const chunksResult = (await Promise.all(this.createBatchRequest(chunk(chunks, this.batchSize)))).flat();
results.push(...chunksResult);
blockCount += chunks.length;
if (typeof this.onProgress === 'function') {
this.onProgress({
percentage: blockCount / blocks.length,
currentIndex: blockCount,
totalIndex: blocks.length,
});
}
}
return results;
}
}
/**
* Fetch transactions from web3 provider on batches
*/
export class BatchTransactionService {
provider: Provider;
onProgress?: BatchBlockOnProgress;
concurrencySize: number;
batchSize: number;
shouldRetry: boolean;
retryMax: number;
retryOn: number;
constructor({
provider,
onProgress,
concurrencySize = 10,
batchSize = 10,
shouldRetry = true,
retryMax = 5,
retryOn = 500,
}: BatchBlockServiceConstructor) {
this.provider = provider;
this.onProgress = onProgress;
this.concurrencySize = concurrencySize;
this.batchSize = batchSize;
this.shouldRetry = shouldRetry;
this.retryMax = retryMax;
this.retryOn = retryOn;
}
async getTransaction(txHash: string): Promise<TransactionResponse> {
const txObject = await this.provider.getTransaction(txHash);
if (!txObject) {
const errMsg = `No transaction for ${txHash}`;
throw new Error(errMsg);
}
return txObject;
}
createBatchRequest(batchArray: string[][]): Promise<TransactionResponse[]>[] {
return batchArray.map(async (txs: string[], index: number) => {
await sleep(20 * index);
return (async () => {
let retries = 0;
let err;
// eslint-disable-next-line no-unmodified-loop-condition
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
try {
return await Promise.all(txs.map((tx) => this.getTransaction(tx)));
} catch (e) {
retries++;
err = e;
// retry on 0.5 seconds
await sleep(this.retryOn);
}
}
throw err;
})();
});
}
async getBatchTransactions(txs: string[]): Promise<TransactionResponse[]> {
let txCount = 0;
const results = [];
for (const chunks of chunk(txs, this.concurrencySize * this.batchSize)) {
const chunksResult = (await Promise.all(this.createBatchRequest(chunk(chunks, this.batchSize)))).flat();
results.push(...chunksResult);
txCount += chunks.length;
if (typeof this.onProgress === 'function') {
this.onProgress({ percentage: txCount / txs.length, currentIndex: txCount, totalIndex: txs.length });
}
}
return results;
}
}
export interface BatchEventServiceConstructor {
provider: Provider;
contract: BaseContract;
onProgress?: BatchEventOnProgress;
concurrencySize?: number;
blocksPerRequest?: number;
shouldRetry?: boolean;
retryMax?: number;
retryOn?: number;
}
export type BatchEventOnProgress = ({
percentage,
type,
fromBlock,
toBlock,
count,
}: {
percentage: number;
type?: ContractEventName;
fromBlock?: number;
toBlock?: number;
count?: number;
}) => void;
// To enable iteration only numbers are accepted for fromBlock input
export type EventInput = {
fromBlock: number;
toBlock: number;
type: ContractEventName;
};
/**
* Fetch events from web3 provider on bulk
*/
export class BatchEventsService {
provider: Provider;
contract: BaseContract;
onProgress?: BatchEventOnProgress;
concurrencySize: number;
blocksPerRequest: number;
shouldRetry: boolean;
retryMax: number;
retryOn: number;
constructor({
provider,
contract,
onProgress,
concurrencySize = 10,
blocksPerRequest = 2000,
shouldRetry = true,
retryMax = 5,
retryOn = 500,
}: BatchEventServiceConstructor) {
this.provider = provider;
this.contract = contract;
this.onProgress = onProgress;
this.concurrencySize = concurrencySize;
this.blocksPerRequest = blocksPerRequest;
this.shouldRetry = shouldRetry;
this.retryMax = retryMax;
this.retryOn = retryOn;
}
async getPastEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]> {
let err;
let retries = 0;
// eslint-disable-next-line no-unmodified-loop-condition
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
try {
return (await this.contract.queryFilter(type, fromBlock, toBlock)) as EventLog[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
err = e;
retries++;
// If provider.getBlockNumber returned last block that isn't accepted (happened on Avalanche/Gnosis),
// get events to last accepted block
if (e.message.includes('after last accepted block')) {
const acceptedBlock = parseInt(e.message.split('after last accepted block ')[1]);
toBlock = acceptedBlock;
}
// retry on 0.5 seconds
await sleep(this.retryOn);
}
}
throw err;
}
createBatchRequest(batchArray: EventInput[]): Promise<EventLog[]>[] {
return batchArray.map(async (event: EventInput, index: number) => {
await sleep(20 * index);
return this.getPastEvents(event);
});
}
async getBatchEvents({ fromBlock, toBlock, type = '*' }: EventInput): Promise<EventLog[]> {
if (!toBlock) {
toBlock = await this.provider.getBlockNumber();
}
const eventsToSync = [];
for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) {
const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1;
eventsToSync.push({ fromBlock: i, toBlock: j, type });
}
const events = [];
const eventChunk = chunk(eventsToSync, this.concurrencySize);
let chunkCount = 0;
for (const chunk of eventChunk) {
chunkCount++;
const fetchedEvents = (await Promise.all(this.createBatchRequest(chunk))).flat();
events.push(...fetchedEvents);
if (typeof this.onProgress === 'function') {
this.onProgress({
percentage: chunkCount / eventChunk.length,
type,
fromBlock: chunk[0].fromBlock,
toBlock: chunk[chunk.length - 1].toBlock,
count: fetchedEvents.length,
});
}
}
return events;
}
}

147
src/services/data.ts Normal file

@ -0,0 +1,147 @@
import path from 'path';
import { stat, mkdir, readFile, writeFile } from 'fs/promises';
import { zip, unzip, AsyncZippable, Unzipped } from 'fflate';
import { BaseEvents, MinimalEvents } from './events';
export async function existsAsync(fileOrDir: string): Promise<boolean> {
try {
await stat(fileOrDir);
return true;
} catch {
return false;
}
}
export function zipAsync(file: AsyncZippable): Promise<Uint8Array> {
return new Promise((res, rej) => {
zip(file, { mtime: new Date('1/1/1980') }, (err, data) => {
if (err) {
rej(err);
return;
}
res(data);
});
});
}
export function unzipAsync(data: Uint8Array): Promise<Unzipped> {
return new Promise((res, rej) => {
unzip(data, {}, (err, data) => {
if (err) {
rej(err);
return;
}
res(data);
});
});
}
export async function saveEvents<T extends MinimalEvents>({
name,
userDirectory,
events,
}: {
name: string;
userDirectory: string;
events: T[];
}) {
const fileName = `${name}.json`.toLowerCase();
const filePath = path.join(userDirectory, fileName);
const stringEvents = JSON.stringify(events, null, 2) + '\n';
const payload = await zipAsync({
[fileName]: new TextEncoder().encode(stringEvents),
});
if (!(await existsAsync(userDirectory))) {
await mkdir(userDirectory, { recursive: true });
}
await writeFile(filePath + '.zip', payload);
await writeFile(filePath, stringEvents);
}
export async function loadSavedEvents<T extends MinimalEvents>({
name,
userDirectory,
deployedBlock,
}: {
name: string;
userDirectory: string;
deployedBlock: number;
}): Promise<BaseEvents<T>> {
const filePath = path.join(userDirectory, `${name}.json`.toLowerCase());
if (!(await existsAsync(filePath))) {
return {
events: [] as T[],
lastBlock: null,
};
}
try {
const events = JSON.parse(await readFile(filePath, { encoding: 'utf8' })) as T[];
return {
events,
lastBlock: events && events.length ? events[events.length - 1].blockNumber : deployedBlock,
};
} catch (err) {
console.log('Method loadSavedEvents has error');
console.log(err);
return {
events: [],
lastBlock: deployedBlock,
};
}
}
export async function download({ name, cacheDirectory }: { name: string; cacheDirectory: string }) {
const fileName = `${name}.json`.toLowerCase();
const zipName = `${fileName}.zip`;
const zipPath = path.join(cacheDirectory, zipName);
const data = await readFile(zipPath);
const { [fileName]: content } = await unzipAsync(data);
return new TextDecoder().decode(content);
}
export async function loadCachedEvents<T extends MinimalEvents>({
name,
cacheDirectory,
deployedBlock,
}: {
name: string;
cacheDirectory: string;
deployedBlock: number;
}): Promise<BaseEvents<T>> {
try {
const module = await download({ cacheDirectory, name });
if (module) {
const events = JSON.parse(module);
const lastBlock = events && events.length ? events[events.length - 1].blockNumber : deployedBlock;
return {
events,
lastBlock,
};
}
return {
events: [],
lastBlock: deployedBlock,
};
} catch (err) {
console.log('Method loadCachedEvents has error');
console.log(err);
return {
events: [],
lastBlock: deployedBlock,
};
}
}

246
src/services/deposits.ts Normal file

@ -0,0 +1,246 @@
import { bnToBytes, bytesToBN, leBuff2Int, leInt2Buff, rBigInt, toFixedHex } from './utils';
import { buffPedersenHash } from './pedersen';
export type DepositType = {
currency: string;
amount: string;
netId: string | number;
};
export type createDepositParams = {
nullifier: bigint;
secret: bigint;
};
export type createDepositObject = {
preimage: Uint8Array;
noteHex: string;
commitment: bigint;
commitmentHex: string;
nullifierHash: bigint;
nullifierHex: string;
};
export type createNoteParams = DepositType & {
nullifier?: bigint;
secret?: bigint;
};
export type parsedNoteExec = DepositType & {
note: string;
};
export type depositTx = {
from: string;
transactionHash: string;
};
export type withdrawalTx = {
to: string;
transactionHash: string;
};
export async function createDeposit({ nullifier, secret }: createDepositParams): Promise<createDepositObject> {
const preimage = new Uint8Array([...leInt2Buff(nullifier), ...leInt2Buff(secret)]);
const noteHex = toFixedHex(bytesToBN(preimage), 62);
const commitment = BigInt(await buffPedersenHash(preimage));
const commitmentHex = toFixedHex(commitment);
const nullifierHash = BigInt(await buffPedersenHash(leInt2Buff(nullifier)));
const nullifierHex = toFixedHex(nullifierHash);
return {
preimage,
noteHex,
commitment,
commitmentHex,
nullifierHash,
nullifierHex,
};
}
export interface DepositConstructor {
currency: string;
amount: string;
netId: number;
nullifier: bigint;
secret: bigint;
note: string;
noteHex: string;
invoice: string;
commitmentHex: string;
nullifierHex: string;
}
export class Deposit {
currency: string;
amount: string;
netId: number;
nullifier: bigint;
secret: bigint;
note: string;
noteHex: string;
invoice: string;
commitmentHex: string;
nullifierHex: string;
constructor({
currency,
amount,
netId,
nullifier,
secret,
note,
noteHex,
invoice,
commitmentHex,
nullifierHex,
}: DepositConstructor) {
this.currency = currency;
this.amount = amount;
this.netId = netId;
this.nullifier = nullifier;
this.secret = secret;
this.note = note;
this.noteHex = noteHex;
this.invoice = invoice;
this.commitmentHex = commitmentHex;
this.nullifierHex = nullifierHex;
}
toString() {
return JSON.stringify(
{
currency: this.currency,
amount: this.amount,
netId: this.netId,
nullifier: this.nullifier,
secret: this.secret,
note: this.note,
noteHex: this.noteHex,
invoice: this.invoice,
commitmentHex: this.commitmentHex,
nullifierHex: this.nullifierHex,
},
null,
2,
);
}
static async createNote({ currency, amount, netId, nullifier, secret }: createNoteParams): Promise<Deposit> {
if (!nullifier) {
nullifier = rBigInt(31);
}
if (!secret) {
secret = rBigInt(31);
}
const depositObject = await createDeposit({
nullifier,
secret,
});
const newDeposit = new Deposit({
currency: currency.toLowerCase(),
amount: amount,
netId: Number(netId),
note: `tornado-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.noteHex}`,
noteHex: depositObject.noteHex,
invoice: `tornadoInvoice-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.commitmentHex}`,
nullifier: nullifier,
secret: secret,
commitmentHex: depositObject.commitmentHex,
nullifierHex: depositObject.nullifierHex,
});
return newDeposit;
}
static async parseNote(noteString: string): Promise<Deposit> {
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g;
const match = noteRegex.exec(noteString);
if (!match) {
throw new Error('The note has invalid format');
}
const matchGroup = match?.groups as unknown as parsedNoteExec;
const currency = matchGroup.currency.toLowerCase();
const amount = matchGroup.amount;
const netId = Number(matchGroup.netId);
const bytes = bnToBytes('0x' + matchGroup.note);
const nullifier = BigInt(leBuff2Int(bytes.slice(0, 31)).toString());
const secret = BigInt(leBuff2Int(bytes.slice(31, 62)).toString());
const depositObject = await createDeposit({ nullifier, secret });
const invoice = `tornadoInvoice-${currency}-${amount}-${netId}-${depositObject.commitmentHex}`;
const newDeposit = new Deposit({
currency: currency,
amount: amount,
netId: netId,
note: noteString,
noteHex: depositObject.noteHex,
invoice: invoice,
nullifier: nullifier,
secret: secret,
commitmentHex: depositObject.commitmentHex,
nullifierHex: depositObject.nullifierHex,
});
return newDeposit;
}
}
export type parsedInvoiceExec = DepositType & {
commitment: string;
};
export class Invoice {
currency: string;
amount: string;
netId: number;
commitment: string;
invoice: string;
constructor(invoiceString: string) {
const invoiceRegex =
/tornadoInvoice-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<commitment>[0-9a-fA-F]{64})/g;
const match = invoiceRegex.exec(invoiceString);
if (!match) {
throw new Error('The note has invalid format');
}
const matchGroup = match?.groups as unknown as parsedInvoiceExec;
const currency = matchGroup.currency.toLowerCase();
const amount = matchGroup.amount;
const netId = Number(matchGroup.netId);
this.currency = currency;
this.amount = amount;
this.netId = netId;
this.commitment = '0x' + matchGroup.commitment;
this.invoice = invoiceString;
}
toString() {
return JSON.stringify(
{
currency: this.currency,
amount: this.amount,
netId: this.netId,
commitment: this.commitment,
invoice: this.invoice,
},
null,
2,
);
}
}

719
src/services/events/base.ts Normal file

@ -0,0 +1,719 @@
import { BaseContract, Provider, EventLog, TransactionResponse, getAddress, Block, ContractEventName } from 'ethers';
import type { Tornado, TornadoRouter, TornadoProxyLight, Governance, RelayerRegistry } from '@tornado/contracts';
import * as graph from '../graphql';
import {
BatchEventsService,
BatchBlockService,
BatchTransactionService,
BatchEventOnProgress,
BatchBlockOnProgress,
} from '../batch';
import { fetchDataOptions } from '../providers';
import type {
BaseEvents,
MinimalEvents,
DepositsEvents,
WithdrawalsEvents,
EncryptedNotesEvents,
GovernanceProposalCreatedEvents,
GovernanceVotedEvents,
GovernanceDelegatedEvents,
GovernanceUndelegatedEvents,
RegistersEvents,
BaseGraphEvents,
} from './types';
export const DEPOSIT = 'deposit';
export const WITHDRAWAL = 'withdrawal';
export type BaseEventsServiceConstructor = {
netId: number | string;
provider: Provider;
graphApi?: string;
subgraphName?: string;
contract: BaseContract;
type?: string;
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export type BatchGraphOnProgress = ({
type,
fromBlock,
toBlock,
count,
}: {
type?: ContractEventName;
fromBlock?: number;
toBlock?: number;
count?: number;
}) => void;
export type BaseGraphParams = {
graphApi: string;
subgraphName: string;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
};
export class BaseEventsService<EventType extends MinimalEvents> {
netId: number | string;
provider: Provider;
graphApi?: string;
subgraphName?: string;
contract: BaseContract;
type: string;
deployedBlock: number;
batchEventsService: BatchEventsService;
fetchDataOptions?: fetchDataOptions;
constructor({
netId,
provider,
graphApi,
subgraphName,
contract,
type = '',
deployedBlock = 0,
fetchDataOptions,
}: BaseEventsServiceConstructor) {
this.netId = netId;
this.provider = provider;
this.graphApi = graphApi;
this.subgraphName = subgraphName;
this.fetchDataOptions = fetchDataOptions;
this.contract = contract;
this.type = type;
this.deployedBlock = deployedBlock;
this.batchEventsService = new BatchEventsService({
provider,
contract,
onProgress: this.updateEventProgress,
});
}
getInstanceName(): string {
return '';
}
getType(): string {
return this.type || '';
}
getGraphMethod(): string {
return '';
}
getGraphParams(): BaseGraphParams {
return {
graphApi: this.graphApi || '',
subgraphName: this.subgraphName || '',
fetchDataOptions: this.fetchDataOptions,
onProgress: this.updateGraphProgress,
};
}
/* eslint-disable @typescript-eslint/no-unused-vars */
updateEventProgress({ percentage, type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {}
updateBlockProgress({ percentage, currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {}
updateTransactionProgress({ percentage, currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {}
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchGraphOnProgress>[0]) {}
/* eslint-enable @typescript-eslint/no-unused-vars */
async formatEvents(events: EventLog[]): Promise<EventType[]> {
// eslint-disable-next-line no-return-await
return await new Promise((resolve) => resolve(events as unknown as EventType[]));
}
/**
* Get saved or cached events
*/
async getEventsFromDB(): Promise<BaseEvents<EventType>> {
return {
events: [],
lastBlock: null,
};
}
async getEventsFromCache(): Promise<BaseEvents<EventType>> {
return {
events: [],
lastBlock: null,
};
}
async getSavedEvents(): Promise<BaseEvents<EventType>> {
let cachedEvents = await this.getEventsFromDB();
if (!cachedEvents || !cachedEvents.events.length) {
cachedEvents = await this.getEventsFromCache();
}
return cachedEvents;
}
/**
* Get latest events
*/
async getEventsFromGraph({
fromBlock,
methodName = '',
}: {
fromBlock: number;
methodName?: string;
}): Promise<BaseEvents<EventType>> {
if (!this.graphApi || !this.subgraphName) {
return {
events: [],
lastBlock: fromBlock,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { events, lastSyncBlock } = (await (graph as any)[methodName || this.getGraphMethod()]({
fromBlock,
...this.getGraphParams(),
})) as BaseGraphEvents<EventType>;
return {
events,
lastBlock: lastSyncBlock,
};
}
async getEventsFromRpc({
fromBlock,
toBlock,
}: {
fromBlock: number;
toBlock?: number;
}): Promise<BaseEvents<EventType>> {
try {
if (!toBlock) {
toBlock = await this.provider.getBlockNumber();
}
if (fromBlock >= toBlock) {
return {
events: [],
lastBlock: toBlock,
};
}
this.updateEventProgress({ percentage: 0, type: this.getType() });
const events = await this.formatEvents(
await this.batchEventsService.getBatchEvents({ fromBlock, toBlock, type: this.getType() }),
);
if (!events.length) {
return {
events,
lastBlock: toBlock,
};
}
return {
events,
lastBlock: toBlock,
};
} catch (err) {
console.log(err);
return {
events: [],
lastBlock: fromBlock,
};
}
}
async getLatestEvents({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<EventType>> {
const allEvents = [];
const graphEvents = await this.getEventsFromGraph({ fromBlock });
const lastSyncBlock =
graphEvents.lastBlock && graphEvents.lastBlock >= fromBlock ? graphEvents.lastBlock : fromBlock;
const rpcEvents = await this.getEventsFromRpc({ fromBlock: lastSyncBlock });
allEvents.push(...graphEvents.events);
allEvents.push(...rpcEvents.events);
const lastBlock = rpcEvents
? rpcEvents.lastBlock
: allEvents[allEvents.length - 1]
? allEvents[allEvents.length - 1].blockNumber
: fromBlock;
return {
events: allEvents,
lastBlock,
};
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validateEvents({ events, lastBlock }: BaseEvents<EventType>) {}
/**
* Handle saving events
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async saveEvents({ events, lastBlock }: BaseEvents<EventType>) {}
/**
* Trigger saving and receiving latest events
*/
async updateEvents() {
const savedEvents = await this.getSavedEvents();
let fromBlock = this.deployedBlock;
if (savedEvents && savedEvents.lastBlock) {
fromBlock = savedEvents.lastBlock + 1;
}
const newEvents = await this.getLatestEvents({ fromBlock });
const eventSet = new Set();
let allEvents: EventType[] = [];
allEvents.push(...savedEvents.events);
allEvents.push(...newEvents.events);
allEvents = allEvents
.sort((a, b) => {
if (a.blockNumber === b.blockNumber) {
return a.logIndex - b.logIndex;
}
return a.blockNumber - b.blockNumber;
})
.filter(({ transactionHash, logIndex }) => {
const eventKey = `${transactionHash}_${logIndex}`;
const hasEvent = eventSet.has(eventKey);
eventSet.add(eventKey);
return !hasEvent;
});
const lastBlock = newEvents
? newEvents.lastBlock
: allEvents[allEvents.length - 1]
? allEvents[allEvents.length - 1].blockNumber
: null;
this.validateEvents({ events: allEvents, lastBlock });
await this.saveEvents({ events: allEvents, lastBlock });
return {
events: allEvents,
lastBlock,
};
}
}
export type BaseDepositsServiceConstructor = {
netId: number | string;
provider: Provider;
graphApi?: string;
subgraphName?: string;
Tornado: Tornado;
type: string;
amount: string;
currency: string;
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export type DepositsGraphParams = BaseGraphParams & {
amount: string;
currency: string;
};
export class BaseDepositsService extends BaseEventsService<DepositsEvents | WithdrawalsEvents> {
amount: string;
currency: string;
batchTransactionService: BatchTransactionService;
batchBlockService: BatchBlockService;
constructor({
netId,
provider,
graphApi,
subgraphName,
Tornado,
type,
amount,
currency,
deployedBlock,
fetchDataOptions,
}: BaseDepositsServiceConstructor) {
super({ netId, provider, graphApi, subgraphName, contract: Tornado, type, deployedBlock, fetchDataOptions });
this.amount = amount;
this.currency = currency;
this.batchTransactionService = new BatchTransactionService({
provider,
onProgress: this.updateTransactionProgress,
});
this.batchBlockService = new BatchBlockService({
provider,
onProgress: this.updateBlockProgress,
});
}
getInstanceName(): string {
return `${this.getType().toLowerCase()}s_${this.netId}_${this.currency}_${this.amount}`;
}
getGraphMethod(): string {
return `getAll${this.getType()}s`;
}
getGraphParams(): DepositsGraphParams {
return {
graphApi: this.graphApi || '',
subgraphName: this.subgraphName || '',
amount: this.amount,
currency: this.currency,
fetchDataOptions: this.fetchDataOptions,
onProgress: this.updateGraphProgress,
};
}
async formatEvents(events: EventLog[]): Promise<(DepositsEvents | WithdrawalsEvents)[]> {
const type = this.getType().toLowerCase();
if (type === DEPOSIT) {
const formattedEvents = events.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
const { commitment, leafIndex, timestamp } = args;
return {
blockNumber,
logIndex,
transactionHash,
commitment: commitment as string,
leafIndex: Number(leafIndex),
timestamp: Number(timestamp),
};
});
const txs = await this.batchTransactionService.getBatchTransactions([
...new Set(formattedEvents.map(({ transactionHash }) => transactionHash)),
]);
return formattedEvents.map((event) => {
const { from } = txs.find(({ hash }) => hash === event.transactionHash) as TransactionResponse;
return {
...event,
from,
};
});
} else {
const formattedEvents = events.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
const { nullifierHash, to, fee } = args;
return {
blockNumber,
logIndex,
transactionHash,
nullifierHash: String(nullifierHash),
to: getAddress(to),
fee: String(fee),
};
});
const blocks = await this.batchBlockService.getBatchBlocks([
...new Set(formattedEvents.map(({ blockNumber }) => blockNumber)),
]);
return formattedEvents.map((event) => {
const { timestamp } = blocks.find(({ number }) => number === event.blockNumber) as Block;
return {
...event,
timestamp,
};
});
}
}
validateEvents({ events }: { events: (DepositsEvents | WithdrawalsEvents)[] }) {
if (events.length && this.getType().toLowerCase() === DEPOSIT) {
const lastEvent = events[events.length - 1] as DepositsEvents;
if (lastEvent.leafIndex !== events.length - 1) {
const errMsg = `Deposit events invalid wants ${events.length - 1} leafIndex have ${lastEvent.leafIndex}`;
throw new Error(errMsg);
}
}
}
}
export type BaseEncryptedNotesServiceConstructor = {
netId: number | string;
provider: Provider;
graphApi?: string;
subgraphName?: string;
Router: TornadoRouter | TornadoProxyLight;
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export class BaseEncryptedNotesService extends BaseEventsService<EncryptedNotesEvents> {
constructor({
netId,
provider,
graphApi,
subgraphName,
Router,
deployedBlock,
fetchDataOptions,
}: BaseEncryptedNotesServiceConstructor) {
super({ netId, provider, graphApi, subgraphName, contract: Router, deployedBlock, fetchDataOptions });
}
getInstanceName(): string {
return `encrypted_notes_${this.netId}`;
}
getType(): string {
return 'EncryptedNote';
}
getGraphMethod(): string {
return 'getAllEncryptedNotes';
}
async formatEvents(events: EventLog[]) {
return events
.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
const { encryptedNote } = args;
if (encryptedNote) {
const eventObjects = {
blockNumber,
logIndex,
transactionHash,
};
return {
...eventObjects,
encryptedNote,
};
}
})
.filter((e) => e) as EncryptedNotesEvents[];
}
}
export type BaseGovernanceEventTypes =
| GovernanceProposalCreatedEvents
| GovernanceVotedEvents
| GovernanceDelegatedEvents
| GovernanceUndelegatedEvents;
export type BaseGovernanceServiceConstructor = {
netId: number | string;
provider: Provider;
graphApi?: string;
subgraphName?: string;
Governance: Governance;
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export class BaseGovernanceService extends BaseEventsService<BaseGovernanceEventTypes> {
batchTransactionService: BatchTransactionService;
constructor({
netId,
provider,
graphApi,
subgraphName,
Governance,
deployedBlock,
fetchDataOptions,
}: BaseGovernanceServiceConstructor) {
super({ netId, provider, graphApi, subgraphName, contract: Governance, deployedBlock, fetchDataOptions });
this.batchTransactionService = new BatchTransactionService({
provider,
onProgress: this.updateTransactionProgress,
});
}
getInstanceName() {
return `governance_${this.netId}`;
}
getType() {
return '*';
}
getGraphMethod() {
return 'governanceEvents';
}
async formatEvents(events: EventLog[]): Promise<BaseGovernanceEventTypes[]> {
const formattedEvents = events
.map(({ blockNumber, index: logIndex, transactionHash, args, eventName: event }) => {
const eventObjects = {
blockNumber,
logIndex,
transactionHash,
event,
};
if (event === 'ProposalCreated') {
const { id, proposer, target, startTime, endTime, description } = args;
return {
...eventObjects,
id,
proposer,
target,
startTime,
endTime,
description,
} as GovernanceProposalCreatedEvents;
}
if (event === 'Voted') {
const { proposalId, voter, support, votes } = args;
return {
...eventObjects,
proposalId,
voter,
support,
votes,
} as GovernanceVotedEvents;
}
if (event === 'Delegated') {
const { account, to: delegateTo } = args;
return {
...eventObjects,
account,
delegateTo,
} as GovernanceDelegatedEvents;
}
if (event === 'Undelegated') {
const { account, from: delegateFrom } = args;
return {
...eventObjects,
account,
delegateFrom,
} as GovernanceUndelegatedEvents;
}
})
.filter((e) => e) as BaseGovernanceEventTypes[];
type GovernanceVotedEventsIndexed = GovernanceVotedEvents & {
index: number;
};
const votedEvents = formattedEvents
.map((event, index) => ({ ...event, index }))
.filter(({ event }) => event === 'Voted') as GovernanceVotedEventsIndexed[];
if (votedEvents.length) {
this.updateTransactionProgress({ percentage: 0 });
const txs = await this.batchTransactionService.getBatchTransactions([
...new Set(votedEvents.map(({ transactionHash }) => transactionHash)),
]);
votedEvents.forEach((event) => {
// eslint-disable-next-line prefer-const
let { data: input, from } = txs.find((t) => t.hash === event.transactionHash) as TransactionResponse;
// Filter spammy txs
if (!input || input.length > 2048) {
input = '';
}
// @ts-expect-error check formattedEvents types later
formattedEvents[event.index].from = from;
// @ts-expect-error check formattedEvents types later
formattedEvents[event.index].input = input;
});
}
return formattedEvents;
}
async getEventsFromGraph({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<BaseGovernanceEventTypes>> {
// TheGraph doesn't support governance subgraphs
if (!this.graphApi || this.graphApi.includes('api.thegraph.com')) {
return {
events: [],
lastBlock: fromBlock,
};
}
return super.getEventsFromGraph({ fromBlock });
}
}
export type BaseRegistryServiceConstructor = {
netId: number | string;
provider: Provider;
graphApi?: string;
subgraphName?: string;
RelayerRegistry: RelayerRegistry;
deployedBlock?: number;
fetchDataOptions?: fetchDataOptions;
};
export class BaseRegistryService extends BaseEventsService<RegistersEvents> {
constructor({
netId,
provider,
graphApi,
subgraphName,
RelayerRegistry,
deployedBlock,
fetchDataOptions,
}: BaseRegistryServiceConstructor) {
super({ netId, provider, graphApi, subgraphName, contract: RelayerRegistry, deployedBlock, fetchDataOptions });
}
getInstanceName() {
return `registered_${this.netId}`;
}
// Name of type used for events
getType() {
return 'RelayerRegistered';
}
// Name of method used for graph
getGraphMethod() {
return 'getAllRegisters';
}
async formatEvents(events: EventLog[]) {
return events.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
const eventObjects = {
blockNumber,
logIndex,
transactionHash,
};
return {
...eventObjects,
ensName: args.ensName,
relayerAddress: args.relayerAddress,
};
});
}
async fetchRelayers(): Promise<RegistersEvents[]> {
return (await this.updateEvents()).events;
}
}

@ -0,0 +1,3 @@
export * from './types';
export * from './base';
export * from './node';

626
src/services/events/node.ts Normal file

@ -0,0 +1,626 @@
import Table from 'cli-table3';
import moment from 'moment';
import { BatchBlockOnProgress, BatchEventOnProgress } from '../batch';
import { saveEvents, loadSavedEvents, loadCachedEvents } from '../data';
import {
BaseDepositsService,
BaseEncryptedNotesService,
BaseGovernanceService,
BaseRegistryService,
BaseDepositsServiceConstructor,
BaseEncryptedNotesServiceConstructor,
BaseGovernanceServiceConstructor,
BaseRegistryServiceConstructor,
BaseGovernanceEventTypes,
} from './base';
import type { BaseEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, RegistersEvents } from './types';
export type NodeDepositsServiceConstructor = BaseDepositsServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
};
export class NodeDepositsService extends BaseDepositsService {
cacheDirectory?: string;
userDirectory?: string;
constructor({
netId,
provider,
graphApi,
subgraphName,
Tornado,
type,
amount,
currency,
deployedBlock,
fetchDataOptions,
cacheDirectory,
userDirectory,
}: NodeDepositsServiceConstructor) {
super({
netId,
provider,
graphApi,
subgraphName,
Tornado,
type,
amount,
currency,
deployedBlock,
fetchDataOptions,
});
this.cacheDirectory = cacheDirectory;
this.userDirectory = userDirectory;
}
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
}
}
}
updateTransactionProgress({ currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {
if (totalIndex) {
console.log(`Fetched ${currentIndex} deposit txs of ${totalIndex}`);
}
}
updateBlockProgress({ currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {
if (totalIndex) {
console.log(`Fetched ${currentIndex} withdrawal blocks of ${totalIndex}`);
}
}
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events from graph node count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
}
}
}
async getEventsFromDB() {
if (!this.userDirectory) {
console.log(
'Updating events for',
this.amount,
this.currency.toUpperCase(),
`${this.getType().toLowerCase()}s\n`,
);
console.log(`savedEvents count - ${0}`);
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const savedEvents = await loadSavedEvents<DepositsEvents | WithdrawalsEvents>({
name: this.getInstanceName(),
userDirectory: this.userDirectory,
deployedBlock: this.deployedBlock,
});
console.log('Updating events for', this.amount, this.currency.toUpperCase(), `${this.getType().toLowerCase()}s\n`);
console.log(`savedEvents count - ${savedEvents.events.length}`);
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
return savedEvents;
}
async getEventsFromCache() {
if (!this.cacheDirectory) {
console.log(`cachedEvents count - ${0}`);
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const cachedEvents = await loadCachedEvents<DepositsEvents | WithdrawalsEvents>({
name: this.getInstanceName(),
cacheDirectory: this.cacheDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
return cachedEvents;
}
async saveEvents({ events, lastBlock }: BaseEvents<DepositsEvents | WithdrawalsEvents>) {
const instanceName = this.getInstanceName();
console.log('\ntotalEvents count - ', events.length);
console.log(
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
);
const eventTable = new Table();
eventTable.push(
[{ colSpan: 2, content: `${this.getType()}s`, hAlign: 'center' }],
['Instance', `${this.netId} chain ${this.amount} ${this.currency.toUpperCase()}`],
['Anonymity set', `${events.length} equal user ${this.getType().toLowerCase()}s`],
[{ colSpan: 2, content: `Latest ${this.getType().toLowerCase()}s` }],
...events
.slice(events.length - 10)
.reverse()
.map(({ timestamp }, index) => {
const eventIndex = events.length - index;
const eventTime = moment.unix(timestamp).fromNow();
return [eventIndex, eventTime];
}),
);
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<DepositsEvents | WithdrawalsEvents>({
name: instanceName,
userDirectory: this.userDirectory,
events,
});
}
}
}
export type NodeEncryptedNotesServiceConstructor = BaseEncryptedNotesServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
};
export class NodeEncryptedNotesService extends BaseEncryptedNotesService {
cacheDirectory?: string;
userDirectory?: string;
constructor({
netId,
provider,
graphApi,
subgraphName,
Router,
deployedBlock,
fetchDataOptions,
cacheDirectory,
userDirectory,
}: NodeEncryptedNotesServiceConstructor) {
super({
netId,
provider,
graphApi,
subgraphName,
Router,
deployedBlock,
fetchDataOptions,
});
this.cacheDirectory = cacheDirectory;
this.userDirectory = userDirectory;
}
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
}
}
}
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events from graph node count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
}
}
}
async getEventsFromDB() {
if (!this.userDirectory) {
console.log(`Updating events for ${this.netId} chain encrypted events\n`);
console.log(`savedEvents count - ${0}`);
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const savedEvents = await loadSavedEvents<EncryptedNotesEvents>({
name: this.getInstanceName(),
userDirectory: this.userDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`Updating events for ${this.netId} chain encrypted events\n`);
console.log(`savedEvents count - ${savedEvents.events.length}`);
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
return savedEvents;
}
async getEventsFromCache() {
if (!this.cacheDirectory) {
console.log(`cachedEvents count - ${0}`);
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const cachedEvents = await loadCachedEvents<EncryptedNotesEvents>({
name: this.getInstanceName(),
cacheDirectory: this.cacheDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
return cachedEvents;
}
async saveEvents({ events, lastBlock }: BaseEvents<EncryptedNotesEvents>) {
const instanceName = this.getInstanceName();
console.log('\ntotalEvents count - ', events.length);
console.log(
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
);
const eventTable = new Table();
eventTable.push(
[{ colSpan: 2, content: 'Encrypted Notes', hAlign: 'center' }],
['Network', `${this.netId} chain`],
['Events', `${events.length} events`],
[{ colSpan: 2, content: 'Latest events' }],
...events
.slice(events.length - 10)
.reverse()
.map(({ blockNumber }, index) => {
const eventIndex = events.length - index;
return [eventIndex, blockNumber];
}),
);
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<EncryptedNotesEvents>({
name: instanceName,
userDirectory: this.userDirectory,
events,
});
}
}
}
export type NodeGovernanceServiceConstructor = BaseGovernanceServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
};
export class NodeGovernanceService extends BaseGovernanceService {
cacheDirectory?: string;
userDirectory?: string;
constructor({
netId,
provider,
graphApi,
subgraphName,
Governance,
deployedBlock,
fetchDataOptions,
cacheDirectory,
userDirectory,
}: NodeGovernanceServiceConstructor) {
super({
netId,
provider,
graphApi,
subgraphName,
Governance,
deployedBlock,
fetchDataOptions,
});
this.cacheDirectory = cacheDirectory;
this.userDirectory = userDirectory;
}
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
}
}
}
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events from graph node count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
}
}
}
updateTransactionProgress({ currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {
if (totalIndex) {
console.log(`Fetched ${currentIndex} governance txs of ${totalIndex}`);
}
}
async getEventsFromDB() {
if (!this.userDirectory) {
console.log(`Updating events for ${this.netId} chain governance events\n`);
console.log(`savedEvents count - ${0}`);
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const savedEvents = await loadSavedEvents<BaseGovernanceEventTypes>({
name: this.getInstanceName(),
userDirectory: this.userDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`Updating events for ${this.netId} chain governance events\n`);
console.log(`savedEvents count - ${savedEvents.events.length}`);
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
return savedEvents;
}
async getEventsFromCache() {
if (!this.cacheDirectory) {
console.log(`cachedEvents count - ${0}`);
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const cachedEvents = await loadCachedEvents<BaseGovernanceEventTypes>({
name: this.getInstanceName(),
cacheDirectory: this.cacheDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
return cachedEvents;
}
async saveEvents({ events, lastBlock }: BaseEvents<BaseGovernanceEventTypes>) {
const instanceName = this.getInstanceName();
console.log('\ntotalEvents count - ', events.length);
console.log(
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
);
const eventTable = new Table();
eventTable.push(
[{ colSpan: 2, content: 'Governance Events', hAlign: 'center' }],
['Network', `${this.netId} chain`],
['Events', `${events.length} events`],
[{ colSpan: 2, content: 'Latest events' }],
...events
.slice(events.length - 10)
.reverse()
.map(({ blockNumber }, index) => {
const eventIndex = events.length - index;
return [eventIndex, blockNumber];
}),
);
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<BaseGovernanceEventTypes>({
name: instanceName,
userDirectory: this.userDirectory,
events,
});
}
}
}
export type NodeRegistryServiceConstructor = BaseRegistryServiceConstructor & {
cacheDirectory?: string;
userDirectory?: string;
};
export class NodeRegistryService extends BaseRegistryService {
cacheDirectory?: string;
userDirectory?: string;
constructor({
netId,
provider,
graphApi,
subgraphName,
RelayerRegistry,
deployedBlock,
fetchDataOptions,
cacheDirectory,
userDirectory,
}: NodeRegistryServiceConstructor) {
super({
netId,
provider,
graphApi,
subgraphName,
RelayerRegistry,
deployedBlock,
fetchDataOptions,
});
this.cacheDirectory = cacheDirectory;
this.userDirectory = userDirectory;
}
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
}
}
}
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
if (toBlock) {
console.log(`fromBlock - ${fromBlock}`);
console.log(`toBlock - ${toBlock}`);
if (count) {
console.log(`downloaded ${type} events from graph node count - ${count}`);
console.log('____________________________________________');
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
}
}
}
async getEventsFromDB() {
if (!this.userDirectory) {
console.log(`Updating events for ${this.netId} chain registry events\n`);
console.log(`savedEvents count - ${0}`);
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const savedEvents = await loadSavedEvents<RegistersEvents>({
name: this.getInstanceName(),
userDirectory: this.userDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`Updating events for ${this.netId} chain registry events\n`);
console.log(`savedEvents count - ${savedEvents.events.length}`);
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
return savedEvents;
}
async getEventsFromCache() {
if (!this.cacheDirectory) {
console.log(`cachedEvents count - ${0}`);
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
return {
events: [],
lastBlock: this.deployedBlock,
};
}
const cachedEvents = await loadCachedEvents<RegistersEvents>({
name: this.getInstanceName(),
cacheDirectory: this.cacheDirectory,
deployedBlock: this.deployedBlock,
});
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
return cachedEvents;
}
async saveEvents({ events, lastBlock }: BaseEvents<RegistersEvents>) {
const instanceName = this.getInstanceName();
console.log('\ntotalEvents count - ', events.length);
console.log(
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
);
const eventTable = new Table();
eventTable.push(
[{ colSpan: 2, content: 'Registered Relayers', hAlign: 'center' }],
['Network', `${this.netId} chain`],
['Events', `${events.length} events`],
[{ colSpan: 2, content: 'Latest events' }],
...events
.slice(events.length - 10)
.reverse()
.map(({ blockNumber }, index) => {
const eventIndex = events.length - index;
return [eventIndex, blockNumber];
}),
);
console.log(eventTable.toString() + '\n');
if (this.userDirectory) {
await saveEvents<RegistersEvents>({
name: instanceName,
userDirectory: this.userDirectory,
events,
});
}
}
}

@ -0,0 +1,69 @@
import { RelayerParams } from '../relayerClient';
export interface BaseEvents<T> {
events: T[];
lastBlock: number | null;
}
export interface BaseGraphEvents<T> {
events: T[];
lastSyncBlock: number;
}
export interface MinimalEvents {
blockNumber: number;
logIndex: number;
transactionHash: string;
}
export type GovernanceEvents = MinimalEvents & {
event: string;
};
export type GovernanceProposalCreatedEvents = GovernanceEvents & {
id: number;
proposer: string;
target: string;
startTime: number;
endTime: number;
description: string;
};
export type GovernanceVotedEvents = GovernanceEvents & {
proposalId: number;
voter: string;
support: boolean;
votes: string;
from: string;
input: string;
};
export type GovernanceDelegatedEvents = GovernanceEvents & {
account: string;
delegateTo: string;
};
export type GovernanceUndelegatedEvents = GovernanceEvents & {
account: string;
delegateFrom: string;
};
export type RegistersEvents = MinimalEvents & RelayerParams;
export type DepositsEvents = MinimalEvents & {
commitment: string;
leafIndex: number;
timestamp: number;
from: string;
};
export type WithdrawalsEvents = MinimalEvents & {
nullifierHash: string;
to: string;
fee: string;
timestamp: number;
};
export type EncryptedNotesEvents = MinimalEvents & {
encryptedNote: string;
};

124
src/services/fees.ts Normal file

@ -0,0 +1,124 @@
import { Transaction, parseUnits } from 'ethers';
import type { BigNumberish, TransactionLike } from 'ethers';
import { OvmGasPriceOracle } from '../typechain';
const DUMMY_ADDRESS = '0x1111111111111111111111111111111111111111';
const DUMMY_NONCE = '0x1111111111111111111111111111111111111111111111111111111111111111';
const DUMMY_WITHDRAW_DATA =
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111';
/**
* Example:
*
* amountInWei (0.1 ETH) * tokenDecimals (18) * tokenPriceInWei (0.0008) = 125 TOKEN
*/
export function convertETHToTokenAmount(
amountInWei: BigNumberish,
tokenPriceInWei: BigNumberish,
tokenDecimals: number = 18,
): bigint {
const tokenDecimalsMultiplier = BigInt(10 ** Number(tokenDecimals));
return (BigInt(amountInWei) * tokenDecimalsMultiplier) / BigInt(tokenPriceInWei);
}
export interface RelayerFeeParams {
gasPrice: BigNumberish;
gasLimit?: BigNumberish;
l1Fee?: BigNumberish;
denomination: BigNumberish;
ethRefund: BigNumberish;
tokenPriceInWei: BigNumberish;
tokenDecimals: number;
relayerFeePercent?: number;
isEth?: boolean;
premiumPercent?: number;
}
export class TornadoFeeOracle {
ovmGasPriceOracle?: OvmGasPriceOracle;
constructor(ovmGasPriceOracle?: OvmGasPriceOracle) {
if (ovmGasPriceOracle) {
this.ovmGasPriceOracle = ovmGasPriceOracle;
}
}
/**
* Calculate L1 fee for op-stack chains
*
* This is required since relayers would pay the full transaction fees for users
*/
fetchL1OptimismFee(tx?: TransactionLike): Promise<bigint> {
if (!this.ovmGasPriceOracle) {
return new Promise((resolve) => resolve(BigInt(0)));
}
if (!tx) {
// this tx is only used to simulate bytes size of the encoded tx so has nothing to with the accuracy
// inspired by the old style classic-ui calculation
tx = {
type: 0,
gasLimit: 1_000_000,
nonce: Number(DUMMY_NONCE),
data: DUMMY_WITHDRAW_DATA,
gasPrice: parseUnits('1', 'gwei'),
from: DUMMY_ADDRESS,
to: DUMMY_ADDRESS,
};
}
return this.ovmGasPriceOracle.getL1Fee.staticCall(Transaction.from(tx).unsignedSerialized);
}
/**
* We don't need to distinguish default refunds by tokens since most users interact with other defi protocols after withdrawal
* So we default with 1M gas which is enough for two or three swaps
* Using 30 gwei for default but it is recommended to supply cached gasPrice value from the UI
*/
defaultEthRefund(gasPrice?: BigNumberish, gasLimit?: BigNumberish): bigint {
return (gasPrice ? BigInt(gasPrice) : parseUnits('30', 'gwei')) * BigInt(gasLimit || 1_000_000);
}
/**
* Calculates token amount for required ethRefund purchases required to calculate fees
*/
calculateTokenAmount(ethRefund: BigNumberish, tokenPriceInEth: BigNumberish, tokenDecimals?: number): bigint {
return convertETHToTokenAmount(ethRefund, tokenPriceInEth, tokenDecimals);
}
/**
* Warning: For tokens you need to check if the fees are above denomination
* (Usually happens for small denomination pool or if the gas price is high)
*/
calculateRelayerFee({
gasPrice,
gasLimit = 600_000,
l1Fee = 0,
denomination,
ethRefund = BigInt(0),
tokenPriceInWei,
tokenDecimals = 18,
relayerFeePercent = 0.33,
isEth = true,
premiumPercent = 20,
}: RelayerFeeParams): bigint {
const gasCosts = BigInt(gasPrice) * BigInt(gasLimit) + BigInt(l1Fee);
const relayerFee = (BigInt(denomination) * BigInt(Math.floor(10000 * relayerFeePercent))) / BigInt(10000 * 100);
if (isEth) {
// Add 20% premium
return ((gasCosts + relayerFee) * BigInt(premiumPercent ? 100 + premiumPercent : 100)) / BigInt(100);
}
const feeInEth = gasCosts + BigInt(ethRefund);
return (
((convertETHToTokenAmount(feeInEth, tokenPriceInWei, tokenDecimals) + relayerFee) *
BigInt(premiumPercent ? 100 + premiumPercent : 100)) /
BigInt(100)
);
}
}

@ -0,0 +1,784 @@
import { getAddress } from 'ethers';
import { fetchData, fetchDataOptions } from '../providers';
import type {
BaseGraphEvents,
RegistersEvents,
DepositsEvents,
WithdrawalsEvents,
EncryptedNotesEvents,
BatchGraphOnProgress,
} from '../events';
import {
_META,
GET_DEPOSITS,
GET_STATISTIC,
GET_REGISTERED,
GET_WITHDRAWALS,
GET_NOTE_ACCOUNTS,
GET_ENCRYPTED_NOTES,
} from './queries';
export * from './queries';
const isEmptyArray = (arr: object) => !Array.isArray(arr) || !arr.length;
const first = 1000;
export type queryGraphParams = {
graphApi: string;
subgraphName: string;
query: string;
variables?: {
[key: string]: string | number;
};
fetchDataOptions?: fetchDataOptions;
};
export async function queryGraph<T>({
graphApi,
subgraphName,
query,
variables,
fetchDataOptions,
}: queryGraphParams): Promise<T> {
const graphUrl = `${graphApi}/subgraphs/name/${subgraphName}`;
const { data, errors } = await fetchData(graphUrl, {
...fetchDataOptions,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
});
if (errors) {
throw new Error(JSON.stringify(errors));
}
if (data?._meta?.hasIndexingErrors) {
throw new Error('Subgraph has indexing errors');
}
return data;
}
export interface GraphStatistic {
deposits: {
index: string;
timestamp: string;
blockNumber: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getStatisticParams {
graphApi: string;
subgraphName: string;
currency: string;
amount: string;
fetchDataOptions?: fetchDataOptions;
}
export interface getStatisticReturns {
events: {
timestamp: number;
leafIndex: number;
blockNumber: number;
}[];
lastSyncBlock: null | number;
}
export async function getStatistic({
graphApi,
subgraphName,
currency,
amount,
fetchDataOptions,
}: getStatisticParams): Promise<getStatisticReturns> {
try {
const {
deposits,
_meta: {
block: { number: lastSyncBlock },
},
} = await queryGraph<GraphStatistic>({
graphApi,
subgraphName,
query: GET_STATISTIC,
variables: {
currency,
first: 10,
orderBy: 'index',
orderDirection: 'desc',
amount,
},
fetchDataOptions,
});
const events = deposits
.map((e) => ({
timestamp: Number(e.timestamp),
leafIndex: Number(e.index),
blockNumber: Number(e.blockNumber),
}))
.reverse();
const [lastEvent] = events.slice(-1);
return {
events,
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
};
} catch (err) {
console.log('Error from getStatistic query');
console.log(err);
return {
events: [],
lastSyncBlock: null,
};
}
}
export interface GraphMeta {
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getMetaParams {
graphApi: string;
subgraphName: string;
fetchDataOptions?: fetchDataOptions;
}
export interface getMetaReturns {
lastSyncBlock: null | number;
hasIndexingErrors: null | boolean;
}
export async function getMeta({ graphApi, subgraphName, fetchDataOptions }: getMetaParams): Promise<getMetaReturns> {
try {
const {
_meta: {
block: { number: lastSyncBlock },
hasIndexingErrors,
},
} = await queryGraph<GraphMeta>({
graphApi,
subgraphName,
query: _META,
fetchDataOptions,
});
return {
lastSyncBlock,
hasIndexingErrors,
};
} catch (err) {
console.log('Error from getMeta query');
console.log(err);
return {
lastSyncBlock: null,
hasIndexingErrors: null,
};
}
}
export interface GraphRegisters {
relayers: {
id: string;
address: string;
ensName: string;
blockRegistration: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getRegistersParams {
graphApi: string;
subgraphName: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export function getRegisters({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
}: getRegistersParams): Promise<GraphRegisters> {
return queryGraph<GraphRegisters>({
graphApi,
subgraphName,
query: GET_REGISTERED,
variables: {
first,
fromBlock,
},
fetchDataOptions,
});
}
export async function getAllRegisters({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
onProgress,
}: getRegistersParams): Promise<BaseGraphEvents<RegistersEvents>> {
try {
const events = [];
let lastSyncBlock = fromBlock;
// eslint-disable-next-line no-constant-condition
while (true) {
let {
relayers: result,
_meta: {
// eslint-disable-next-line prefer-const
block: { number: currentBlock },
},
} = await getRegisters({ graphApi, subgraphName, fromBlock, fetchDataOptions });
lastSyncBlock = currentBlock;
if (isEmptyArray(result)) {
break;
}
const [firstEvent] = result;
const [lastEvent] = result.slice(-1);
if (typeof onProgress === 'function') {
onProgress({
type: 'Registers',
fromBlock: Number(firstEvent.blockRegistration),
toBlock: Number(lastEvent.blockRegistration),
count: result.length,
});
}
if (result.length < 900) {
events.push(...result);
break;
}
result = result.filter(({ blockRegistration }) => blockRegistration !== lastEvent.blockRegistration);
fromBlock = Number(lastEvent.blockRegistration);
events.push(...result);
}
if (!events.length) {
return {
events: [],
lastSyncBlock,
};
}
const result = events.map(({ id, address, ensName, blockRegistration }) => {
const [transactionHash, logIndex] = id.split('-');
return {
blockNumber: Number(blockRegistration),
logIndex: Number(logIndex),
transactionHash,
ensName,
relayerAddress: getAddress(address),
};
});
return {
events: result,
lastSyncBlock,
};
} catch (err) {
console.log('Error from getAllRegisters query');
console.log(err);
return { events: [], lastSyncBlock: fromBlock };
}
}
export interface GraphDeposits {
deposits: {
id: string;
blockNumber: string;
commitment: string;
index: string;
timestamp: string;
from: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getDepositsParams {
graphApi: string;
subgraphName: string;
currency: string;
amount: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export function getDeposits({
graphApi,
subgraphName,
currency,
amount,
fromBlock,
fetchDataOptions,
}: getDepositsParams): Promise<GraphDeposits> {
return queryGraph<GraphDeposits>({
graphApi,
subgraphName,
query: GET_DEPOSITS,
variables: {
currency,
amount,
first,
fromBlock,
},
fetchDataOptions,
});
}
export async function getAllDeposits({
graphApi,
subgraphName,
currency,
amount,
fromBlock,
fetchDataOptions,
onProgress,
}: getDepositsParams): Promise<BaseGraphEvents<DepositsEvents>> {
try {
const events = [];
let lastSyncBlock = fromBlock;
// eslint-disable-next-line no-constant-condition
while (true) {
let {
deposits: result,
_meta: {
// eslint-disable-next-line prefer-const
block: { number: currentBlock },
},
} = await getDeposits({ graphApi, subgraphName, currency, amount, fromBlock, fetchDataOptions });
lastSyncBlock = currentBlock;
if (isEmptyArray(result)) {
break;
}
const [firstEvent] = result;
const [lastEvent] = result.slice(-1);
if (typeof onProgress === 'function') {
onProgress({
type: 'Deposits',
fromBlock: Number(firstEvent.blockNumber),
toBlock: Number(lastEvent.blockNumber),
count: result.length,
});
}
if (result.length < 900) {
events.push(...result);
break;
}
result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
fromBlock = Number(lastEvent.blockNumber);
events.push(...result);
}
if (!events.length) {
return {
events: [],
lastSyncBlock,
};
}
const result = events.map(({ id, blockNumber, commitment, index, timestamp, from }) => {
const [transactionHash, logIndex] = id.split('-');
return {
blockNumber: Number(blockNumber),
logIndex: Number(logIndex),
transactionHash,
commitment,
leafIndex: Number(index),
timestamp: Number(timestamp),
from: getAddress(from),
};
});
const [lastEvent] = result.slice(-1);
return {
events: result,
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
};
} catch (err) {
console.log('Error from getAllDeposits query');
console.log(err);
return {
events: [],
lastSyncBlock: fromBlock,
};
}
}
export interface GraphWithdrawals {
withdrawals: {
id: string;
blockNumber: string;
nullifier: string;
to: string;
fee: string;
timestamp: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getWithdrawalParams {
graphApi: string;
subgraphName: string;
currency: string;
amount: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export function getWithdrawals({
graphApi,
subgraphName,
currency,
amount,
fromBlock,
fetchDataOptions,
}: getWithdrawalParams): Promise<GraphWithdrawals> {
return queryGraph<GraphWithdrawals>({
graphApi,
subgraphName,
query: GET_WITHDRAWALS,
variables: {
currency,
amount,
first,
fromBlock,
},
fetchDataOptions,
});
}
export async function getAllWithdrawals({
graphApi,
subgraphName,
currency,
amount,
fromBlock,
fetchDataOptions,
onProgress,
}: getWithdrawalParams): Promise<BaseGraphEvents<WithdrawalsEvents>> {
try {
const events = [];
let lastSyncBlock = fromBlock;
// eslint-disable-next-line no-constant-condition
while (true) {
let {
withdrawals: result,
_meta: {
// eslint-disable-next-line prefer-const
block: { number: currentBlock },
},
} = await getWithdrawals({ graphApi, subgraphName, currency, amount, fromBlock, fetchDataOptions });
lastSyncBlock = currentBlock;
if (isEmptyArray(result)) {
break;
}
const [firstEvent] = result;
const [lastEvent] = result.slice(-1);
if (typeof onProgress === 'function') {
onProgress({
type: 'Withdrawals',
fromBlock: Number(firstEvent.blockNumber),
toBlock: Number(lastEvent.blockNumber),
count: result.length,
});
}
if (result.length < 900) {
events.push(...result);
break;
}
result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
fromBlock = Number(lastEvent.blockNumber);
events.push(...result);
}
if (!events.length) {
return {
events: [],
lastSyncBlock,
};
}
const result = events.map(({ id, blockNumber, nullifier, to, fee, timestamp }) => {
const [transactionHash, logIndex] = id.split('-');
return {
blockNumber: Number(blockNumber),
logIndex: Number(logIndex),
transactionHash,
nullifierHash: nullifier,
to: getAddress(to),
fee,
timestamp: Number(timestamp),
};
});
const [lastEvent] = result.slice(-1);
return {
events: result,
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
};
} catch (err) {
console.log('Error from getAllWithdrawals query');
console.log(err);
return {
events: [],
lastSyncBlock: fromBlock,
};
}
}
export interface GraphNoteAccounts {
noteAccounts: {
id: string;
index: string;
address: string;
encryptedAccount: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getNoteAccountsParams {
graphApi: string;
subgraphName: string;
address: string;
fetchDataOptions?: fetchDataOptions;
}
export interface getNoteAccountsReturns {
events: {
id: string;
index: string;
address: string;
encryptedAccount: string;
}[];
lastSyncBlock: null | number;
}
export async function getNoteAccounts({
graphApi,
subgraphName,
address,
fetchDataOptions,
}: getNoteAccountsParams): Promise<getNoteAccountsReturns> {
try {
const {
noteAccounts: events,
_meta: {
block: { number: lastSyncBlock },
},
} = await queryGraph<GraphNoteAccounts>({
graphApi,
subgraphName,
query: GET_NOTE_ACCOUNTS,
variables: {
address,
},
fetchDataOptions,
});
return {
events,
lastSyncBlock,
};
} catch (err) {
console.log('Error from getNoteAccounts query');
console.log(err);
return {
events: [],
lastSyncBlock: null,
};
}
}
export interface GraphEncryptedNotes {
encryptedNotes: {
blockNumber: string;
index: string;
transactionHash: string;
encryptedNote: string;
}[];
_meta: {
block: {
number: number;
};
hasIndexingErrors: boolean;
};
}
export interface getEncryptedNotesParams {
graphApi: string;
subgraphName: string;
fromBlock: number;
fetchDataOptions?: fetchDataOptions;
onProgress?: BatchGraphOnProgress;
}
export function getEncryptedNotes({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
}: getEncryptedNotesParams): Promise<GraphEncryptedNotes> {
return queryGraph<GraphEncryptedNotes>({
graphApi,
subgraphName,
query: GET_ENCRYPTED_NOTES,
variables: {
first,
fromBlock,
},
fetchDataOptions,
});
}
export async function getAllEncryptedNotes({
graphApi,
subgraphName,
fromBlock,
fetchDataOptions,
onProgress,
}: getEncryptedNotesParams): Promise<BaseGraphEvents<EncryptedNotesEvents>> {
try {
const events = [];
let lastSyncBlock = fromBlock;
// eslint-disable-next-line no-constant-condition
while (true) {
let {
encryptedNotes: result,
_meta: {
// eslint-disable-next-line prefer-const
block: { number: currentBlock },
},
} = await getEncryptedNotes({ graphApi, subgraphName, fromBlock, fetchDataOptions });
lastSyncBlock = currentBlock;
if (isEmptyArray(result)) {
break;
}
const [firstEvent] = result;
const [lastEvent] = result.slice(-1);
if (typeof onProgress === 'function') {
onProgress({
type: 'EncryptedNotes',
fromBlock: Number(firstEvent.blockNumber),
toBlock: Number(lastEvent.blockNumber),
count: result.length,
});
}
if (result.length < 900) {
events.push(...result);
break;
}
result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
fromBlock = Number(lastEvent.blockNumber);
events.push(...result);
}
if (!events.length) {
return {
events: [],
lastSyncBlock,
};
}
const result = events.map((e) => ({
blockNumber: Number(e.blockNumber),
logIndex: Number(e.index),
transactionHash: e.transactionHash,
encryptedNote: e.encryptedNote,
}));
const [lastEvent] = result.slice(-1);
return {
events: result,
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
};
} catch (err) {
console.log('Error from getAllEncryptedNotes query');
console.log(err);
return {
events: [],
lastSyncBlock: fromBlock,
};
}
}

@ -0,0 +1,125 @@
export const GET_STATISTIC = `
query getStatistic($currency: String!, $amount: String!, $first: Int, $orderBy: BigInt, $orderDirection: String) {
deposits(first: $first, orderBy: $orderBy, orderDirection: $orderDirection, where: { currency: $currency, amount: $amount }) {
index
timestamp
blockNumber
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const _META = `
query getMeta {
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const GET_REGISTERED = `
query getRegistered($first: Int, $fromBlock: Int) {
relayers(first: $first, orderBy: blockRegistration, orderDirection: asc, where: {
blockRegistration_gte: $fromBlock
}) {
id
address
ensName
blockRegistration
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const GET_DEPOSITS = `
query getDeposits($currency: String!, $amount: String!, $first: Int, $fromBlock: Int) {
deposits(first: $first, orderBy: index, orderDirection: asc, where: {
amount: $amount,
currency: $currency,
blockNumber_gte: $fromBlock
}) {
id
blockNumber
commitment
index
timestamp
from
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const GET_WITHDRAWALS = `
query getWithdrawals($currency: String!, $amount: String!, $first: Int, $fromBlock: Int!) {
withdrawals(first: $first, orderBy: blockNumber, orderDirection: asc, where: {
currency: $currency,
amount: $amount,
blockNumber_gte: $fromBlock
}) {
id
blockNumber
nullifier
to
fee
timestamp
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const GET_NOTE_ACCOUNTS = `
query getNoteAccount($address: String!) {
noteAccounts(where: { address: $address }) {
id
index
address
encryptedAccount
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;
export const GET_ENCRYPTED_NOTES = `
query getEncryptedNotes($first: Int, $fromBlock: Int) {
encryptedNotes(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
blockNumber
index
transactionHash
encryptedNote
}
_meta {
block {
number
}
hasIndexingErrors
}
}
`;

19
src/services/index.ts Normal file

@ -0,0 +1,19 @@
export * from './events';
export * from './graphql';
export * from './schemas';
export * from './batch';
export * from './data';
export * from './deposits';
export * from './fees';
export * from './merkleTree';
export * from './mimc';
export * from './multicall';
export * from './networkConfig';
export * from './parser';
export * from './pedersen';
export * from './prices';
export * from './providers';
export * from './relayerClient';
export * from './tokens';
export * from './utils';
export * from './websnark';

135
src/services/merkleTree.ts Normal file

@ -0,0 +1,135 @@
import { Worker as NodeWorker } from 'worker_threads';
import { MerkleTree, Element } from '@tornado/fixed-merkle-tree';
import type { Tornado } from '@tornado/contracts';
import { isNode, toFixedHex } from './utils';
import { mimc } from './mimc';
import type { DepositType } from './deposits';
import type { DepositsEvents } from './events';
export type MerkleTreeConstructor = DepositType & {
Tornado: Tornado;
commitment?: string;
merkleTreeHeight?: number;
emptyElement?: string;
merkleWorkerPath?: string;
};
export class MerkleTreeService {
currency: string;
amount: string;
netId: number;
Tornado: Tornado;
commitment?: string;
instanceName: string;
merkleTreeHeight: number;
emptyElement: string;
merkleWorkerPath?: string;
constructor({
netId,
amount,
currency,
Tornado,
commitment,
merkleTreeHeight = 20,
emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292',
merkleWorkerPath,
}: MerkleTreeConstructor) {
const instanceName = `${netId}_${currency}_${amount}`;
this.currency = currency;
this.amount = amount;
this.netId = Number(netId);
this.Tornado = Tornado;
this.instanceName = instanceName;
this.commitment = commitment;
this.merkleTreeHeight = merkleTreeHeight;
this.emptyElement = emptyElement;
this.merkleWorkerPath = merkleWorkerPath;
}
async createTree({ events }: { events: Element[] }) {
const { hash: hashFunction } = await mimc.getHash();
if (this.merkleWorkerPath) {
console.log('Using merkleWorker\n');
try {
if (isNode) {
const merkleWorkerPromise = new Promise((resolve, reject) => {
const worker = new NodeWorker(this.merkleWorkerPath as string, {
workerData: {
merkleTreeHeight: this.merkleTreeHeight,
elements: events,
zeroElement: this.emptyElement,
},
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
}) as Promise<string>;
return MerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction);
} else {
const merkleWorkerPromise = new Promise((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const worker = new (Worker as any)(this.merkleWorkerPath);
worker.onmessage = (e: { data: string }) => {
resolve(e.data);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
worker.onerror = (e: any) => {
reject(e);
};
worker.postMessage({
merkleTreeHeight: this.merkleTreeHeight,
elements: events,
zeroElement: this.emptyElement,
});
}) as Promise<string>;
return MerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction);
}
} catch (err) {
console.log('merkleWorker failed, falling back to synchronous merkle tree');
console.log(err);
}
}
return new MerkleTree(this.merkleTreeHeight, events, {
zeroElement: this.emptyElement,
hashFunction,
});
}
async verifyTree({ events }: { events: DepositsEvents[] }) {
console.log(
`\nCreating deposit tree for ${this.netId} ${this.amount} ${this.currency.toUpperCase()} would take a while\n`,
);
console.time('Created tree in');
const tree = await this.createTree({ events: events.map(({ commitment }) => BigInt(commitment).toString()) });
console.timeEnd('Created tree in');
console.log('');
const isKnownRoot = await this.Tornado.isKnownRoot(toFixedHex(BigInt(tree.root)));
if (!isKnownRoot) {
const errMsg = `Deposit Event ${this.netId} ${this.amount} ${this.currency} is invalid`;
throw new Error(errMsg);
}
return tree;
}
}

28
src/services/mimc.ts Normal file

@ -0,0 +1,28 @@
import { MimcSponge, buildMimcSponge } from 'circomlibjs';
import type { Element, HashFunction } from '@tornado/fixed-merkle-tree';
export class Mimc {
sponge?: MimcSponge;
hash?: HashFunction<Element>;
mimcPromise: Promise<void>;
constructor() {
this.mimcPromise = this.initMimc();
}
async initMimc() {
this.sponge = await buildMimcSponge();
this.hash = (left, right) => this.sponge?.F.toString(this.sponge?.multiHash([BigInt(left), BigInt(right)]));
}
async getHash() {
await this.mimcPromise;
return {
sponge: this.sponge,
hash: this.hash,
};
}
}
export const mimc = new Mimc();

37
src/services/multicall.ts Normal file

@ -0,0 +1,37 @@
import { BaseContract, Interface } from 'ethers';
import { Multicall } from '../typechain';
export interface Call3 {
contract?: BaseContract;
address?: string;
interface?: Interface;
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any[];
allowFailure?: boolean;
}
export async function multicall(Multicall: Multicall, calls: Call3[]) {
const calldata = calls.map((call) => {
const target = (call.contract?.target || call.address) as string;
const callInterface = (call.contract?.interface || call.interface) as Interface;
return {
target,
callData: callInterface.encodeFunctionData(call.name, call.params),
allowFailure: call.allowFailure ?? false,
};
});
const returnData = await Multicall.aggregate3.staticCall(calldata);
const res = returnData.map((call, i) => {
const callInterface = (calls[i].contract?.interface || calls[i].interface) as Interface;
const [result, data] = call;
const decodeResult =
result && data && data !== '0x' ? callInterface.decodeFunctionResult(calls[i].name, data) : null;
return !decodeResult ? null : decodeResult.length === 1 ? decodeResult[0] : decodeResult;
});
return res;
}

@ -0,0 +1,741 @@
export interface RpcUrl {
name: string;
url: string;
}
export type RpcUrls = {
[key in string]: RpcUrl;
};
export interface SubgraphUrl {
name: string;
url: string;
}
export type SubgraphUrls = {
[key in string]: SubgraphUrl;
};
export type TornadoInstance = {
instanceAddress: {
[key in string]: string;
};
optionalInstances?: string[];
tokenAddress?: string;
tokenGasLimit?: number;
symbol: string;
decimals: number;
gasLimit?: number;
};
export type TokenInstances = {
[key in string]: TornadoInstance;
};
export type Config = {
rpcCallRetryAttempt?: number;
// Should be in gwei
gasPrices: {
// fallback gasPrice / maxFeePerGas value
instant: number;
fast?: number;
standard?: number;
low?: number;
// fallback EIP-1559 params
maxPriorityFeePerGas?: number;
};
nativeCurrency: string;
currencyName: string;
explorerUrl: {
tx: string;
address: string;
block: string;
};
merkleTreeHeight: number;
emptyElement: string;
networkName: string;
deployedBlock: number;
rpcUrls: RpcUrls;
multicall: string;
routerContract: string;
registryContract?: string;
echoContract: string;
aggregatorContract?: string;
reverseRecordsContract?: string;
gasPriceOracleContract?: string;
gasStationApi?: string;
ovmGasPriceOracleContract?: string;
tornadoSubgraph: string;
registrySubgraph?: string;
subgraphs: SubgraphUrls;
tokens: TokenInstances;
optionalTokens?: string[];
ensSubdomainKey: string;
// Should be in seconds
pollInterval: number;
constants: {
GOVERNANCE_BLOCK?: number;
NOTE_ACCOUNT_BLOCK?: number;
ENCRYPTED_NOTES_BLOCK?: number;
REGISTRY_BLOCK?: number;
// Should be in seconds
MINING_BLOCK_TIME?: number;
};
'torn.contract.tornadocash.eth'?: string;
'governance.contract.tornadocash.eth'?: string;
'staking-rewards.contract.tornadocash.eth'?: string;
'tornado-router.contract.tornadocash.eth'?: string;
'tornado-proxy-light.contract.tornadocash.eth'?: string;
};
export type networkConfig = {
[key in string]: Config;
};
export const blockSyncInterval = 10000;
export const enabledChains = ['1', '10', '56', '100', '137', '42161', '43114', '11155111'];
const theGraph = {
name: 'Hosted Graph',
url: 'https://api.thegraph.com',
};
const tornado = {
name: 'Tornado Subgraphs',
url: 'https://tornadocash-rpc.com',
};
export const networkConfig: networkConfig = {
netId1: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 80,
fast: 50,
standard: 25,
low: 8,
},
nativeCurrency: 'eth',
currencyName: 'ETH',
explorerUrl: {
tx: 'https://etherscan.io/tx/',
address: 'https://etherscan.io/address/',
block: 'https://etherscan.io/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Ethereum Mainnet',
deployedBlock: 9116966,
rpcUrls: {
tornado: {
name: 'Tornado RPC',
url: 'https://tornadocash-rpc.com',
},
chainnodes: {
name: 'Tornado RPC',
url: 'https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
mevblockerRPC: {
name: 'MevblockerRPC',
url: 'https://rpc.mevblocker.io',
},
stackup: {
name: 'Stackup RPC',
url: 'https://public.stackup.sh/api/v1/node/ethereum-mainnet',
},
noderealRPC: {
name: 'NodeReal RPC',
url: 'https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7',
},
notadegenRPC: {
name: 'NotADegen RPC',
url: 'https://rpc.notadegen.com/eth',
},
keydonixRPC: {
name: 'Keydonix RPC',
url: 'https://ethereum.keydonix.com/v1/mainnet',
},
oneRPC: {
name: '1RPC',
url: 'https://1rpc.io/eth',
},
},
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
registryContract: '0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2',
echoContract: '0x9B27DD5Bb15d42DC224FCD0B7caEbBe16161Df42',
aggregatorContract: '0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49',
reverseRecordsContract: '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C',
tornadoSubgraph: 'tornadocash/mainnet-tornado-subgraph',
registrySubgraph: 'tornadocash/tornado-relayer-registry',
subgraphs: {
tornado,
theGraph,
},
tokens: {
eth: {
instanceAddress: {
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
'100': '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291',
},
symbol: 'ETH',
decimals: 18,
},
dai: {
instanceAddress: {
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
'10000': '0x07687e702b410Fa43f4cB4Af7FA097918ffD2730',
'100000': '0x23773E65ed146A459791799d01336DB287f25334',
},
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
tokenGasLimit: 70_000,
symbol: 'DAI',
decimals: 18,
gasLimit: 700_000,
},
cdai: {
instanceAddress: {
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
'50000': '0x03893a7c7463AE47D46bc7f091665f1893656003',
'500000': '0x2717c5e28cf931547B621a5dddb772Ab6A35B701',
'5000000': '0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af',
},
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
tokenGasLimit: 200_000,
symbol: 'cDAI',
decimals: 8,
gasLimit: 700_000,
},
usdc: {
instanceAddress: {
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
},
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
tokenGasLimit: 70_000,
symbol: 'USDC',
decimals: 6,
gasLimit: 700_000,
},
usdt: {
instanceAddress: {
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
},
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
tokenGasLimit: 70_000,
symbol: 'USDT',
decimals: 6,
gasLimit: 700_000,
},
wbtc: {
instanceAddress: {
'0.1': '0x178169B423a011fff22B9e3F3abeA13414dDD0F1',
'1': '0x610B717796ad172B316836AC95a2ffad065CeaB4',
'10': '0xbB93e510BbCD0B7beb5A853875f9eC60275CF498',
},
tokenAddress: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
tokenGasLimit: 70_000,
symbol: 'WBTC',
decimals: 8,
gasLimit: 700_000,
},
},
ensSubdomainKey: 'mainnet-tornado',
pollInterval: 15,
constants: {
GOVERNANCE_BLOCK: 11474695,
NOTE_ACCOUNT_BLOCK: 11842486,
ENCRYPTED_NOTES_BLOCK: 14248730,
REGISTRY_BLOCK: 14173129,
MINING_BLOCK_TIME: 15,
},
'torn.contract.tornadocash.eth': '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
'governance.contract.tornadocash.eth': '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
'tornado-router.contract.tornadocash.eth': '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
'staking-rewards.contract.tornadocash.eth': '0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29',
},
netId56: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 5,
fast: 5,
standard: 5,
low: 5,
},
nativeCurrency: 'bnb',
currencyName: 'BNB',
explorerUrl: {
tx: 'https://bscscan.com/tx/',
address: 'https://bscscan.com/address/',
block: 'https://bscscan.com/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Binance Smart Chain',
deployedBlock: 8158799,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
tornadoSubgraph: 'tornadocash/bsc-tornado-subgraph',
subgraphs: {
tornado,
theGraph,
},
rpcUrls: {
tornado: {
name: 'Tornado RPC',
url: 'https://tornadocash-rpc.com/bsc',
},
chainnodes: {
name: 'Tornado RPC',
url: 'https://bsc-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
stackup: {
name: 'Stackup RPC',
url: 'https://public.stackup.sh/api/v1/node/bsc-mainnet',
},
noderealRPC: {
name: 'NodeReal RPC',
url: 'https://bsc-mainnet.nodereal.io/v1/64a9df0874fb4a93b9d0a3849de012d3',
},
oneRPC: {
name: '1RPC',
url: 'https://1rpc.io/bnb',
},
},
tokens: {
bnb: {
instanceAddress: {
'0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F',
'1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
},
symbol: 'BNB',
decimals: 18,
},
},
ensSubdomainKey: 'bsc-tornado',
pollInterval: 10,
constants: {
NOTE_ACCOUNT_BLOCK: 8159269,
ENCRYPTED_NOTES_BLOCK: 8159269,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId137: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 100,
fast: 75,
standard: 50,
low: 30,
},
nativeCurrency: 'matic',
currencyName: 'MATIC',
explorerUrl: {
tx: 'https://polygonscan.com/tx/',
address: 'https://polygonscan.com/address/',
block: 'https://polygonscan.com/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Polygon (Matic) Network',
deployedBlock: 16257962,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
gasPriceOracleContract: '0xF81A8D8D3581985D3969fe53bFA67074aDFa8F3C',
tornadoSubgraph: 'tornadocash/matic-tornado-subgraph',
subgraphs: {
tornado,
theGraph,
},
rpcUrls: {
chainnodes: {
name: 'Tornado RPC',
url: 'https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
stackup: {
name: 'Stackup RPC',
url: 'https://public.stackup.sh/api/v1/node/polygon-mainnet',
},
oneRpc: {
name: '1RPC',
url: 'https://1rpc.io/matic',
},
},
tokens: {
matic: {
instanceAddress: {
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
'1000': '0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178',
'10000': '0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040',
'100000': '0xa5C2254e4253490C54cef0a4347fddb8f75A4998',
},
symbol: 'MATIC',
decimals: 18,
},
},
ensSubdomainKey: 'polygon-tornado',
pollInterval: 10,
constants: {
NOTE_ACCOUNT_BLOCK: 16257996,
ENCRYPTED_NOTES_BLOCK: 16257996,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId10: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 0.001,
fast: 0.001,
standard: 0.001,
low: 0.001,
},
nativeCurrency: 'eth',
currencyName: 'ETH',
explorerUrl: {
tx: 'https://optimistic.etherscan.io/tx/',
address: 'https://optimistic.etherscan.io/address/',
block: 'https://optimistic.etherscan.io/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Optimism',
deployedBlock: 2243689,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
ovmGasPriceOracleContract: '0x420000000000000000000000000000000000000F',
tornadoSubgraph: 'tornadocash/optimism-tornado-subgraph',
subgraphs: {
tornado,
theGraph,
},
rpcUrls: {
tornado: {
name: 'Tornado RPC',
url: 'https://tornadocash-rpc.com/op',
},
chainnodes: {
name: 'Tornado RPC',
url: 'https://optimism-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
optimism: {
name: 'Optimism RPC',
url: 'https://mainnet.optimism.io',
},
stackup: {
name: 'Stackup RPC',
url: 'https://public.stackup.sh/api/v1/node/optimism-mainnet',
},
oneRpc: {
name: '1RPC',
url: 'https://1rpc.io/op',
},
},
tokens: {
eth: {
instanceAddress: {
'0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F',
'1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
},
symbol: 'ETH',
decimals: 18,
},
},
ensSubdomainKey: 'optimism-tornado',
pollInterval: 15,
constants: {
NOTE_ACCOUNT_BLOCK: 2243694,
ENCRYPTED_NOTES_BLOCK: 2243694,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId42161: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 4,
fast: 3,
standard: 2.52,
low: 2.29,
},
nativeCurrency: 'eth',
currencyName: 'ETH',
explorerUrl: {
tx: 'https://arbiscan.io/tx/',
address: 'https://arbiscan.io/address/',
block: 'https://arbiscan.io/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Arbitrum One',
deployedBlock: 3430648,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
tornadoSubgraph: 'tornadocash/arbitrum-tornado-subgraph',
subgraphs: {
tornado,
theGraph,
},
rpcUrls: {
tornado: {
name: 'Tornado RPC',
url: 'https://tornadocash-rpc.com/arbitrum',
},
chainnodes: {
name: 'Tornado RPC',
url: 'https://arbitrum-one.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
arbitrum: {
name: 'Arbitrum RPC',
url: 'https://arb1.arbitrum.io/rpc',
},
stackup: {
name: 'Stackup RPC',
url: 'https://public.stackup.sh/api/v1/node/arbitrum-one',
},
oneRpc: {
name: '1rpc',
url: 'https://1rpc.io/arb',
},
},
tokens: {
eth: {
instanceAddress: {
'0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F',
'1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
},
symbol: 'ETH',
decimals: 18,
},
},
ensSubdomainKey: 'arbitrum-tornado',
pollInterval: 15,
constants: {
NOTE_ACCOUNT_BLOCK: 3430605,
ENCRYPTED_NOTES_BLOCK: 3430605,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId100: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 6,
fast: 5,
standard: 4,
low: 1,
},
nativeCurrency: 'xdai',
currencyName: 'xDAI',
explorerUrl: {
tx: 'https://blockscout.com/xdai/mainnet/tx/',
address: 'https://blockscout.com/xdai/mainnet/address/',
block: 'https://blockscout.com/xdai/mainnet/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Gnosis Chain',
deployedBlock: 17754561,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
tornadoSubgraph: 'tornadocash/xdai-tornado-subgraph',
subgraphs: {
tornado,
theGraph,
},
rpcUrls: {
tornado: {
name: 'Tornado RPC',
url: 'https://tornadocash-rpc.com/gnosis',
},
chainnodes: {
name: 'Tornado RPC',
url: 'https://gnosis-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
gnosis: {
name: 'Gnosis RPC',
url: 'https://rpc.gnosischain.com',
},
stackup: {
name: 'Stackup RPC',
url: 'https://public.stackup.sh/api/v1/node/arbitrum-one',
},
blockPi: {
name: 'BlockPi',
url: 'https://gnosis.blockpi.network/v1/rpc/public',
},
},
tokens: {
xdai: {
instanceAddress: {
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
'1000': '0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178',
'10000': '0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040',
'100000': '0xa5C2254e4253490C54cef0a4347fddb8f75A4998',
},
symbol: 'xDAI',
decimals: 18,
},
},
ensSubdomainKey: 'gnosis-tornado',
pollInterval: 15,
constants: {
NOTE_ACCOUNT_BLOCK: 17754564,
ENCRYPTED_NOTES_BLOCK: 17754564,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId43114: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 225,
fast: 35,
standard: 25,
low: 25,
},
nativeCurrency: 'avax',
currencyName: 'AVAX',
explorerUrl: {
tx: 'https://snowtrace.io/tx/',
address: 'https://snowtrace.io/address/',
block: 'https://snowtrace.io/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Avalanche Mainnet',
deployedBlock: 4429818,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
tornadoSubgraph: 'tornadocash/avalanche-tornado-subgraph',
subgraphs: {
theGraph,
},
rpcUrls: {
oneRPC: {
name: 'OneRPC',
url: 'https://1rpc.io/avax/c',
},
avalancheRPC: {
name: 'Avalanche RPC',
url: 'https://api.avax.network/ext/bc/C/rpc',
},
meowRPC: {
name: 'Meow RPC',
url: 'https://avax.meowrpc.com',
},
},
tokens: {
avax: {
instanceAddress: {
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
'500': '0xaf8d1839c3c67cf571aa74B5c12398d4901147B3',
},
symbol: 'AVAX',
decimals: 18,
},
},
ensSubdomainKey: 'avalanche-tornado',
pollInterval: 10,
constants: {
NOTE_ACCOUNT_BLOCK: 4429813,
ENCRYPTED_NOTES_BLOCK: 4429813,
},
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
},
netId11155111: {
rpcCallRetryAttempt: 15,
gasPrices: {
instant: 2,
fast: 2,
standard: 2,
low: 2,
},
nativeCurrency: 'eth',
currencyName: 'SepoliaETH',
explorerUrl: {
tx: 'https://sepolia.etherscan.io/tx/',
address: 'https://sepolia.etherscan.io/address/',
block: 'https://sepolia.etherscan.io/block/',
},
merkleTreeHeight: 20,
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
networkName: 'Ethereum Sepolia',
deployedBlock: 5594395,
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
routerContract: '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
registryContract: '0x1428e5d2356b13778A13108b10c440C83011dfB8',
echoContract: '0xcDD1fc3F5ac2782D83449d3AbE80D6b7B273B0e5',
aggregatorContract: '0x4088712AC9fad39ea133cdb9130E465d235e9642',
reverseRecordsContract: '0xEc29700C0283e5Be64AcdFe8077d6cC95dE23C23',
tornadoSubgraph: 'tornadocash/sepolia-tornado-subgraph',
subgraphs: {
tornado,
},
rpcUrls: {
tornado: {
name: 'Tornado RPC',
url: 'https://tornadocash-rpc.com/sepolia',
},
sepolia: {
name: 'Sepolia RPC',
url: 'https://rpc.sepolia.org',
},
chainnodes: {
name: 'Chainnodes RPC',
url: 'https://sepolia.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
},
},
tokens: {
eth: {
instanceAddress: {
'0.1': '0x8C4A04d872a6C1BE37964A21ba3a138525dFF50b',
'1': '0x8cc930096B4Df705A007c4A039BDFA1320Ed2508',
'10': '0x8D10d506D29Fc62ABb8A290B99F66dB27Fc43585',
'100': '0x44c5C92ed73dB43888210264f0C8b36Fd68D8379',
},
symbol: 'ETH',
decimals: 18,
},
dai: {
instanceAddress: {
'100': '0x6921fd1a97441dd603a997ED6DDF388658daf754',
'1000': '0x50a637770F5d161999420F7d70d888DE47207145',
'10000': '0xecD649870407cD43923A816Cc6334a5bdf113621',
'100000': '0x73B4BD04bF83206B6e979BE2507098F92EDf4F90',
},
tokenAddress: '0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357',
tokenGasLimit: 70_000,
symbol: 'DAI',
decimals: 18,
gasLimit: 700_000,
},
},
ensSubdomainKey: 'sepolia-tornado',
pollInterval: 15,
constants: {
GOVERNANCE_BLOCK: 5594395,
NOTE_ACCOUNT_BLOCK: 5594395,
ENCRYPTED_NOTES_BLOCK: 5594395,
MINING_BLOCK_TIME: 15,
},
'torn.contract.tornadocash.eth': '0x3AE6667167C0f44394106E197904519D808323cA',
'governance.contract.tornadocash.eth': '0xe5324cD7602eeb387418e594B87aCADee08aeCAD',
'tornado-router.contract.tornadocash.eth': '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
},
};
export const subdomains = enabledChains.map((chain) => networkConfig[`netId${chain}`].ensSubdomainKey);
export default networkConfig;

62
src/services/parser.ts Normal file

@ -0,0 +1,62 @@
import { InvalidArgumentError } from 'commander';
import { computeAddress, getAddress, Mnemonic } from 'ethers';
import { validateUrl } from './utils';
export function parseNumber(value?: string | number): number {
if (!value || isNaN(Number(value))) {
throw new InvalidArgumentError('Invalid Number');
}
return Number(value);
}
export function parseUrl(value?: string): string {
if (!value || !validateUrl(value, ['http:', 'https:'])) {
throw new InvalidArgumentError('Invalid URL');
}
return value;
}
export function parseRelayer(value?: string): string {
if (!value || !(value.endsWith('.eth') || validateUrl(value, ['http:', 'https:']))) {
throw new InvalidArgumentError('Invalid Relayer ETH address or URL');
}
return value;
}
export function parseAddress(value?: string): string {
if (!value) {
throw new InvalidArgumentError('Invalid Address');
}
try {
return getAddress(value);
} catch {
throw new InvalidArgumentError('Invalid Address');
}
}
export function parseMnemonic(value?: string): string {
if (!value) {
throw new InvalidArgumentError('Invalid Mnemonic');
}
try {
Mnemonic.fromPhrase(value);
} catch {
throw new InvalidArgumentError('Invalid Mnemonic');
}
return value;
}
export function parseKey(value?: string): string {
if (!value) {
throw new InvalidArgumentError('Invalid Private Key');
}
if (value.length === 64) {
value = '0x' + value;
}
try {
computeAddress(value);
} catch {
throw new InvalidArgumentError('Invalid Private Key');
}
return value;
}

32
src/services/pedersen.ts Normal file

@ -0,0 +1,32 @@
import { BabyJub, PedersenHash, Point, buildPedersenHash } from 'circomlibjs';
export class Pedersen {
pedersenHash?: PedersenHash;
babyJub?: BabyJub;
pedersenPromise: Promise<void>;
constructor() {
this.pedersenPromise = this.initPedersen();
}
async initPedersen() {
this.pedersenHash = await buildPedersenHash();
this.babyJub = this.pedersenHash.babyJub;
}
async unpackPoint(buffer: Uint8Array) {
await this.pedersenPromise;
return this.babyJub?.unpackPoint(this.pedersenHash?.hash(buffer) as Uint8Array);
}
toStringBuffer(buffer: Uint8Array): string {
return this.babyJub?.F.toString(buffer);
}
}
export const pedersen = new Pedersen();
export async function buffPedersenHash(buffer: Uint8Array): Promise<string> {
const [hash] = (await pedersen.unpackPoint(buffer)) as Point;
return pedersen.toStringBuffer(hash);
}

31
src/services/prices.ts Normal file

@ -0,0 +1,31 @@
import { parseEther, type Provider } from 'ethers';
import type { OffchainOracle, Multicall } from '../typechain';
import { multicall } from './multicall';
export class TokenPriceOracle {
oracle?: OffchainOracle;
multicall: Multicall;
provider: Provider;
constructor(provider: Provider, multicall: Multicall, oracle?: OffchainOracle) {
this.provider = provider;
this.multicall = multicall;
this.oracle = oracle;
}
fetchPrices(tokens: string[]): Promise<bigint[]> {
// setup mock price for testnets
if (!this.oracle) {
return new Promise((resolve) => resolve(tokens.map(() => parseEther('0.0001'))));
}
return multicall(
this.multicall,
tokens.map((token) => ({
contract: this.oracle,
name: 'getRateToEth',
params: [token, true],
})),
);
}
}

646
src/services/providers.ts Normal file

@ -0,0 +1,646 @@
import type { EventEmitter } from 'stream';
import type { RequestOptions } from 'http';
import crossFetch from 'cross-fetch';
import { HttpProxyAgent } from 'http-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { SocksProxyAgent } from 'socks-proxy-agent';
import {
FetchRequest,
JsonRpcApiProvider,
JsonRpcProvider,
Wallet,
HDNodeWallet,
FetchGetUrlFunc,
Provider,
SigningKey,
TransactionRequest,
JsonRpcSigner,
BrowserProvider,
Networkish,
Eip1193Provider,
VoidSigner,
Network,
parseUnits,
FetchUrlFeeDataNetworkPlugin,
BigNumberish,
FeeData,
EnsPlugin,
GasCostPlugin,
} from 'ethers';
import type { RequestInfo, RequestInit, Response, HeadersInit } from 'node-fetch';
import { GasPriceOracle, GasPriceOracle__factory, Multicall, Multicall__factory } from '../typechain';
import { isNode, sleep } from './utils';
import type { Config } from './networkConfig';
import { multicall } from './multicall';
declare global {
interface Window {
ethereum?: Eip1193Provider & EventEmitter;
}
}
// Update this for every Tor Browser release
export const defaultUserAgent = 'Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0';
export const fetch = crossFetch as unknown as nodeFetch;
export type nodeFetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;
export type fetchDataOptions = RequestInit & {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
headers?: HeadersInit | any;
maxRetry?: number;
retryOn?: number;
userAgent?: string;
timeout?: number;
proxy?: string;
torPort?: number;
// eslint-disable-next-line @typescript-eslint/ban-types
debug?: Function;
returnResponse?: boolean;
};
export type NodeAgent = RequestOptions['agent'] | ((parsedUrl: URL) => RequestOptions['agent']);
export function getHttpAgent({
fetchUrl,
proxyUrl,
torPort,
retry,
}: {
fetchUrl: string;
proxyUrl?: string;
torPort?: number;
retry: number;
}): NodeAgent | undefined {
if (torPort) {
return new SocksProxyAgent(`socks5h://tor${retry}@127.0.0.1:${torPort}`);
}
if (!proxyUrl) {
return;
}
const isHttps = fetchUrl.includes('https://');
if (proxyUrl.includes('socks://') || proxyUrl.includes('socks4://') || proxyUrl.includes('socks5://')) {
return new SocksProxyAgent(proxyUrl);
}
if (proxyUrl.includes('http://') || proxyUrl.includes('https://')) {
if (isHttps) {
return new HttpsProxyAgent(proxyUrl);
}
return new HttpProxyAgent(proxyUrl);
}
}
export async function fetchData(url: string, options: fetchDataOptions = {}) {
const MAX_RETRY = options.maxRetry ?? 3;
const RETRY_ON = options.retryOn ?? 500;
const userAgent = options.userAgent ?? defaultUserAgent;
let retry = 0;
let errorObject;
if (!options.method) {
if (!options.body) {
options.method = 'GET';
} else {
options.method = 'POST';
}
}
if (!options.headers) {
options.headers = {};
}
if (isNode && !options.headers['User-Agent']) {
options.headers['User-Agent'] = userAgent;
}
while (retry < MAX_RETRY + 1) {
let timeout;
// Define promise timeout when the options.timeout is available
if (!options.signal && options.timeout) {
const controller = new AbortController();
options.signal = controller.signal;
// Define timeout in seconds
timeout = setTimeout(() => {
controller.abort();
}, options.timeout);
}
if (!options.agent && isNode && (options.proxy || options.torPort)) {
options.agent = getHttpAgent({
fetchUrl: url,
proxyUrl: options.proxy,
torPort: options.torPort,
retry,
});
}
if (options.debug && typeof options.debug === 'function') {
options.debug('request', {
url,
retry,
errorObject,
options,
});
}
try {
const resp = await fetch(url, {
method: options.method,
headers: options.headers,
body: options.body,
redirect: options.redirect,
signal: options.signal,
agent: options.agent,
});
if (options.debug && typeof options.debug === 'function') {
options.debug('response', resp);
}
if (!resp.ok) {
const errMsg = `Request to ${url} failed with error code ${resp.status}:\n` + (await resp.text());
throw new Error(errMsg);
}
if (options.returnResponse) {
return resp;
}
const contentType = resp.headers.get('content-type');
// If server returns JSON object, parse it and return as an object
if (contentType?.includes('application/json')) {
return await resp.json();
}
// Else if the server returns text parse it as a string
if (contentType?.includes('text')) {
return await resp.text();
}
// Return as a response object https://developer.mozilla.org/en-US/docs/Web/API/Response
return resp;
} catch (error) {
if (timeout) {
clearTimeout(timeout);
}
errorObject = error;
retry++;
await sleep(RETRY_ON);
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
if (options.debug && typeof options.debug === 'function') {
options.debug('error', errorObject);
}
throw errorObject;
}
/* eslint-disable prettier/prettier, @typescript-eslint/no-explicit-any */
export const fetchGetUrlFunc =
(options: fetchDataOptions = {}): FetchGetUrlFunc =>
async (req, _signal) => {
let signal;
if (_signal) {
const controller = new AbortController();
signal = controller.signal;
_signal.addListener(() => {
controller.abort();
});
}
const init = {
...options,
method: req.method || 'POST',
headers: req.headers,
body: req.body || undefined,
signal,
returnResponse: true,
};
const resp = await fetchData(req.url, init);
const headers = {} as { [key in string]: any };
resp.headers.forEach((value: any, key: string) => {
headers[key.toLowerCase()] = value;
});
const respBody = await resp.arrayBuffer();
const body = respBody == null ? null : new Uint8Array(respBody);
return {
statusCode: resp.status,
statusMessage: resp.statusText,
headers,
body,
};
};
/* eslint-enable prettier/prettier, @typescript-eslint/no-explicit-any */
// caching to improve performance
const oracleMapper = new Map();
const multicallMapper = new Map();
export type getProviderOptions = fetchDataOptions & {
pollingInterval?: number;
gasPriceOracle?: string;
gasStationApi?: string;
};
export function getGasOraclePlugin(networkKey: string, fetchOptions?: getProviderOptions) {
const gasStationApi = fetchOptions?.gasStationApi || 'https://gasstation.polygon.technology/v2';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return new FetchUrlFeeDataNetworkPlugin(gasStationApi, async (fetchFeeData, provider, request) => {
if (!oracleMapper.has(networkKey)) {
oracleMapper.set(networkKey, GasPriceOracle__factory.connect(fetchOptions?.gasPriceOracle as string, provider));
}
if (!multicallMapper.has(networkKey)) {
multicallMapper.set(
networkKey,
Multicall__factory.connect('0xcA11bde05977b3631167028862bE2a173976CA11', provider),
);
}
const Oracle = oracleMapper.get(networkKey) as GasPriceOracle;
const Multicall = multicallMapper.get(networkKey) as Multicall;
const [timestamp, heartbeat, feePerGas, priorityFeePerGas] = await multicall(Multicall, [
{
contract: Oracle,
name: 'timestamp',
},
{
contract: Oracle,
name: 'heartbeat',
},
{
contract: Oracle,
name: 'maxFeePerGas',
},
{
contract: Oracle,
name: 'maxPriorityFeePerGas',
},
]);
const isOutdated = Number(timestamp) <= Date.now() / 1000 - Number(heartbeat);
if (!isOutdated) {
const maxPriorityFeePerGas = (priorityFeePerGas * BigInt(13)) / BigInt(10);
const maxFeePerGas = feePerGas * BigInt(2) + maxPriorityFeePerGas;
return {
gasPrice: maxFeePerGas,
maxFeePerGas,
maxPriorityFeePerGas,
};
}
const fetchReq = new FetchRequest(gasStationApi);
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
if (isNode) {
// Prevent Cloudflare from blocking our request in node.js
fetchReq.setHeader('User-Agent', 'ethers');
}
const [
{
bodyJson: { fast },
},
{ gasPrice },
] = await Promise.all([fetchReq.send(), fetchFeeData()]);
return {
gasPrice,
maxFeePerGas: parseUnits(`${fast.maxFee}`, 9),
maxPriorityFeePerGas: parseUnits(`${fast.maxPriorityFee}`, 9),
};
});
}
export async function getProvider(rpcUrl: string, fetchOptions?: getProviderOptions): Promise<JsonRpcProvider> {
const fetchReq = new FetchRequest(rpcUrl);
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
// omit network plugins and mimic registerEth function (required for polygon)
const _staticNetwork = await new JsonRpcProvider(fetchReq).getNetwork();
const ensPlugin = _staticNetwork.getPlugin('org.ethers.plugins.network.Ens');
const gasCostPlugin = _staticNetwork.getPlugin('org.ethers.plugins.network.GasCost');
const gasStationPlugin = <FetchUrlFeeDataNetworkPlugin>(
_staticNetwork.getPlugin('org.ethers.plugins.network.FetchUrlFeeDataPlugin')
);
const staticNetwork = new Network(_staticNetwork.name, _staticNetwork.chainId);
if (ensPlugin) {
staticNetwork.attachPlugin(ensPlugin);
}
if (gasCostPlugin) {
staticNetwork.attachPlugin(gasCostPlugin);
}
if (fetchOptions?.gasPriceOracle) {
staticNetwork.attachPlugin(getGasOraclePlugin(`${_staticNetwork.chainId}_${rpcUrl}`, fetchOptions));
} else if (gasStationPlugin) {
staticNetwork.attachPlugin(gasStationPlugin);
}
const provider = new JsonRpcProvider(fetchReq, staticNetwork, {
staticNetwork,
});
provider.pollingInterval = fetchOptions?.pollingInterval || 1000;
return provider;
}
export function getProviderWithNetId(
netId: BigNumberish,
rpcUrl: string,
config: Config,
fetchOptions?: getProviderOptions,
): JsonRpcProvider {
const { networkName, reverseRecordsContract, gasPriceOracleContract, gasStationApi, pollInterval } = config;
const hasEns = Boolean(reverseRecordsContract);
const fetchReq = new FetchRequest(rpcUrl);
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
const staticNetwork = new Network(networkName, netId);
if (hasEns) {
staticNetwork.attachPlugin(new EnsPlugin(null, Number(netId)));
}
staticNetwork.attachPlugin(new GasCostPlugin());
if (gasPriceOracleContract) {
staticNetwork.attachPlugin(
getGasOraclePlugin(`${netId}_${rpcUrl}`, {
gasPriceOracle: gasPriceOracleContract,
gasStationApi,
}),
);
}
const provider = new JsonRpcProvider(fetchReq, staticNetwork, {
staticNetwork,
});
provider.pollingInterval = fetchOptions?.pollingInterval || pollInterval * 1000;
return provider;
}
export const populateTransaction = async (
signer: TornadoWallet | TornadoVoidSigner | TornadoRpcSigner,
tx: TransactionRequest,
) => {
const provider = signer.provider as Provider;
if (!tx.from) {
tx.from = signer.address;
} else if (tx.from !== signer.address) {
const errMsg = `populateTransaction: signer mismatch for tx, wants ${tx.from} have ${signer.address}`;
throw new Error(errMsg);
}
const [feeData, nonce] = await Promise.all([
(async () => {
if (tx.maxFeePerGas && tx.maxPriorityFeePerGas) {
return new FeeData(null, BigInt(tx.maxFeePerGas), BigInt(tx.maxPriorityFeePerGas));
}
if (tx.gasPrice) {
return new FeeData(BigInt(tx.gasPrice), null, null);
}
const fetchedFeeData = await provider.getFeeData();
if (fetchedFeeData.maxFeePerGas && fetchedFeeData.maxPriorityFeePerGas) {
return new FeeData(
null,
(fetchedFeeData.maxFeePerGas * (BigInt(10000) + BigInt(signer.gasPriceBump))) / BigInt(10000),
fetchedFeeData.maxPriorityFeePerGas,
);
} else {
return new FeeData(
((fetchedFeeData.gasPrice as bigint) * (BigInt(10000) + BigInt(signer.gasPriceBump))) / BigInt(10000),
null,
null,
);
}
})(),
(async () => {
if (tx.nonce) {
return tx.nonce;
}
let fetchedNonce = await provider.getTransactionCount(signer.address, 'pending');
// Deal with cached nonce results
if (signer.bumpNonce && signer.nonce && signer.nonce >= fetchedNonce) {
console.log(
`populateTransaction: bumping nonce from ${fetchedNonce} to ${fetchedNonce + 1} for ${signer.address}`,
);
fetchedNonce++;
}
return fetchedNonce;
})(),
]);
tx.nonce = nonce;
// EIP-1559
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
tx.maxFeePerGas = feeData.maxFeePerGas;
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
if (!tx.type) {
tx.type = 2;
}
delete tx.gasPrice;
} else if (feeData.gasPrice) {
tx.gasPrice = feeData.gasPrice;
if (!tx.type) {
tx.type = 0;
}
delete tx.maxFeePerGas;
delete tx.maxPriorityFeePerGas;
}
// gasLimit
tx.gasLimit =
tx.gasLimit ||
(await (async () => {
try {
const gasLimit = await provider.estimateGas(tx);
return gasLimit === BigInt(21000)
? gasLimit
: (gasLimit * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000);
} catch (err) {
if (signer.gasFailover) {
console.log('populateTransaction: warning gas estimation failed falling back to 3M gas');
// Gas failover
return BigInt('3000000');
}
throw err;
}
})());
return tx;
};
export type TornadoWalletOptions = {
gasPriceBump?: number;
gasLimitBump?: number;
gasFailover?: boolean;
bumpNonce?: boolean;
};
export class TornadoWallet extends Wallet {
nonce?: number | null;
gasPriceBump: number;
gasLimitBump: number;
gasFailover: boolean;
bumpNonce: boolean;
constructor(
key: string | SigningKey,
provider?: null | Provider,
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {},
) {
super(key, provider);
// 10% bump from the recommended fee
this.gasPriceBump = gasPriceBump ?? 1000;
// 30% bump from the recommended gaslimit
this.gasLimitBump = gasLimitBump ?? 3000;
this.gasFailover = gasFailover ?? false;
// Disable bump nonce feature unless being used by the server environment
this.bumpNonce = bumpNonce ?? false;
}
static fromMnemonic(mneomnic: string, provider: Provider, index = 0, options?: TornadoWalletOptions) {
const defaultPath = `m/44'/60'/0'/0/${index}`;
const { privateKey } = HDNodeWallet.fromPhrase(mneomnic, undefined, defaultPath);
return new TornadoWallet(privateKey as unknown as SigningKey, provider, options);
}
async populateTransaction(tx: TransactionRequest) {
const txObject = await populateTransaction(this, tx);
this.nonce = txObject.nonce;
return super.populateTransaction(txObject);
}
}
export class TornadoVoidSigner extends VoidSigner {
nonce?: number | null;
gasPriceBump: number;
gasLimitBump: number;
gasFailover: boolean;
bumpNonce: boolean;
constructor(
address: string,
provider?: null | Provider,
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {},
) {
super(address, provider);
// 10% bump from the recommended fee
this.gasPriceBump = gasPriceBump ?? 1000;
// 30% bump from the recommended gaslimit
this.gasLimitBump = gasLimitBump ?? 3000;
this.gasFailover = gasFailover ?? false;
// turn off bumpNonce feature for view only wallet
this.bumpNonce = bumpNonce ?? false;
}
async populateTransaction(tx: TransactionRequest) {
const txObject = await populateTransaction(this, tx);
this.nonce = txObject.nonce;
return super.populateTransaction(txObject);
}
}
export class TornadoRpcSigner extends JsonRpcSigner {
nonce?: number | null;
gasPriceBump: number;
gasLimitBump: number;
gasFailover: boolean;
bumpNonce: boolean;
constructor(
provider: JsonRpcApiProvider,
address: string,
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {},
) {
super(provider, address);
// 10% bump from the recommended fee
this.gasPriceBump = gasPriceBump ?? 1000;
// 30% bump from the recommended gaslimit
this.gasLimitBump = gasLimitBump ?? 3000;
this.gasFailover = gasFailover ?? false;
// turn off bumpNonce feature for browser wallet
this.bumpNonce = bumpNonce ?? false;
}
async sendUncheckedTransaction(tx: TransactionRequest) {
return super.sendUncheckedTransaction(await populateTransaction(this, tx));
}
}
/* eslint-disable @typescript-eslint/no-explicit-any */
export type connectWalletFunc = (...args: any[]) => Promise<void>;
export type handleWalletFunc = (...args: any[]) => void;
/* eslint-enable @typescript-eslint/no-explicit-any */
export type TornadoBrowserProviderOptions = TornadoWalletOptions & {
webChainId?: BigNumberish;
connectWallet?: connectWalletFunc;
handleNetworkChanges?: handleWalletFunc;
handleAccountChanges?: handleWalletFunc;
handleAccountDisconnect?: handleWalletFunc;
};
export class TornadoBrowserProvider extends BrowserProvider {
options?: TornadoBrowserProviderOptions;
constructor(ethereum: Eip1193Provider, network?: Networkish, options?: TornadoBrowserProviderOptions) {
super(ethereum, network);
this.options = options;
}
async getSigner(address: string): Promise<TornadoRpcSigner> {
const signerAddress = (await super.getSigner(address)).address;
if (
this.options?.webChainId &&
this.options?.connectWallet &&
Number(await super.send('eth_chainId', [])) !== Number(this.options?.webChainId)
) {
await this.options.connectWallet();
}
if (this.options?.handleNetworkChanges) {
window?.ethereum?.on('chainChanged', this.options.handleNetworkChanges);
}
if (this.options?.handleAccountChanges) {
window?.ethereum?.on('accountsChanged', this.options.handleAccountChanges);
}
if (this.options?.handleAccountDisconnect) {
window?.ethereum?.on('disconnect', this.options.handleAccountDisconnect);
}
return new TornadoRpcSigner(this, signerAddress, this.options);
}
}

@ -0,0 +1,412 @@
import { namehash, parseEther } from 'ethers';
import type { Aggregator } from '@tornado/contracts';
import type { RelayerStructOutput } from '@tornado/contracts/dist/contracts/Governance/Aggregator/Aggregator';
import { sleep } from './utils';
import type { Config } from './networkConfig';
import { fetchData, fetchDataOptions } from './providers';
import { ajv, jobsSchema, getStatusSchema } from './schemas';
import type { snarkProofs } from './websnark';
export const MIN_STAKE_BALANCE = parseEther('500');
export interface RelayerParams {
ensName: string;
relayerAddress?: string;
}
export interface Relayer {
netId: number;
url: string;
rewardAccount: string;
currentQueue: number;
tornadoServiceFee: number;
}
export type RelayerInfo = Relayer & {
hostname: string;
ensName: string;
stakeBalance: bigint;
relayerAddress: string;
ethPrices?: {
[key in string]: string;
};
};
export type RelayerError = {
hostname: string;
relayerAddress?: string;
errorMessage?: string;
};
export interface RelayerStatus {
url: string;
rewardAccount: string;
instances: {
[key in string]: {
instanceAddress: {
[key in string]: string;
};
tokenAddress?: string;
symbol: string;
decimals: number;
};
};
gasPrices?: {
fast: number;
additionalProperties?: number;
};
netId: number;
ethPrices?: {
[key in string]: string;
};
tornadoServiceFee: number;
latestBlock?: number;
version: string;
health: {
status: string;
error: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorsLog: any[];
};
currentQueue: number;
}
export interface RelayerTornadoWithdraw {
id?: string;
error?: string;
}
export interface RelayerTornadoJobs {
error?: string;
id: string;
type?: string;
status: string;
contract?: string;
proof?: string;
args?: string[];
txHash?: string;
confirmations?: number;
failedReason?: string;
}
const semVerRegex =
/^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
export interface semanticVersion {
major: string;
minor: string;
patch: string;
prerelease?: string;
buildmetadata?: string;
}
export function parseSemanticVersion(version: string) {
const { groups } = semVerRegex.exec(version) as RegExpExecArray;
return groups as unknown as semanticVersion;
}
export function isRelayerUpdated(relayerVersion: string, netId: number | string) {
const { major, patch, prerelease } = parseSemanticVersion(relayerVersion);
// Save backwards compatibility with V4 relayers for Ethereum Mainnet
const requiredMajor = netId === 1 ? '4' : '5';
const isUpdatedMajor = major === requiredMajor;
if (prerelease) return false;
return isUpdatedMajor && (Number(patch) >= 5 || Number(netId) !== 1); // Patch checking - also backwards compatibility for Mainnet
}
export function calculateScore({ stakeBalance, tornadoServiceFee }: RelayerInfo, minFee = 0.33, maxFee = 0.53) {
if (tornadoServiceFee < minFee) {
tornadoServiceFee = minFee;
} else if (tornadoServiceFee >= maxFee) {
return BigInt(0);
}
const serviceFeeCoefficient = (tornadoServiceFee - minFee) ** 2;
const feeDiffCoefficient = 1 / (maxFee - minFee) ** 2;
const coefficientsMultiplier = 1 - feeDiffCoefficient * serviceFeeCoefficient;
return BigInt(Math.floor(Number(stakeBalance) * coefficientsMultiplier));
}
export function getWeightRandom(weightsScores: bigint[], random: bigint) {
for (let i = 0; i < weightsScores.length; i++) {
if (random < weightsScores[i]) {
return i;
}
random = random - weightsScores[i];
}
return Math.floor(Math.random() * weightsScores.length);
}
export function pickWeightedRandomRelayer(relayers: RelayerInfo[], netId: string | number) {
let minFee: number, maxFee: number;
if (Number(netId) !== 1) {
minFee = 0.01;
maxFee = 0.3;
}
const weightsScores = relayers.map((el) => calculateScore(el, minFee, maxFee));
const totalWeight = weightsScores.reduce((acc, curr) => {
return (acc = acc + curr);
}, BigInt('0'));
const random = BigInt(Number(totalWeight) * Math.random());
const weightRandomIndex = getWeightRandom(weightsScores, random);
return relayers[weightRandomIndex];
}
export interface RelayerClientConstructor {
netId: number | string;
config: Config;
Aggregator: Aggregator;
fetchDataOptions?: fetchDataOptions;
}
export type RelayerClientWithdraw = snarkProofs & {
contract: string;
};
export class RelayerClient {
netId: number;
config: Config;
Aggregator: Aggregator;
selectedRelayer?: Relayer;
fetchDataOptions?: fetchDataOptions;
constructor({ netId, config, Aggregator, fetchDataOptions }: RelayerClientConstructor) {
this.netId = Number(netId);
this.config = config;
this.Aggregator = Aggregator;
this.fetchDataOptions = fetchDataOptions;
}
async askRelayerStatus({
hostname,
relayerAddress,
}: {
hostname: string;
relayerAddress?: string;
}): Promise<RelayerStatus> {
const url = `https://${!hostname.endsWith('/') ? hostname + '/' : hostname}`;
const rawStatus = (await fetchData(`${url}status`, {
...this.fetchDataOptions,
headers: {
'Content-Type': 'application/json, application/x-www-form-urlencoded',
},
timeout: this.fetchDataOptions?.torPort ? 10000 : 3000,
maxRetry: this.fetchDataOptions?.torPort ? 2 : 0,
})) as object;
const statusValidator = ajv.compile(getStatusSchema(this.netId, this.config));
if (!statusValidator(rawStatus)) {
throw new Error('Invalid status schema');
}
const status = {
...rawStatus,
url,
} as RelayerStatus;
if (status.currentQueue > 5) {
throw new Error('Withdrawal queue is overloaded');
}
if (status.netId !== this.netId) {
throw new Error('This relayer serves a different network');
}
if (relayerAddress && this.netId === 1 && status.rewardAccount !== relayerAddress) {
throw new Error('The Relayer reward address must match registered address');
}
if (!isRelayerUpdated(status.version, this.netId)) {
throw new Error('Outdated version.');
}
return status;
}
async filterRelayer(
curr: RelayerStructOutput,
relayer: RelayerParams,
subdomains: string[],
debugRelayer: boolean = false,
): Promise<RelayerInfo | RelayerError> {
const { ensSubdomainKey } = this.config;
const subdomainIndex = subdomains.indexOf(ensSubdomainKey);
const mainnetSubdomain = curr.records[0];
const hostname = curr.records[subdomainIndex];
const isHostWithProtocol = hostname.includes('http');
const { owner, balance: stakeBalance, isRegistered } = curr;
const { ensName, relayerAddress } = relayer;
const isOwner = !relayerAddress || relayerAddress === owner;
const hasMinBalance = stakeBalance >= MIN_STAKE_BALANCE;
const preCondition =
hostname && isOwner && mainnetSubdomain && isRegistered && hasMinBalance && !isHostWithProtocol;
if (preCondition || debugRelayer) {
try {
const status = await this.askRelayerStatus({ hostname, relayerAddress });
return {
netId: status.netId,
url: status.url,
hostname,
ensName,
stakeBalance,
relayerAddress,
rewardAccount: status.rewardAccount,
ethPrices: status.ethPrices,
currentQueue: status.currentQueue,
tornadoServiceFee: status.tornadoServiceFee,
} as RelayerInfo;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
if (debugRelayer) {
throw err;
}
return {
hostname,
relayerAddress,
errorMessage: err.message,
} as RelayerError;
}
} else {
if (debugRelayer) {
const errMsg = `Relayer ${hostname} condition not met`;
throw new Error(errMsg);
}
return {
hostname,
relayerAddress,
errorMessage: `Relayer ${hostname} condition not met`,
};
}
}
async getValidRelayers(
// this should be ascending order of events
relayers: RelayerParams[],
subdomains: string[],
debugRelayer: boolean = false,
): Promise<{
validRelayers: RelayerInfo[];
invalidRelayers: RelayerError[];
}> {
const relayersSet = new Set();
const uniqueRelayers = relayers.reverse().filter(({ ensName }) => {
if (!relayersSet.has(ensName)) {
relayersSet.add(ensName);
return true;
}
return false;
});
const relayerNameHashes = uniqueRelayers.map((r) => namehash(r.ensName));
const relayersData = await this.Aggregator.relayersData.staticCall(relayerNameHashes, subdomains);
const invalidRelayers: RelayerError[] = [];
const validRelayers = (
await Promise.all(
relayersData.map((curr, index) => this.filterRelayer(curr, uniqueRelayers[index], subdomains, debugRelayer)),
)
).filter((r) => {
if ((r as RelayerError).errorMessage) {
invalidRelayers.push(r);
return false;
}
return true;
}) as RelayerInfo[];
return {
validRelayers,
invalidRelayers,
};
}
pickWeightedRandomRelayer(relayers: RelayerInfo[]) {
return pickWeightedRandomRelayer(relayers, this.netId);
}
async tornadoWithdraw({ contract, proof, args }: RelayerClientWithdraw) {
const { url } = this.selectedRelayer as Relayer;
const withdrawResponse = (await fetchData(`${url}v1/tornadoWithdraw`, {
...this.fetchDataOptions,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
contract,
proof,
args,
}),
})) as RelayerTornadoWithdraw;
const { id, error } = withdrawResponse;
if (error) {
throw new Error(error);
}
let relayerStatus: string | undefined;
const jobUrl = `${url}v1/jobs/${id}`;
console.log(`Job submitted: ${jobUrl}\n`);
while (!relayerStatus || !['FAILED', 'CONFIRMED'].includes(relayerStatus)) {
const jobResponse = await fetchData(jobUrl, {
...this.fetchDataOptions,
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (jobResponse.error) {
throw new Error(error);
}
const jobValidator = ajv.compile(jobsSchema);
if (!jobValidator(jobResponse)) {
const errMsg = `${jobUrl} has an invalid job response`;
throw new Error(errMsg);
}
const { status, txHash, confirmations, failedReason } = jobResponse as unknown as RelayerTornadoJobs;
if (relayerStatus !== status) {
if (status === 'FAILED') {
const errMsg = `Job ${status}: ${jobUrl} failed reason: ${failedReason}`;
throw new Error(errMsg);
} else if (status === 'SENT') {
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}\n`);
} else if (status === 'MINED') {
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`);
} else if (status === 'CONFIRMED') {
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`);
} else {
console.log(`Job ${status}: ${jobUrl}\n`);
}
relayerStatus = status;
}
await sleep(3000);
}
}
}

@ -0,0 +1,21 @@
import Ajv from 'ajv';
import type { BigNumberish } from 'ethers';
export const ajv = new Ajv({ allErrors: true });
ajv.addKeyword({
keyword: 'BN',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
validate: (schema: any, data: BigNumberish) => {
try {
BigInt(data);
return true;
} catch (e) {
return false;
}
},
errors: true,
});
export * from './status';
export * from './jobs';

@ -0,0 +1,59 @@
export type jobsSchema = {
type: string;
properties: {
error: {
type: string;
};
id: {
type: string;
};
type: {
type: string;
};
status: {
type: string;
};
contract: {
type: string;
};
proof: {
type: string;
};
args: {
type: string;
items: {
type: string;
};
};
txHash: {
type: string;
};
confirmations: {
type: string;
};
failedReason: {
type: string;
};
};
required: string[];
};
export const jobsSchema: jobsSchema = {
type: 'object',
properties: {
error: { type: 'string' },
id: { type: 'string' },
type: { type: 'string' },
status: { type: 'string' },
contract: { type: 'string' },
proof: { type: 'string' },
args: {
type: 'array',
items: { type: 'string' },
},
txHash: { type: 'string' },
confirmations: { type: 'number' },
failedReason: { type: 'string' },
},
required: ['id', 'status'],
};

@ -0,0 +1,181 @@
import type { Config } from '../networkConfig';
export type statusInstanceType = {
type: string;
properties: {
instanceAddress: {
type: string;
properties: {
[key in string]: typeof addressType;
};
required: string[];
};
tokenAddress?: typeof addressType;
symbol?: { enum: string[] };
decimals: { enum: number[] };
};
required: string[];
};
export type statusInstancesType = {
type: string;
properties: {
[key in string]: statusInstanceType;
};
required: string[];
};
export type statusEthPricesType = {
type: string;
properties: {
[key in string]: typeof bnType;
};
required?: string[];
};
export type statusSchema = {
type: string;
properties: {
rewardAccount: typeof addressType;
instances?: statusInstancesType;
gasPrices: {
type: string;
properties: {
[key in string]: {
type: string;
};
};
required: string[];
};
netId: {
type: string;
};
ethPrices?: statusEthPricesType;
tornadoServiceFee?: {
type: string;
maximum: number;
minimum: number;
};
latestBlock?: {
type: string;
};
version: {
type: string;
};
health: {
type: string;
properties: {
status: { const: string };
error: { type: string };
};
required: string[];
};
currentQueue: {
type: string;
};
};
required: string[];
};
const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' };
const bnType = { type: 'string', BN: true };
const statusSchema: statusSchema = {
type: 'object',
properties: {
rewardAccount: addressType,
gasPrices: {
type: 'object',
properties: {
fast: { type: 'number' },
additionalProperties: { type: 'number' },
},
required: ['fast'],
},
netId: { type: 'integer' },
tornadoServiceFee: { type: 'number', maximum: 20, minimum: 0 },
latestBlock: { type: 'number' },
version: { type: 'string' },
health: {
type: 'object',
properties: {
status: { const: 'true' },
error: { type: 'string' },
},
required: ['status'],
},
currentQueue: { type: 'number' },
},
required: ['rewardAccount', 'instances', 'netId', 'tornadoServiceFee', 'version', 'health'],
};
export function getStatusSchema(netId: number | string, config: Config) {
const { tokens, optionalTokens = [], nativeCurrency } = config;
// deep copy schema
const schema = JSON.parse(JSON.stringify(statusSchema)) as statusSchema;
const instances = Object.keys(tokens).reduce(
(acc: statusInstancesType, token) => {
const { instanceAddress, tokenAddress, symbol, decimals, optionalInstances = [] } = tokens[token];
const amounts = Object.keys(instanceAddress);
const instanceProperties: statusInstanceType = {
type: 'object',
properties: {
instanceAddress: {
type: 'object',
properties: amounts.reduce((acc: { [key in string]: typeof addressType }, cur) => {
acc[cur] = addressType;
return acc;
}, {}),
required: amounts.filter((amount) => !optionalInstances.includes(amount)),
},
decimals: { enum: [decimals] },
},
required: ['instanceAddress', 'decimals'].concat(
tokenAddress ? ['tokenAddress'] : [],
symbol ? ['symbol'] : [],
),
};
if (tokenAddress) {
instanceProperties.properties.tokenAddress = addressType;
}
if (symbol) {
instanceProperties.properties.symbol = { enum: [symbol] };
}
acc.properties[token] = instanceProperties;
if (!optionalTokens.includes(token)) {
acc.required.push(token);
}
return acc;
},
{
type: 'object',
properties: {},
required: [],
},
);
schema.properties.instances = instances;
if (Number(netId) === 1) {
const _tokens = Object.keys(tokens).filter((t) => t !== nativeCurrency);
const ethPrices: statusEthPricesType = {
type: 'object',
properties: _tokens.reduce((acc: { [key in string]: typeof bnType }, token: string) => {
acc[token] = bnType;
return acc;
}, {}),
// required: _tokens
};
schema.properties.ethPrices = ethPrices;
// schema.required.push('ethPrices')
}
return schema;
}

90
src/services/tokens.ts Normal file

@ -0,0 +1,90 @@
import { Provider, ZeroAddress } from 'ethers';
import { ERC20__factory, Multicall } from '../typechain';
import { chunk } from './utils';
import { Call3, multicall } from './multicall';
export interface tokenBalances {
address: string;
name: string;
symbol: string;
decimals: number;
balance: bigint;
}
export async function getTokenBalances({
provider,
Multicall,
currencyName,
userAddress,
tokenAddresses = [],
}: {
provider: Provider;
Multicall: Multicall;
currencyName: string;
userAddress: string;
tokenAddresses: string[];
}): Promise<tokenBalances[]> {
const tokenCalls = tokenAddresses
.map((tokenAddress) => {
const Token = ERC20__factory.connect(tokenAddress, provider);
return [
{
contract: Token,
name: 'balanceOf',
params: [userAddress],
},
{
contract: Token,
name: 'name',
},
{
contract: Token,
name: 'symbol',
},
{
contract: Token,
name: 'decimals',
},
];
})
.flat() as Call3[];
const multicallResults = await multicall(Multicall, [
{
contract: Multicall,
name: 'getEthBalance',
params: [userAddress],
},
...(tokenCalls.length ? tokenCalls : []),
]);
const ethResults = multicallResults[0];
const tokenResults = multicallResults.slice(1).length
? chunk(multicallResults.slice(1), tokenCalls.length / tokenAddresses.length)
: [];
const tokenBalances = tokenResults.map((tokenResult, index) => {
const [tokenBalance, tokenName, tokenSymbol, tokenDecimals] = tokenResult;
const tokenAddress = tokenAddresses[index];
return {
address: tokenAddress,
name: tokenName,
symbol: tokenSymbol,
decimals: Number(tokenDecimals),
balance: tokenBalance,
};
});
return [
{
address: ZeroAddress,
name: currencyName,
symbol: currencyName,
decimals: 18,
balance: ethResults,
},
...tokenBalances,
];
}

131
src/services/utils.ts Normal file

@ -0,0 +1,131 @@
import { URL } from 'url';
import BN from 'bn.js';
import type { BigNumberish } from 'ethers';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
type bnInput = number | string | number[] | Uint8Array | Buffer | BN;
export const isNode =
!(
process as typeof process & {
browser?: boolean;
}
).browser && typeof globalThis.window === 'undefined';
export const chunk = <T>(arr: T[], size: number): T[][] =>
[...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(size * i, size + size * i));
export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function validateUrl(url: string, protocols?: string[]) {
try {
const parsedUrl = new URL(url);
if (protocols && protocols.length) {
return protocols.map((p) => p.toLowerCase()).includes(parsedUrl.protocol);
}
return true;
} catch {
return false;
}
}
export function bufferToBytes(b: Buffer) {
return new Uint8Array(b.buffer);
}
export function bytesToBase64(bytes: Uint8Array) {
let binary = '';
const len = bytes.byteLength;
for (let i = 0; i < len; ++i) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
export function base64ToBytes(base64: string) {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
export function bytesToHex(bytes: Uint8Array) {
return (
'0x' +
Array.from(bytes)
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
);
}
// Convert BE encoded bytes (Buffer | Uint8Array) array to BigInt
export function bytesToBN(bytes: Uint8Array) {
return BigInt(bytesToHex(bytes));
}
// Convert BigInt to BE encoded Uint8Array type
export function bnToBytes(bigint: bigint | string) {
// Parse bigint to hex string
let hexString: string = typeof bigint === 'bigint' ? bigint.toString(16) : bigint;
// Remove hex string prefix if exists
if (hexString.startsWith('0x')) {
hexString = hexString.replace('0x', '');
}
// Hex string length should be a multiplier of two (To make correct bytes)
if (hexString.length % 2 !== 0) {
hexString = '0' + hexString;
}
return Uint8Array.from((hexString.match(/.{1,2}/g) as string[]).map((byte) => parseInt(byte, 16)));
}
// Convert LE encoded bytes (Buffer | Uint8Array) array to BigInt
export function leBuff2Int(bytes: Uint8Array) {
return new BN(bytes, 16, 'le');
}
// Convert BigInt to LE encoded Uint8Array type
export function leInt2Buff(bigint: bnInput | bigint) {
return Uint8Array.from(new BN(bigint as bnInput).toArray('le', 31));
}
// Inherited from tornado-core and tornado-cli
export function toFixedHex(numberish: BigNumberish, length = 32) {
return (
'0x' +
BigInt(numberish)
.toString(16)
.padStart(length * 2, '0')
);
}
export function toFixedLength(string: string, length: number = 32) {
string = string.replace('0x', '');
return '0x' + string.padStart(length * 2, '0');
}
// Random BigInt in a range of bytes
export function rBigInt(nbytes: number = 31) {
return bytesToBN(crypto.getRandomValues(new Uint8Array(nbytes)));
}
// Used for JSON.stringify(value, bigIntReplacer, space)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function bigIntReplacer(key: any, value: any) {
return typeof value === 'bigint' ? value.toString() : value;
}
export function substring(str: string, length: number = 10) {
if (str.length < length * 2) {
return str;
}
return `${str.substring(0, length)}...${str.substring(str.length - length)}`;
}

85
src/services/websnark.ts Normal file

@ -0,0 +1,85 @@
// @ts-expect-error no-websnark-types
import * as websnarkUtils from '@tornado/websnark/src/utils';
// @ts-expect-error no-websnark-types
import websnarkGroth from '@tornado/websnark/src/groth16';
import type { Element } from '@tornado/fixed-merkle-tree';
import type { AddressLike, BytesLike, BigNumberish } from 'ethers';
import { toFixedHex } from './utils';
export type snarkInputs = {
// Public snark inputs
root: Element;
nullifierHex: string;
recipient: AddressLike;
relayer: AddressLike;
fee: bigint;
refund: bigint;
// Private snark inputs
nullifier: bigint;
secret: bigint;
pathElements: Element[];
pathIndices: Element[];
};
export type snarkArgs = [
_root: BytesLike,
_nullifierHash: BytesLike,
_recipient: AddressLike,
_relayer: AddressLike,
_fee: BigNumberish,
_refund: BigNumberish,
];
export type snarkProofs = {
proof: BytesLike;
args: snarkArgs;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let groth16: any;
const groth16Promise = (async () => {
if (!groth16) {
groth16 = await websnarkGroth({ wasmInitialMemory: 2000 });
}
})();
export async function calculateSnarkProof(
input: snarkInputs,
circuit: object,
provingKey: ArrayBuffer,
): Promise<snarkProofs> {
await groth16Promise;
const snarkInput = {
root: input.root,
nullifierHash: BigInt(input.nullifierHex).toString(),
recipient: BigInt(input.recipient as string),
relayer: BigInt(input.relayer as string),
fee: input.fee,
refund: input.refund,
nullifier: input.nullifier,
secret: input.secret,
pathElements: input.pathElements,
pathIndices: input.pathIndices,
};
console.log('Start generating SNARK proof', snarkInput);
console.time('SNARK proof time');
const proofData = await websnarkUtils.genWitnessAndProve(groth16, snarkInput, circuit, provingKey);
const proof = websnarkUtils.toSolidityInput(proofData).proof as BytesLike;
console.timeEnd('SNARK proof time');
const args = [
toFixedHex(input.root, 32) as BytesLike,
toFixedHex(input.nullifierHex, 32) as BytesLike,
input.recipient,
input.relayer,
toFixedHex(input.fee, 32) as BigNumberish,
toFixedHex(input.refund, 32) as BigNumberish,
] as snarkArgs;
return { proof, args };
}

@ -42,7 +42,7 @@
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
"resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
@ -108,5 +108,6 @@
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
},
"include": ["./src/**/*"]
}