diff --git a/.env.example b/.env.example
index a36f606..866928d 100644
--- a/.env.example
+++ b/.env.example
@@ -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
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
index c3ad939..f1e2882 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -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"]
}
}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..a4105e6
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+dist/* linguist-vendored
+static/* linguist-vendored
\ No newline at end of file
diff --git a/README.md b/README.md
index 53bfd63..ffdc8b2 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,38 @@
-# Tornado-CLI
+
-Command line tool to interact with [Tornado Cash](https://tornado.ws).
+
-### 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
+
-- 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 ` 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 ` 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 --rpc --tor --private-key
+$ yarn deposit
```
-Note that `--tor ` is optional, and use `--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 --rpc --relayer --tor --private-key
+$ yarn withdraw
```
-Note that `--relayer `, `--tor ` and `--rpc ` are optional parameters, and use `--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 `, `--tor-port ` and `--rpc-url ` are optional parameters, and use `--wallet-withdrawal --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
+$ yarn createDeposit
```
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 --rpc --tor
+$ node cli.js depositInvoice
```
-Parameter `--rpc ` is optional, if you don't provide it, default RPC (corresponding to note chain) will be used.
+Parameter `--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.
\ No newline at end of file
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..7987929
Binary files /dev/null and b/logo.png differ
diff --git a/logo2.png b/logo2.png
new file mode 100644
index 0000000..40e33e1
Binary files /dev/null and b/logo2.png differ
diff --git a/package.json b/package.json
index a6c2b88..853fa4e 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/rollup.config.mjs b/rollup.config.mjs
index b1ed6fc..98e4b34 100644
--- a/rollup.config.mjs
+++ b/rollup.config.mjs
@@ -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'
+ })
+ ],
}
]
diff --git a/scripts/figlet-font.ts b/scripts/figlet-font.ts
new file mode 100644
index 0000000..27a9f38
--- /dev/null
+++ b/scripts/figlet-font.ts
@@ -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;
+`)
\ No newline at end of file
diff --git a/src/cli.ts b/src/cli.ts
new file mode 100644
index 0000000..b341924
--- /dev/null
+++ b/src/cli.ts
@@ -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();
diff --git a/src/fonts/figletStandard.ts b/src/fonts/figletStandard.ts
new file mode 100644
index 0000000..85d7393
--- /dev/null
+++ b/src/fonts/figletStandard.ts
@@ -0,0 +1,2238 @@
+export const figletStandard: string = `flf2a$ 6 5 16 15 13 0 24463 229
+Standard by Glenn Chappell & Ian Chai 3/93 -- based on Frank's .sig
+Includes ISO Latin-1
+figlet release 2.1 -- 12 Aug 1994
+Modified for figlet 2.2 by John Cowan
+ to add Latin-{2,3,4,5} support (Unicode U+0100-017F).
+Permission is hereby given to modify this font, as long as the
+modifier's name is placed on a comment line.
+
+Modified by Paul Burton 12/96 to include new parameter
+supported by FIGlet and FIGWin. May also be slightly modified for better use
+of new full-width/kern/smush alternatives, but default output is NOT changed.
+
+Font modified May 20, 2012 by patorjk to add the 0xCA0 character
+ $@
+ $@
+ $@
+ $@
+ $@
+ $@@
+ _ @
+ | |@
+ | |@
+ |_|@
+ (_)@
+ @@
+ _ _ @
+ ( | )@
+ V V @
+ $ @
+ $ @
+ @@
+ _ _ @
+ _| || |_ @
+ |_ .. _|@
+ |_ _|@
+ |_||_| @
+ @@
+ _ @
+ | | @
+ / __)@
+ \\__ \\@
+ ( /@
+ |_| @@
+ _ __@
+ (_)/ /@
+ / / @
+ / /_ @
+ /_/(_)@
+ @@
+ ___ @
+ ( _ ) @
+ / _ \\/\\@
+ | (_> <@
+ \\___/\\/@
+ @@
+ _ @
+ ( )@
+ |/ @
+ $ @
+ $ @
+ @@
+ __@
+ / /@
+ | | @
+ | | @
+ | | @
+ \\_\\@@
+ __ @
+ \\ \\ @
+ | |@
+ | |@
+ | |@
+ /_/ @@
+ @
+ __/\\__@
+ \\ /@
+ /_ _\\@
+ \\/ @
+ @@
+ @
+ _ @
+ _| |_ @
+ |_ _|@
+ |_| @
+ @@
+ @
+ @
+ @
+ _ @
+ ( )@
+ |/ @@
+ @
+ @
+ _____ @
+ |_____|@
+ $ @
+ @@
+ @
+ @
+ @
+ _ @
+ (_)@
+ @@
+ __@
+ / /@
+ / / @
+ / / @
+ /_/ @
+ @@
+ ___ @
+ / _ \\ @
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+ _ @
+ / |@
+ | |@
+ | |@
+ |_|@
+ @@
+ ____ @
+ |___ \\ @
+ __) |@
+ / __/ @
+ |_____|@
+ @@
+ _____ @
+ |___ / @
+ |_ \\ @
+ ___) |@
+ |____/ @
+ @@
+ _ _ @
+ | || | @
+ | || |_ @
+ |__ _|@
+ |_| @
+ @@
+ ____ @
+ | ___| @
+ |___ \\ @
+ ___) |@
+ |____/ @
+ @@
+ __ @
+ / /_ @
+ | '_ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+ _____ @
+ |___ |@
+ / / @
+ / / @
+ /_/ @
+ @@
+ ___ @
+ ( _ ) @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+ ___ @
+ / _ \\ @
+ | (_) |@
+ \\__, |@
+ /_/ @
+ @@
+ @
+ _ @
+ (_)@
+ _ @
+ (_)@
+ @@
+ @
+ _ @
+ (_)@
+ _ @
+ ( )@
+ |/ @@
+ __@
+ / /@
+ / / @
+ \\ \\ @
+ \\_\\@
+ @@
+ @
+ _____ @
+ |_____|@
+ |_____|@
+ $ @
+ @@
+ __ @
+ \\ \\ @
+ \\ \\@
+ / /@
+ /_/ @
+ @@
+ ___ @
+ |__ \\@
+ / /@
+ |_| @
+ (_) @
+ @@
+ ____ @
+ / __ \\ @
+ / / _\` |@
+ | | (_| |@
+ \\ \\__,_|@
+ \\____/ @@
+ _ @
+ / \\ @
+ / _ \\ @
+ / ___ \\ @
+ /_/ \\_\\@
+ @@
+ ____ @
+ | __ ) @
+ | _ \\ @
+ | |_) |@
+ |____/ @
+ @@
+ ____ @
+ / ___|@
+ | | @
+ | |___ @
+ \\____|@
+ @@
+ ____ @
+ | _ \\ @
+ | | | |@
+ | |_| |@
+ |____/ @
+ @@
+ _____ @
+ | ____|@
+ | _| @
+ | |___ @
+ |_____|@
+ @@
+ _____ @
+ | ___|@
+ | |_ @
+ | _| @
+ |_| @
+ @@
+ ____ @
+ / ___|@
+ | | _ @
+ | |_| |@
+ \\____|@
+ @@
+ _ _ @
+ | | | |@
+ | |_| |@
+ | _ |@
+ |_| |_|@
+ @@
+ ___ @
+ |_ _|@
+ | | @
+ | | @
+ |___|@
+ @@
+ _ @
+ | |@
+ _ | |@
+ | |_| |@
+ \\___/ @
+ @@
+ _ __@
+ | |/ /@
+ | ' / @
+ | . \\ @
+ |_|\\_\\@
+ @@
+ _ @
+ | | @
+ | | @
+ | |___ @
+ |_____|@
+ @@
+ __ __ @
+ | \\/ |@
+ | |\\/| |@
+ | | | |@
+ |_| |_|@
+ @@
+ _ _ @
+ | \\ | |@
+ | \\| |@
+ | |\\ |@
+ |_| \\_|@
+ @@
+ ___ @
+ / _ \\ @
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+ ____ @
+ | _ \\ @
+ | |_) |@
+ | __/ @
+ |_| @
+ @@
+ ___ @
+ / _ \\ @
+ | | | |@
+ | |_| |@
+ \\__\\_\\@
+ @@
+ ____ @
+ | _ \\ @
+ | |_) |@
+ | _ < @
+ |_| \\_\\@
+ @@
+ ____ @
+ / ___| @
+ \\___ \\ @
+ ___) |@
+ |____/ @
+ @@
+ _____ @
+ |_ _|@
+ | | @
+ | | @
+ |_| @
+ @@
+ _ _ @
+ | | | |@
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+ __ __@
+ \\ \\ / /@
+ \\ \\ / / @
+ \\ V / @
+ \\_/ @
+ @@
+ __ __@
+ \\ \\ / /@
+ \\ \\ /\\ / / @
+ \\ V V / @
+ \\_/\\_/ @
+ @@
+ __ __@
+ \\ \\/ /@
+ \\ / @
+ / \\ @
+ /_/\\_\\@
+ @@
+ __ __@
+ \\ \\ / /@
+ \\ V / @
+ | | @
+ |_| @
+ @@
+ _____@
+ |__ /@
+ / / @
+ / /_ @
+ /____|@
+ @@
+ __ @
+ | _|@
+ | | @
+ | | @
+ | | @
+ |__|@@
+ __ @
+ \\ \\ @
+ \\ \\ @
+ \\ \\ @
+ \\_\\@
+ @@
+ __ @
+ |_ |@
+ | |@
+ | |@
+ | |@
+ |__|@@
+ /\\ @
+ |/\\|@
+ $ @
+ $ @
+ $ @
+ @@
+ @
+ @
+ @
+ @
+ _____ @
+ |_____|@@
+ _ @
+ ( )@
+ \\|@
+ $ @
+ $ @
+ @@
+ @
+ __ _ @
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+ _ @
+ | |__ @
+ | '_ \\ @
+ | |_) |@
+ |_.__/ @
+ @@
+ @
+ ___ @
+ / __|@
+ | (__ @
+ \\___|@
+ @@
+ _ @
+ __| |@
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+ @
+ ___ @
+ / _ \\@
+ | __/@
+ \\___|@
+ @@
+ __ @
+ / _|@
+ | |_ @
+ | _|@
+ |_| @
+ @@
+ @
+ __ _ @
+ / _\` |@
+ | (_| |@
+ \\__, |@
+ |___/ @@
+ _ @
+ | |__ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ @@
+ _ @
+ (_)@
+ | |@
+ | |@
+ |_|@
+ @@
+ _ @
+ (_)@
+ | |@
+ | |@
+ _/ |@
+ |__/ @@
+ _ @
+ | | __@
+ | |/ /@
+ | < @
+ |_|\\_\\@
+ @@
+ _ @
+ | |@
+ | |@
+ | |@
+ |_|@
+ @@
+ @
+ _ __ ___ @
+ | '_ \` _ \\ @
+ | | | | | |@
+ |_| |_| |_|@
+ @@
+ @
+ _ __ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ @@
+ @
+ ___ @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+ @
+ _ __ @
+ | '_ \\ @
+ | |_) |@
+ | .__/ @
+ |_| @@
+ @
+ __ _ @
+ / _\` |@
+ | (_| |@
+ \\__, |@
+ |_|@@
+ @
+ _ __ @
+ | '__|@
+ | | @
+ |_| @
+ @@
+ @
+ ___ @
+ / __|@
+ \\__ \\@
+ |___/@
+ @@
+ _ @
+ | |_ @
+ | __|@
+ | |_ @
+ \\__|@
+ @@
+ @
+ _ _ @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+ @
+ __ __@
+ \\ \\ / /@
+ \\ V / @
+ \\_/ @
+ @@
+ @
+ __ __@
+ \\ \\ /\\ / /@
+ \\ V V / @
+ \\_/\\_/ @
+ @@
+ @
+ __ __@
+ \\ \\/ /@
+ > < @
+ /_/\\_\\@
+ @@
+ @
+ _ _ @
+ | | | |@
+ | |_| |@
+ \\__, |@
+ |___/ @@
+ @
+ ____@
+ |_ /@
+ / / @
+ /___|@
+ @@
+ __@
+ / /@
+ | | @
+ < < @
+ | | @
+ \\_\\@@
+ _ @
+ | |@
+ | |@
+ | |@
+ | |@
+ |_|@@
+ __ @
+ \\ \\ @
+ | | @
+ > >@
+ | | @
+ /_/ @@
+ /\\/|@
+ |/\\/ @
+ $ @
+ $ @
+ $ @
+ @@
+ _ _ @
+ (_)_(_)@
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+ _ _ @
+ (_)_(_)@
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+ _ _ @
+ (_) (_)@
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+ _ _ @
+ (_)_(_)@
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+ _ _ @
+ (_)_(_)@
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+ _ _ @
+ (_) (_)@
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+ ___ @
+ / _ \\@
+ | |/ /@
+ | |\\ \\@
+ | ||_/@
+ |_| @@
+160 NO-BREAK SPACE
+ $@
+ $@
+ $@
+ $@
+ $@
+ $@@
+161 INVERTED EXCLAMATION MARK
+ _ @
+ (_)@
+ | |@
+ | |@
+ |_|@
+ @@
+162 CENT SIGN
+ _ @
+ | | @
+ / __)@
+ | (__ @
+ \\ )@
+ |_| @@
+163 POUND SIGN
+ ___ @
+ / ,_\\ @
+ _| |_ @
+ | |___ @
+ (_,____|@
+ @@
+164 CURRENCY SIGN
+ /\\___/\\@
+ \\ _ /@
+ | (_) |@
+ / ___ \\@
+ \\/ \\/@
+ @@
+165 YEN SIGN
+ __ __ @
+ \\ V / @
+ |__ __|@
+ |__ __|@
+ |_| @
+ @@
+166 BROKEN BAR
+ _ @
+ | |@
+ |_|@
+ _ @
+ | |@
+ |_|@@
+167 SECTION SIGN
+ __ @
+ _/ _)@
+ / \\ \\ @
+ \\ \\\\ \\@
+ \\ \\_/@
+ (__/ @@
+168 DIAERESIS
+ _ _ @
+ (_) (_)@
+ $ $ @
+ $ $ @
+ $ $ @
+ @@
+169 COPYRIGHT SIGN
+ _____ @
+ / ___ \\ @
+ / / __| \\ @
+ | | (__ |@
+ \\ \\___| / @
+ \\_____/ @@
+170 FEMININE ORDINAL INDICATOR
+ __ _ @
+ / _\` |@
+ \\__,_|@
+ |____|@
+ $ @
+ @@
+171 LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
+ ____@
+ / / /@
+ / / / @
+ \\ \\ \\ @
+ \\_\\_\\@
+ @@
+172 NOT SIGN
+ @
+ _____ @
+ |___ |@
+ |_|@
+ $ @
+ @@
+173 SOFT HYPHEN
+ @
+ @
+ ____ @
+ |____|@
+ $ @
+ @@
+174 REGISTERED SIGN
+ _____ @
+ / ___ \\ @
+ / | _ \\ \\ @
+ | | / |@
+ \\ |_|_\\ / @
+ \\_____/ @@
+175 MACRON
+ _____ @
+ |_____|@
+ $ @
+ $ @
+ $ @
+ @@
+176 DEGREE SIGN
+ __ @
+ / \\ @
+ | () |@
+ \\__/ @
+ $ @
+ @@
+177 PLUS-MINUS SIGN
+ _ @
+ _| |_ @
+ |_ _|@
+ _|_|_ @
+ |_____|@
+ @@
+178 SUPERSCRIPT TWO
+ ___ @
+ |_ )@
+ / / @
+ /___|@
+ $ @
+ @@
+179 SUPERSCRIPT THREE
+ ____@
+ |__ /@
+ |_ \\@
+ |___/@
+ $ @
+ @@
+180 ACUTE ACCENT
+ __@
+ /_/@
+ $ @
+ $ @
+ $ @
+ @@
+181 MICRO SIGN
+ @
+ _ _ @
+ | | | |@
+ | |_| |@
+ | ._,_|@
+ |_| @@
+182 PILCROW SIGN
+ _____ @
+ / |@
+ | (| | |@
+ \\__ | |@
+ |_|_|@
+ @@
+183 MIDDLE DOT
+ @
+ _ @
+ (_)@
+ $ @
+ $ @
+ @@
+184 CEDILLA
+ @
+ @
+ @
+ @
+ _ @
+ )_)@@
+185 SUPERSCRIPT ONE
+ _ @
+ / |@
+ | |@
+ |_|@
+ $ @
+ @@
+186 MASCULINE ORDINAL INDICATOR
+ ___ @
+ / _ \\@
+ \\___/@
+ |___|@
+ $ @
+ @@
+187 RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
+ ____ @
+ \\ \\ \\ @
+ \\ \\ \\@
+ / / /@
+ /_/_/ @
+ @@
+188 VULGAR FRACTION ONE QUARTER
+ _ __ @
+ / | / / _ @
+ | |/ / | | @
+ |_/ /|_ _|@
+ /_/ |_| @
+ @@
+189 VULGAR FRACTION ONE HALF
+ _ __ @
+ / | / /__ @
+ | |/ /_ )@
+ |_/ / / / @
+ /_/ /___|@
+ @@
+190 VULGAR FRACTION THREE QUARTERS
+ ____ __ @
+ |__ / / / _ @
+ |_ \\/ / | | @
+ |___/ /|_ _|@
+ /_/ |_| @
+ @@
+191 INVERTED QUESTION MARK
+ _ @
+ (_) @
+ | | @
+ / /_ @
+ \\___|@
+ @@
+192 LATIN CAPITAL LETTER A WITH GRAVE
+ __ @
+ \\_\\ @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+193 LATIN CAPITAL LETTER A WITH ACUTE
+ __ @
+ /_/ @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+194 LATIN CAPITAL LETTER A WITH CIRCUMFLEX
+ //\\ @
+ |/_\\| @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+195 LATIN CAPITAL LETTER A WITH TILDE
+ /\\/| @
+ |/\\/ @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+196 LATIN CAPITAL LETTER A WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+197 LATIN CAPITAL LETTER A WITH RING ABOVE
+ _ @
+ (o) @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+198 LATIN CAPITAL LETTER AE
+ ______ @
+ / ____|@
+ / _ _| @
+ / __ |___ @
+ /_/ |_____|@
+ @@
+199 LATIN CAPITAL LETTER C WITH CEDILLA
+ ____ @
+ / ___|@
+ | | @
+ | |___ @
+ \\____|@
+ )_) @@
+200 LATIN CAPITAL LETTER E WITH GRAVE
+ __ @
+ _\\_\\_ @
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+201 LATIN CAPITAL LETTER E WITH ACUTE
+ __ @
+ _/_/_ @
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+202 LATIN CAPITAL LETTER E WITH CIRCUMFLEX
+ //\\ @
+ |/_\\| @
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+203 LATIN CAPITAL LETTER E WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+204 LATIN CAPITAL LETTER I WITH GRAVE
+ __ @
+ \\_\\ @
+ |_ _|@
+ | | @
+ |___|@
+ @@
+205 LATIN CAPITAL LETTER I WITH ACUTE
+ __ @
+ /_/ @
+ |_ _|@
+ | | @
+ |___|@
+ @@
+206 LATIN CAPITAL LETTER I WITH CIRCUMFLEX
+ //\\ @
+ |/_\\|@
+ |_ _|@
+ | | @
+ |___|@
+ @@
+207 LATIN CAPITAL LETTER I WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ |_ _| @
+ | | @
+ |___| @
+ @@
+208 LATIN CAPITAL LETTER ETH
+ ____ @
+ | _ \\ @
+ _| |_| |@
+ |__ __| |@
+ |____/ @
+ @@
+209 LATIN CAPITAL LETTER N WITH TILDE
+ /\\/|@
+ |/\\/ @
+ | \\| |@
+ | .\` |@
+ |_|\\_|@
+ @@
+210 LATIN CAPITAL LETTER O WITH GRAVE
+ __ @
+ \\_\\ @
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+211 LATIN CAPITAL LETTER O WITH ACUTE
+ __ @
+ /_/ @
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+212 LATIN CAPITAL LETTER O WITH CIRCUMFLEX
+ //\\ @
+ |/_\\| @
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+213 LATIN CAPITAL LETTER O WITH TILDE
+ /\\/| @
+ |/\\/ @
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+214 LATIN CAPITAL LETTER O WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+215 MULTIPLICATION SIGN
+ @
+ @
+ /\\/\\@
+ > <@
+ \\/\\/@
+ @@
+216 LATIN CAPITAL LETTER O WITH STROKE
+ ____ @
+ / _// @
+ | |// |@
+ | //| |@
+ //__/ @
+ @@
+217 LATIN CAPITAL LETTER U WITH GRAVE
+ __ @
+ _\\_\\_ @
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+218 LATIN CAPITAL LETTER U WITH ACUTE
+ __ @
+ _/_/_ @
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+219 LATIN CAPITAL LETTER U WITH CIRCUMFLEX
+ //\\ @
+ |/ \\| @
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+220 LATIN CAPITAL LETTER U WITH DIAERESIS
+ _ _ @
+ (_) (_)@
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+221 LATIN CAPITAL LETTER Y WITH ACUTE
+ __ @
+ __/_/__@
+ \\ \\ / /@
+ \\ V / @
+ |_| @
+ @@
+222 LATIN CAPITAL LETTER THORN
+ _ @
+ | |___ @
+ | __ \\@
+ | ___/@
+ |_| @
+ @@
+223 LATIN SMALL LETTER SHARP S
+ ___ @
+ / _ \\@
+ | |/ /@
+ | |\\ \\@
+ | ||_/@
+ |_| @@
+224 LATIN SMALL LETTER A WITH GRAVE
+ __ @
+ \\_\\_ @
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+225 LATIN SMALL LETTER A WITH ACUTE
+ __ @
+ /_/_ @
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+226 LATIN SMALL LETTER A WITH CIRCUMFLEX
+ //\\ @
+ |/_\\| @
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+227 LATIN SMALL LETTER A WITH TILDE
+ /\\/| @
+ |/\\/_ @
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+228 LATIN SMALL LETTER A WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+229 LATIN SMALL LETTER A WITH RING ABOVE
+ __ @
+ (()) @
+ / _ '|@
+ | (_| |@
+ \\__,_|@
+ @@
+230 LATIN SMALL LETTER AE
+ @
+ __ ____ @
+ / _\` _ \\@
+ | (_| __/@
+ \\__,____|@
+ @@
+231 LATIN SMALL LETTER C WITH CEDILLA
+ @
+ ___ @
+ / __|@
+ | (__ @
+ \\___|@
+ )_) @@
+232 LATIN SMALL LETTER E WITH GRAVE
+ __ @
+ \\_\\ @
+ / _ \\@
+ | __/@
+ \\___|@
+ @@
+233 LATIN SMALL LETTER E WITH ACUTE
+ __ @
+ /_/ @
+ / _ \\@
+ | __/@
+ \\___|@
+ @@
+234 LATIN SMALL LETTER E WITH CIRCUMFLEX
+ //\\ @
+ |/_\\|@
+ / _ \\@
+ | __/@
+ \\___|@
+ @@
+235 LATIN SMALL LETTER E WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ / _ \\ @
+ | __/ @
+ \\___| @
+ @@
+236 LATIN SMALL LETTER I WITH GRAVE
+ __ @
+ \\_\\@
+ | |@
+ | |@
+ |_|@
+ @@
+237 LATIN SMALL LETTER I WITH ACUTE
+ __@
+ /_/@
+ | |@
+ | |@
+ |_|@
+ @@
+238 LATIN SMALL LETTER I WITH CIRCUMFLEX
+ //\\ @
+ |/_\\|@
+ | | @
+ | | @
+ |_| @
+ @@
+239 LATIN SMALL LETTER I WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ | | @
+ | | @
+ |_| @
+ @@
+240 LATIN SMALL LETTER ETH
+ /\\/\\ @
+ > < @
+ _\\/\\ |@
+ / __\` |@
+ \\____/ @
+ @@
+241 LATIN SMALL LETTER N WITH TILDE
+ /\\/| @
+ |/\\/ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ @@
+242 LATIN SMALL LETTER O WITH GRAVE
+ __ @
+ \\_\\ @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+243 LATIN SMALL LETTER O WITH ACUTE
+ __ @
+ /_/ @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+244 LATIN SMALL LETTER O WITH CIRCUMFLEX
+ //\\ @
+ |/_\\| @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+245 LATIN SMALL LETTER O WITH TILDE
+ /\\/| @
+ |/\\/ @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+246 LATIN SMALL LETTER O WITH DIAERESIS
+ _ _ @
+ (_)_(_)@
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+247 DIVISION SIGN
+ @
+ _ @
+ _(_)_ @
+ |_____|@
+ (_) @
+ @@
+248 LATIN SMALL LETTER O WITH STROKE
+ @
+ ____ @
+ / _//\\ @
+ | (//) |@
+ \\//__/ @
+ @@
+249 LATIN SMALL LETTER U WITH GRAVE
+ __ @
+ _\\_\\_ @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+250 LATIN SMALL LETTER U WITH ACUTE
+ __ @
+ _/_/_ @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+251 LATIN SMALL LETTER U WITH CIRCUMFLEX
+ //\\ @
+ |/ \\| @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+252 LATIN SMALL LETTER U WITH DIAERESIS
+ _ _ @
+ (_) (_)@
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+253 LATIN SMALL LETTER Y WITH ACUTE
+ __ @
+ _/_/_ @
+ | | | |@
+ | |_| |@
+ \\__, |@
+ |___/ @@
+254 LATIN SMALL LETTER THORN
+ _ @
+ | |__ @
+ | '_ \\ @
+ | |_) |@
+ | .__/ @
+ |_| @@
+255 LATIN SMALL LETTER Y WITH DIAERESIS
+ _ _ @
+ (_) (_)@
+ | | | |@
+ | |_| |@
+ \\__, |@
+ |___/ @@
+0x0100 LATIN CAPITAL LETTER A WITH MACRON
+ ____ @
+ /___/ @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+0x0101 LATIN SMALL LETTER A WITH MACRON
+ ___ @
+ /_ _/@
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+0x0102 LATIN CAPITAL LETTER A WITH BREVE
+ _ _ @
+ \\\\_// @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ @@
+0x0103 LATIN SMALL LETTER A WITH BREVE
+ \\_/ @
+ ___ @
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+0x0104 LATIN CAPITAL LETTER A WITH OGONEK
+ @
+ _ @
+ /_\\ @
+ / _ \\ @
+ /_/ \\_\\@
+ (_(@@
+0x0105 LATIN SMALL LETTER A WITH OGONEK
+ @
+ __ _ @
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ (_(@@
+0x0106 LATIN CAPITAL LETTER C WITH ACUTE
+ __ @
+ _/_/ @
+ / ___|@
+ | |___ @
+ \\____|@
+ @@
+0x0107 LATIN SMALL LETTER C WITH ACUTE
+ __ @
+ /__/@
+ / __|@
+ | (__ @
+ \\___|@
+ @@
+0x0108 LATIN CAPITAL LETTER C WITH CIRCUMFLEX
+ /\\ @
+ _//\\\\@
+ / ___|@
+ | |___ @
+ \\____|@
+ @@
+0x0109 LATIN SMALL LETTER C WITH CIRCUMFLEX
+ /\\ @
+ /_\\ @
+ / __|@
+ | (__ @
+ \\___|@
+ @@
+0x010A LATIN CAPITAL LETTER C WITH DOT ABOVE
+ [] @
+ ____ @
+ / ___|@
+ | |___ @
+ \\____|@
+ @@
+0x010B LATIN SMALL LETTER C WITH DOT ABOVE
+ [] @
+ ___ @
+ / __|@
+ | (__ @
+ \\___|@
+ @@
+0x010C LATIN CAPITAL LETTER C WITH CARON
+ \\\\// @
+ _\\/_ @
+ / ___|@
+ | |___ @
+ \\____|@
+ @@
+0x010D LATIN SMALL LETTER C WITH CARON
+ \\\\//@
+ _\\/ @
+ / __|@
+ | (__ @
+ \\___|@
+ @@
+0x010E LATIN CAPITAL LETTER D WITH CARON
+ \\\\// @
+ __\\/ @
+ | _ \\ @
+ | |_| |@
+ |____/ @
+ @@
+0x010F LATIN SMALL LETTER D WITH CARON
+ \\/ _ @
+ __| |@
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+0x0110 LATIN CAPITAL LETTER D WITH STROKE
+ ____ @
+ |_ __ \\ @
+ /| |/ | |@
+ /|_|/_| |@
+ |_____/ @
+ @@
+0x0111 LATIN SMALL LETTER D WITH STROKE
+ ---|@
+ __| |@
+ / _\` |@
+ | (_| |@
+ \\__,_|@
+ @@
+0x0112 LATIN CAPITAL LETTER E WITH MACRON
+ ____ @
+ /___/ @
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+0x0113 LATIN SMALL LETTER E WITH MACRON
+ ____@
+ /_ _/@
+ / _ \\ @
+ | __/ @
+ \\___| @
+ @@
+0x0114 LATIN CAPITAL LETTER E WITH BREVE
+ _ _ @
+ \\\\_// @
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+0x0115 LATIN SMALL LETTER E WITH BREVE
+ \\\\ //@
+ -- @
+ / _ \\ @
+ | __/ @
+ \\___| @
+ @@
+0x0116 LATIN CAPITAL LETTER E WITH DOT ABOVE
+ [] @
+ _____ @
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+0x0117 LATIN SMALL LETTER E WITH DOT ABOVE
+ [] @
+ __ @
+ / _ \\@
+ | __/@
+ \\___|@
+ @@
+0x0118 LATIN CAPITAL LETTER E WITH OGONEK
+ @
+ _____ @
+ | ____|@
+ | _|_ @
+ |_____|@
+ (__(@@
+0x0119 LATIN SMALL LETTER E WITH OGONEK
+ @
+ ___ @
+ / _ \\@
+ | __/@
+ \\___|@
+ (_(@@
+0x011A LATIN CAPITAL LETTER E WITH CARON
+ \\\\// @
+ __\\/_ @
+ | ____|@
+ | _|_ @
+ |_____|@
+ @@
+0x011B LATIN SMALL LETTER E WITH CARON
+ \\\\//@
+ \\/ @
+ / _ \\@
+ | __/@
+ \\___|@
+ @@
+0x011C LATIN CAPITAL LETTER G WITH CIRCUMFLEX
+ _/\\_ @
+ / ___|@
+ | | _ @
+ | |_| |@
+ \\____|@
+ @@
+0x011D LATIN SMALL LETTER G WITH CIRCUMFLEX
+ /\\ @
+ _/_ \\@
+ / _\` |@
+ | (_| |@
+ \\__, |@
+ |___/ @@
+0x011E LATIN CAPITAL LETTER G WITH BREVE
+ _\\/_ @
+ / ___|@
+ | | _ @
+ | |_| |@
+ \\____|@
+ @@
+0x011F LATIN SMALL LETTER G WITH BREVE
+ \\___/ @
+ __ _ @
+ / _\` |@
+ | (_| |@
+ \\__, |@
+ |___/ @@
+0x0120 LATIN CAPITAL LETTER G WITH DOT ABOVE
+ _[]_ @
+ / ___|@
+ | | _ @
+ | |_| |@
+ \\____|@
+ @@
+0x0121 LATIN SMALL LETTER G WITH DOT ABOVE
+ [] @
+ __ _ @
+ / _\` |@
+ | (_| |@
+ \\__, |@
+ |___/ @@
+0x0122 LATIN CAPITAL LETTER G WITH CEDILLA
+ ____ @
+ / ___|@
+ | | _ @
+ | |_| |@
+ \\____|@
+ )__) @@
+0x0123 LATIN SMALL LETTER G WITH CEDILLA
+ @
+ __ _ @
+ / _\` |@
+ | (_| |@
+ \\__, |@
+ |_))))@@
+0x0124 LATIN CAPITAL LETTER H WITH CIRCUMFLEX
+ _/ \\_ @
+ | / \\ |@
+ | |_| |@
+ | _ |@
+ |_| |_|@
+ @@
+0x0125 LATIN SMALL LETTER H WITH CIRCUMFLEX
+ _ /\\ @
+ | |//\\ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ @@
+0x0126 LATIN CAPITAL LETTER H WITH STROKE
+ _ _ @
+ | |=| |@
+ | |_| |@
+ | _ |@
+ |_| |_|@
+ @@
+0x0127 LATIN SMALL LETTER H WITH STROKE
+ _ @
+ |=|__ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ @@
+0x0128 LATIN CAPITAL LETTER I WITH TILDE
+ /\\//@
+ |_ _|@
+ | | @
+ | | @
+ |___|@
+ @@
+0x0129 LATIN SMALL LETTER I WITH TILDE
+ @
+ /\\/@
+ | |@
+ | |@
+ |_|@
+ @@
+0x012A LATIN CAPITAL LETTER I WITH MACRON
+ /___/@
+ |_ _|@
+ | | @
+ | | @
+ |___|@
+ @@
+0x012B LATIN SMALL LETTER I WITH MACRON
+ ____@
+ /___/@
+ | | @
+ | | @
+ |_| @
+ @@
+0x012C LATIN CAPITAL LETTER I WITH BREVE
+ \\__/@
+ |_ _|@
+ | | @
+ | | @
+ |___|@
+ @@
+0x012D LATIN SMALL LETTER I WITH BREVE
+ @
+ \\_/@
+ | |@
+ | |@
+ |_|@
+ @@
+0x012E LATIN CAPITAL LETTER I WITH OGONEK
+ ___ @
+ |_ _|@
+ | | @
+ | | @
+ |___|@
+ (__(@@
+0x012F LATIN SMALL LETTER I WITH OGONEK
+ _ @
+ (_) @
+ | | @
+ | | @
+ |_|_@
+ (_(@@
+0x0130 LATIN CAPITAL LETTER I WITH DOT ABOVE
+ _[] @
+ |_ _|@
+ | | @
+ | | @
+ |___|@
+ @@
+0x0131 LATIN SMALL LETTER DOTLESS I
+ @
+ _ @
+ | |@
+ | |@
+ |_|@
+ @@
+0x0132 LATIN CAPITAL LIGATURE IJ
+ ___ _ @
+ |_ _|| |@
+ | | | |@
+ | |_| |@
+ |__|__/ @
+ @@
+0x0133 LATIN SMALL LIGATURE IJ
+ _ _ @
+ (_) (_)@
+ | | | |@
+ | | | |@
+ |_|_/ |@
+ |__/ @@
+0x0134 LATIN CAPITAL LETTER J WITH CIRCUMFLEX
+ /\\ @
+ /_\\|@
+ _ | | @
+ | |_| | @
+ \\___/ @
+ @@
+0x0135 LATIN SMALL LETTER J WITH CIRCUMFLEX
+ /\\@
+ /_\\@
+ | |@
+ | |@
+ _/ |@
+ |__/ @@
+0x0136 LATIN CAPITAL LETTER K WITH CEDILLA
+ _ _ @
+ | |/ / @
+ | ' / @
+ | . \\ @
+ |_|\\_\\ @
+ )__)@@
+0x0137 LATIN SMALL LETTER K WITH CEDILLA
+ _ @
+ | | __@
+ | |/ /@
+ | < @
+ |_|\\_\\@
+ )_)@@
+0x0138 LATIN SMALL LETTER KRA
+ @
+ _ __ @
+ | |/ \\@
+ | < @
+ |_|\\_\\@
+ @@
+0x0139 LATIN CAPITAL LETTER L WITH ACUTE
+ _ //@
+ | | // @
+ | | @
+ | |___ @
+ |_____|@
+ @@
+0x013A LATIN SMALL LETTER L WITH ACUTE
+ //@
+ | |@
+ | |@
+ | |@
+ |_|@
+ @@
+0x013B LATIN CAPITAL LETTER L WITH CEDILLA
+ _ @
+ | | @
+ | | @
+ | |___ @
+ |_____|@
+ )__)@@
+0x013C LATIN SMALL LETTER L WITH CEDILLA
+ _ @
+ | | @
+ | | @
+ | | @
+ |_| @
+ )_)@@
+0x013D LATIN CAPITAL LETTER L WITH CARON
+ _ \\\\//@
+ | | \\/ @
+ | | @
+ | |___ @
+ |_____|@
+ @@
+0x013E LATIN SMALL LETTER L WITH CARON
+ _ \\\\//@
+ | | \\/ @
+ | | @
+ | | @
+ |_| @
+ @@
+0x013F LATIN CAPITAL LETTER L WITH MIDDLE DOT
+ _ @
+ | | @
+ | | [] @
+ | |___ @
+ |_____|@
+ @@
+0x0140 LATIN SMALL LETTER L WITH MIDDLE DOT
+ _ @
+ | | @
+ | | []@
+ | | @
+ |_| @
+ @@
+0x0141 LATIN CAPITAL LETTER L WITH STROKE
+ __ @
+ | // @
+ |//| @
+ // |__ @
+ |_____|@
+ @@
+0x0142 LATIN SMALL LETTER L WITH STROKE
+ _ @
+ | |@
+ |//@
+ //|@
+ |_|@
+ @@
+0x0143 LATIN CAPITAL LETTER N WITH ACUTE
+ _/ /_ @
+ | \\ | |@
+ | \\| |@
+ | |\\ |@
+ |_| \\_|@
+ @@
+0x0144 LATIN SMALL LETTER N WITH ACUTE
+ _ @
+ _ /_/ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ @@
+0x0145 LATIN CAPITAL LETTER N WITH CEDILLA
+ _ _ @
+ | \\ | |@
+ | \\| |@
+ | |\\ |@
+ |_| \\_|@
+ )_) @@
+0x0146 LATIN SMALL LETTER N WITH CEDILLA
+ @
+ _ __ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ )_) @@
+0x0147 LATIN CAPITAL LETTER N WITH CARON
+ _\\/ _ @
+ | \\ | |@
+ | \\| |@
+ | |\\ |@
+ |_| \\_|@
+ @@
+0x0148 LATIN SMALL LETTER N WITH CARON
+ \\\\// @
+ _\\/_ @
+ | '_ \\ @
+ | | | |@
+ |_| |_|@
+ @@
+0x0149 LATIN SMALL LETTER N PRECEDED BY APOSTROPHE
+ @
+ _ __ @
+ ( )| '_\\ @
+ |/| | | |@
+ |_| |_|@
+ @@
+0x014A LATIN CAPITAL LETTER ENG
+ _ _ @
+ | \\ | |@
+ | \\| |@
+ | |\\ |@
+ |_| \\ |@
+ )_)@@
+0x014B LATIN SMALL LETTER ENG
+ _ __ @
+ | '_ \\ @
+ | | | |@
+ |_| | |@
+ | |@
+ |__ @@
+0x014C LATIN CAPITAL LETTER O WITH MACRON
+ ____ @
+ /_ _/ @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+0x014D LATIN SMALL LETTER O WITH MACRON
+ ____ @
+ /_ _/ @
+ / _ \\ @
+ | (_) |@
+ \\___/ @
+ @@
+0x014E LATIN CAPITAL LETTER O WITH BREVE
+ \\ / @
+ _-_ @
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+0x014F LATIN SMALL LETTER O WITH BREVE
+ \\ / @
+ _-_ @
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+0x0150 LATIN CAPITAL LETTER O WITH DOUBLE ACUTE
+ ___ @
+ /_/_/@
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+0x0151 LATIN SMALL LETTER O WITH DOUBLE ACUTE
+ ___ @
+ /_/_/@
+ / _ \\ @
+ | |_| |@
+ \\___/ @
+ @@
+0x0152 LATIN CAPITAL LIGATURE OE
+ ___ ___ @
+ / _ \\| __|@
+ | | | | | @
+ | |_| | |__@
+ \\___/|____@
+ @@
+0x0153 LATIN SMALL LIGATURE OE
+ @
+ ___ ___ @
+ / _ \\ / _ \\@
+ | (_) | __/@
+ \\___/ \\___|@
+ @@
+0x0154 LATIN CAPITAL LETTER R WITH ACUTE
+ _/_/ @
+ | _ \\ @
+ | |_) |@
+ | _ < @
+ |_| \\_\\@
+ @@
+0x0155 LATIN SMALL LETTER R WITH ACUTE
+ __@
+ _ /_/@
+ | '__|@
+ | | @
+ |_| @
+ @@
+0x0156 LATIN CAPITAL LETTER R WITH CEDILLA
+ ____ @
+ | _ \\ @
+ | |_) |@
+ | _ < @
+ |_| \\_\\@
+ )_) @@
+0x0157 LATIN SMALL LETTER R WITH CEDILLA
+ @
+ _ __ @
+ | '__|@
+ | | @
+ |_| @
+ )_) @@
+0x0158 LATIN CAPITAL LETTER R WITH CARON
+ _\\_/ @
+ | _ \\ @
+ | |_) |@
+ | _ < @
+ |_| \\_\\@
+ @@
+0x0159 LATIN SMALL LETTER R WITH CARON
+ \\\\// @
+ _\\/_ @
+ | '__|@
+ | | @
+ |_| @
+ @@
+0x015A LATIN CAPITAL LETTER S WITH ACUTE
+ _/_/ @
+ / ___| @
+ \\___ \\ @
+ ___) |@
+ |____/ @
+ @@
+0x015B LATIN SMALL LETTER S WITH ACUTE
+ __@
+ _/_/@
+ / __|@
+ \\__ \\@
+ |___/@
+ @@
+0x015C LATIN CAPITAL LETTER S WITH CIRCUMFLEX
+ _/\\_ @
+ / ___| @
+ \\___ \\ @
+ ___) |@
+ |____/ @
+ @@
+0x015D LATIN SMALL LETTER S WITH CIRCUMFLEX
+ @
+ /_\\_@
+ / __|@
+ \\__ \\@
+ |___/@
+ @@
+0x015E LATIN CAPITAL LETTER S WITH CEDILLA
+ ____ @
+ / ___| @
+ \\___ \\ @
+ ___) |@
+ |____/ @
+ )__)@@
+0x015F LATIN SMALL LETTER S WITH CEDILLA
+ @
+ ___ @
+ / __|@
+ \\__ \\@
+ |___/@
+ )_)@@
+0x0160 LATIN CAPITAL LETTER S WITH CARON
+ _\\_/ @
+ / ___| @
+ \\___ \\ @
+ ___) |@
+ |____/ @
+ @@
+0x0161 LATIN SMALL LETTER S WITH CARON
+ \\\\//@
+ _\\/ @
+ / __|@
+ \\__ \\@
+ |___/@
+ @@
+0x0162 LATIN CAPITAL LETTER T WITH CEDILLA
+ _____ @
+ |_ _|@
+ | | @
+ | | @
+ |_| @
+ )__)@@
+0x0163 LATIN SMALL LETTER T WITH CEDILLA
+ _ @
+ | |_ @
+ | __|@
+ | |_ @
+ \\__|@
+ )_)@@
+0x0164 LATIN CAPITAL LETTER T WITH CARON
+ _____ @
+ |_ _|@
+ | | @
+ | | @
+ |_| @
+ @@
+0x0165 LATIN SMALL LETTER T WITH CARON
+ \\/ @
+ | |_ @
+ | __|@
+ | |_ @
+ \\__|@
+ @@
+0x0166 LATIN CAPITAL LETTER T WITH STROKE
+ _____ @
+ |_ _|@
+ | | @
+ -|-|- @
+ |_| @
+ @@
+0x0167 LATIN SMALL LETTER T WITH STROKE
+ _ @
+ | |_ @
+ | __|@
+ |-|_ @
+ \\__|@
+ @@
+0x0168 LATIN CAPITAL LETTER U WITH TILDE
+ @
+ _/\\/_ @
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+0x0169 LATIN SMALL LETTER U WITH TILDE
+ @
+ _/\\/_ @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+0x016A LATIN CAPITAL LETTER U WITH MACRON
+ ____ @
+ /__ _/@
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+0x016B LATIN SMALL LETTER U WITH MACRON
+ ____ @
+ / _ /@
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+0x016C LATIN CAPITAL LETTER U WITH BREVE
+ @
+ \\_/_ @
+ | | | |@
+ | |_| |@
+ \\____|@
+ @@
+0x016D LATIN SMALL LETTER U WITH BREVE
+ @
+ \\_/_ @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+0x016E LATIN CAPITAL LETTER U WITH RING ABOVE
+ O @
+ __ _ @
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+0x016F LATIN SMALL LETTER U WITH RING ABOVE
+ O @
+ __ __ @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+0x0170 LATIN CAPITAL LETTER U WITH DOUBLE ACUTE
+ -- --@
+ /_//_/@
+ | | | |@
+ | |_| |@
+ \\___/ @
+ @@
+0x0171 LATIN SMALL LETTER U WITH DOUBLE ACUTE
+ ____@
+ _/_/_/@
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ @@
+0x0172 LATIN CAPITAL LETTER U WITH OGONEK
+ _ _ @
+ | | | |@
+ | | | |@
+ | |_| |@
+ \\___/ @
+ (__(@@
+0x0173 LATIN SMALL LETTER U WITH OGONEK
+ @
+ _ _ @
+ | | | |@
+ | |_| |@
+ \\__,_|@
+ (_(@@
+0x0174 LATIN CAPITAL LETTER W WITH CIRCUMFLEX
+ __ /\\ __@
+ \\ \\ //\\\\/ /@
+ \\ \\ /\\ / / @
+ \\ V V / @
+ \\_/\\_/ @
+ @@
+0x0175 LATIN SMALL LETTER W WITH CIRCUMFLEX
+ /\\ @
+ __ //\\\\__@
+ \\ \\ /\\ / /@
+ \\ V V / @
+ \\_/\\_/ @
+ @@
+0x0176 LATIN CAPITAL LETTER Y WITH CIRCUMFLEX
+ /\\ @
+ __//\\\\ @
+ \\ \\ / /@
+ \\ V / @
+ |_| @
+ @@
+0x0177 LATIN SMALL LETTER Y WITH CIRCUMFLEX
+ /\\ @
+ //\\\\ @
+ | | | |@
+ | |_| |@
+ \\__, |@
+ |___/ @@
+0x0178 LATIN CAPITAL LETTER Y WITH DIAERESIS
+ [] []@
+ __ _@
+ \\ \\ / /@
+ \\ V / @
+ |_| @
+ @@
+0x0179 LATIN CAPITAL LETTER Z WITH ACUTE
+ __/_/@
+ |__ /@
+ / / @
+ / /_ @
+ /____|@
+ @@
+0x017A LATIN SMALL LETTER Z WITH ACUTE
+ _ @
+ _/_/@
+ |_ /@
+ / / @
+ /___|@
+ @@
+0x017B LATIN CAPITAL LETTER Z WITH DOT ABOVE
+ __[]_@
+ |__ /@
+ / / @
+ / /_ @
+ /____|@
+ @@
+0x017C LATIN SMALL LETTER Z WITH DOT ABOVE
+ [] @
+ ____@
+ |_ /@
+ / / @
+ /___|@
+ @@
+0x017D LATIN CAPITAL LETTER Z WITH CARON
+ _\\_/_@
+ |__ /@
+ / / @
+ / /_ @
+ /____|@
+ @@
+0x017E LATIN SMALL LETTER Z WITH CARON
+ \\\\//@
+ _\\/_@
+ |_ /@
+ / / @
+ /___|@
+ @@
+0x017F LATIN SMALL LETTER LONG S
+ __ @
+ / _|@
+ |-| | @
+ |-| | @
+ |_| @
+ @@
+0x02C7 CARON
+ \\\\//@
+ \\/ @
+ $@
+ $@
+ $@
+ $@@
+0x02D8 BREVE
+ \\\\_//@
+ \\_/ @
+ $@
+ $@
+ $@
+ $@@
+0x02D9 DOT ABOVE
+ []@
+ $@
+ $@
+ $@
+ $@
+ $@@
+0x02DB OGONEK
+ $@
+ $@
+ $@
+ $@
+ $@
+ )_) @@
+0x02DD DOUBLE ACUTE ACCENT
+ _ _ @
+ /_/_/@
+ $@
+ $@
+ $@
+ $@@
+0xCA0 KANNADA LETTER TTHA
+ _____)@
+ /_ ___/@
+ / _ \\ @
+ | (_) | @
+ $\\___/$ @
+ @@
+`;
+export default figletStandard;
diff --git a/src/index.ts b/src/index.ts
index 35c1865..d96cebf 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,3 +1,2 @@
-import { ethers } from 'ethers';
-
-export const provider = new ethers.JsonRpcProvider('http://localhost:8545');
\ No newline at end of file
+export * from './services';
+export * from './typechain';
diff --git a/src/merkleTreeWorker.ts b/src/merkleTreeWorker.ts
new file mode 100644
index 0000000..f4e2201
--- /dev/null
+++ b/src/merkleTreeWorker.ts
@@ -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!');
+}
diff --git a/src/program.ts b/src/program.ts
new file mode 100644
index 0000000..e3a78fb
--- /dev/null
+++ b/src/program.ts
@@ -0,0 +1,1751 @@
+import 'dotenv/config';
+import { readFile, writeFile } from 'fs/promises';
+import path from 'path';
+import process from 'process';
+import { createInterface } from 'readline';
+import { Command } from 'commander';
+import Table from 'cli-table3';
+import colors from '@colors/colors';
+import moment from 'moment';
+import {
+ Tornado__factory,
+ TornadoRouter__factory,
+ RelayerRegistry__factory,
+ Aggregator__factory,
+ Governance__factory,
+} from '@tornado/contracts';
+import {
+ JsonRpcProvider,
+ Provider,
+ TransactionLike,
+ Wallet,
+ VoidSigner,
+ formatEther,
+ formatUnits,
+ parseEther,
+ parseUnits,
+ ZeroAddress,
+ MaxUint256,
+ Transaction,
+ BigNumberish,
+} from 'ethers';
+import type MerkleTree from '@tornado/fixed-merkle-tree';
+import * as packageJson from '../package.json';
+import {
+ ERC20,
+ ERC20__factory,
+ Multicall__factory,
+ OffchainOracle__factory,
+ OvmGasPriceOracle__factory,
+} from './typechain';
+import {
+ parseUrl,
+ parseRelayer,
+ parseNumber,
+ parseMnemonic,
+ parseKey,
+ parseAddress,
+ getProviderOptions,
+ getProviderWithNetId,
+ getTokenBalances,
+ TornadoWallet,
+ TornadoVoidSigner,
+ tokenBalances,
+ Deposit,
+ NodeDepositsService,
+ DepositsEvents,
+ WithdrawalsEvents,
+ Relayer,
+ RelayerInfo,
+ RelayerError,
+ NodeRegistryService,
+ TornadoFeeOracle,
+ TokenPriceOracle,
+ calculateSnarkProof,
+ NodeEncryptedNotesService,
+ NodeGovernanceService,
+ RelayerClient,
+ MerkleTreeService,
+ multicall,
+ Invoice,
+ fetchData,
+ fetchDataOptions,
+ networkConfig,
+ subdomains,
+ Config,
+ enabledChains,
+ substring,
+} from './services';
+
+const DEFAULT_GAS_LIMIT = 600_000;
+const RELAYER_NETWORK = 1;
+const TOKEN_PRICE_ORACLE = '0x0AdDd25a91563696D8567Df78D5A01C9a991F9B8';
+
+// Where cached events, trees, circuits, and key is saved
+const STATIC_DIR = process.env.CACHE_DIR || path.join(__dirname, '../static');
+const EVENTS_DIR = path.join(STATIC_DIR, './events');
+const TREES_DIR = path.join(STATIC_DIR, './trees');
+const MERKLE_WORKER_PATH =
+ process.env.DISABLE_MERKLE_WORKER === 'true' ? undefined : path.join(STATIC_DIR, './merkleTreeWorker.js');
+
+// Where we should backup notes and save events
+const USER_DIR = process.env.USER_DIR || '.';
+const SAVED_DIR = path.join(USER_DIR, './events');
+
+const CIRCUIT_PATH = path.join(__dirname, '../static/tornado.json');
+const KEY_PATH = path.join(__dirname, '../static/tornadoProvingKey.bin');
+
+interface packageJson {
+ name: string;
+ version: string;
+ description: string;
+}
+
+export type commonProgramOptions = {
+ rpc?: string;
+ ethRpc?: string;
+ graph?: string;
+ ethGraph?: string;
+ disableGraph?: boolean;
+ relayer?: string;
+ walletWithdrawal?: boolean;
+ torPort?: number;
+ token?: string;
+ viewOnly?: string;
+ mnemonic?: string;
+ mnemonicIndex?: number;
+ privateKey?: string;
+ nonInteractive?: boolean;
+ localRpc?: boolean;
+};
+
+export async function promptConfirmation(nonInteractive?: boolean) {
+ if (nonInteractive) {
+ return;
+ }
+
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
+ const query = 'Confirm? [Y/n]\n';
+
+ const confirmation = await new Promise((resolve) => prompt.question(query, resolve));
+
+ if (!confirmation || (confirmation as string).toUpperCase() !== 'Y') {
+ throw new Error('User canceled');
+ }
+}
+
+export async function getIPAddress(fetchDataOptions: fetchDataOptions) {
+ const htmlIPInfo = await fetchData('https://check.torproject.org', {
+ ...fetchDataOptions,
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'text/html; charset=utf-8',
+ },
+ });
+ const ip = htmlIPInfo.split('Your IP address appears to be: ').pop().split('')[0];
+ const isTor = htmlIPInfo.includes('Sorry. You are not using Tor.') ? false : true;
+
+ return {
+ ip,
+ isTor,
+ };
+}
+
+export async function getProgramOptions(options: commonProgramOptions): Promise<{
+ options: commonProgramOptions;
+ fetchDataOptions: fetchDataOptions;
+}> {
+ options = {
+ rpc: options.rpc || (process.env.RPC_URL ? parseUrl(process.env.RPC_URL) : undefined),
+ ethRpc: options.ethRpc || (process.env.ETHRPC_URL ? parseUrl(process.env.ETHRPC_URL) : undefined),
+ graph: options.graph || (process.env.GRAPH_URL ? parseUrl(process.env.GRAPH_URL) : undefined),
+ ethGraph: options.ethGraph || (process.env.ETHGRAPH_URL ? parseUrl(process.env.ETHGRAPH_URL) : undefined),
+ disableGraph: Boolean(options.disableGraph) || (process.env.DISABLE_GRAPH === 'true' ? true : undefined),
+ relayer: options.relayer || (process.env.RELAYER ? parseRelayer(process.env.RELAYER) : undefined),
+ walletWithdrawal:
+ Boolean(options.walletWithdrawal) || (process.env.WALLET_WITHDRAWAL === 'true' ? true : undefined),
+ torPort: options.torPort || (process.env.TOR_PORT ? parseNumber(process.env.TOR_PORT) : undefined),
+ token: options.token || (process.env.TOKEN ? parseAddress(process.env.TOKEN) : undefined),
+ viewOnly: options.viewOnly || (process.env.VIEW_ONLY ? parseAddress(process.env.VIEW_ONLY) : undefined),
+ mnemonic: options.mnemonic || (process.env.MNEMONIC ? parseMnemonic(process.env.MNEMONIC) : undefined),
+ mnemonicIndex:
+ options.mnemonicIndex || (process.env.MNEMONIC_INDEX ? parseNumber(process.env.MNEMONIC_INDEX) : undefined),
+ privateKey: options.privateKey || (process.env.PRIVATE_KEY ? parseKey(process.env.PRIVATE_KEY) : undefined),
+ nonInteractive: Boolean(options.nonInteractive) || (process.env.NON_INTERACTIVE === 'true' ? true : undefined),
+ localRpc: Boolean(options.localRpc) || (process.env.LOCAL_RPC === 'true' ? true : undefined),
+ };
+
+ const fetchDataOptions = {
+ torPort: options.torPort,
+ };
+
+ const { ip, isTor } = await getIPAddress(fetchDataOptions);
+
+ const optionsTable = new Table();
+
+ optionsTable.push(
+ [{ colSpan: 2, content: 'Program Options', hAlign: 'center' }],
+ ['IP', ip],
+ ['Is Tor', isTor],
+ ...(Object.keys(options)
+ .map((key) => {
+ const value = (options as unknown as { [key in string]: string })[key];
+
+ if (typeof value !== 'undefined') {
+ return [key, (options as unknown as { [key in string]: string })[key]];
+ }
+ })
+ .filter((r) => r) as string[][]),
+ );
+
+ console.log('\n' + optionsTable.toString() + '\n');
+
+ await promptConfirmation(options.nonInteractive);
+
+ return {
+ options,
+ fetchDataOptions,
+ };
+}
+
+export function getProgramGraphAPI(options: commonProgramOptions, config: Config): string {
+ if (options.disableGraph) {
+ return '';
+ }
+
+ if (!options.graph) {
+ return Object.values(config.subgraphs)[0].url;
+ }
+
+ return options.graph;
+}
+
+export function getProgramProvider(
+ netId: BigNumberish,
+ rpcUrl: string = '',
+ config: Config,
+ providerOptions?: getProviderOptions,
+): JsonRpcProvider {
+ if (!rpcUrl) {
+ rpcUrl = Object.values(config.rpcUrls)[0].url;
+ }
+
+ return getProviderWithNetId(netId, rpcUrl, config, providerOptions);
+}
+
+export function getProgramSigner({
+ options,
+ provider,
+}: {
+ options: commonProgramOptions;
+ provider: Provider;
+}): TornadoVoidSigner | TornadoWallet | undefined {
+ if (options.viewOnly) {
+ return new TornadoVoidSigner(options.viewOnly, provider);
+ }
+
+ if (options.privateKey) {
+ return new TornadoWallet(options.privateKey, provider);
+ }
+
+ if (options.mnemonic) {
+ return TornadoWallet.fromMnemonic(options.mnemonic, provider, options.mnemonicIndex);
+ }
+}
+
+export async function getProgramRelayer({
+ options,
+ fetchDataOptions,
+ netId,
+}: {
+ options: commonProgramOptions;
+ fetchDataOptions?: fetchDataOptions;
+ netId: number | string;
+}): Promise<{
+ validRelayers?: RelayerInfo[] | Relayer[];
+ invalidRelayers?: RelayerError[];
+ relayerClient?: RelayerClient;
+}> {
+ const { ethRpc, ethGraph, relayer, disableGraph } = options;
+
+ const netConfig = networkConfig[`netId${netId}`];
+
+ const ethConfig = networkConfig[`netId${RELAYER_NETWORK}`];
+
+ const {
+ aggregatorContract,
+ registryContract,
+ registrySubgraph,
+ constants: { REGISTRY_BLOCK },
+ } = ethConfig;
+
+ const provider = getProgramProvider(1, ethRpc, ethConfig, {
+ ...fetchDataOptions,
+ });
+
+ const graphApi = getProgramGraphAPI(
+ {
+ disableGraph,
+ graph: ethGraph,
+ },
+ ethConfig,
+ );
+
+ const registryService = new NodeRegistryService({
+ netId: RELAYER_NETWORK,
+ provider,
+ graphApi,
+ subgraphName: registrySubgraph,
+ RelayerRegistry: RelayerRegistry__factory.connect(registryContract as string, provider),
+ deployedBlock: REGISTRY_BLOCK,
+ fetchDataOptions,
+ cacheDirectory: EVENTS_DIR,
+ userDirectory: SAVED_DIR,
+ });
+
+ const relayerClient = new RelayerClient({
+ netId,
+ config: netConfig,
+ Aggregator: Aggregator__factory.connect(aggregatorContract as string, provider),
+ fetchDataOptions,
+ });
+
+ if (relayer) {
+ if (!relayer.endsWith('.eth')) {
+ const relayerStatus = await relayerClient.askRelayerStatus({
+ hostname: new URL(relayer).hostname,
+ });
+
+ if (relayerStatus) {
+ relayerClient.selectedRelayer = {
+ netId: relayerStatus.netId,
+ url: relayerStatus.url,
+ rewardAccount: relayerStatus.rewardAccount,
+ currentQueue: relayerStatus.currentQueue,
+ tornadoServiceFee: relayerStatus.tornadoServiceFee,
+ };
+ }
+
+ return {
+ validRelayers: relayerClient.selectedRelayer ? [relayerClient.selectedRelayer] : [],
+ invalidRelayers: [],
+ relayerClient,
+ };
+ } else {
+ const { validRelayers } = await relayerClient.getValidRelayers([{ ensName: relayer }], subdomains, true);
+ const relayerStatus = validRelayers[0];
+
+ if (relayerStatus) {
+ relayerClient.selectedRelayer = {
+ netId: relayerStatus.netId,
+ url: relayerStatus.url,
+ rewardAccount: relayerStatus.rewardAccount,
+ currentQueue: relayerStatus.currentQueue,
+ tornadoServiceFee: relayerStatus.tornadoServiceFee,
+ };
+ }
+
+ return {
+ validRelayers,
+ invalidRelayers: [],
+ relayerClient,
+ };
+ }
+ }
+
+ console.log('\nGetting valid relayers from registry, would take some time\n');
+
+ const { validRelayers, invalidRelayers } = await relayerClient.getValidRelayers(
+ await registryService.fetchRelayers(),
+ subdomains,
+ );
+
+ const relayerStatus = relayerClient.pickWeightedRandomRelayer(validRelayers);
+
+ if (relayerStatus) {
+ relayerClient.selectedRelayer = {
+ netId: relayerStatus.netId,
+ url: relayerStatus.url,
+ rewardAccount: relayerStatus.rewardAccount,
+ currentQueue: relayerStatus.currentQueue,
+ tornadoServiceFee: relayerStatus.tornadoServiceFee,
+ };
+ }
+
+ return {
+ validRelayers,
+ invalidRelayers,
+ relayerClient,
+ };
+}
+
+export async function programSendTransaction({
+ signer,
+ options,
+ populatedTransaction,
+}: {
+ signer: VoidSigner | Wallet;
+ options: commonProgramOptions;
+ populatedTransaction: TransactionLike;
+}) {
+ const txTable = new Table();
+ // ethers.js doesn't output complete transaction from the contract runner so we populate the transaction once again
+ const txObject =
+ !populatedTransaction.gasLimit || !populatedTransaction.from
+ ? (JSON.parse(JSON.stringify(await signer.populateTransaction(populatedTransaction))) as TransactionLike)
+ : (JSON.parse(JSON.stringify(populatedTransaction, null, 2)) as TransactionLike);
+
+ const txKeys = Object.keys(txObject);
+ const txValues = Object.values(txObject);
+ const txType =
+ signer instanceof VoidSigner
+ ? 'Unsigned Transaction'
+ : options.localRpc
+ ? 'Unbroadcasted Transaction'
+ : 'Send Transaction?';
+
+ txTable.push(
+ [{ colSpan: 2, content: txType, hAlign: 'center' }],
+ ...txKeys.map((key, index) => {
+ if (key === 'data' && txValues[index]) {
+ return ['data', substring(txValues[index], 40)];
+ }
+ return [key, txValues[index]];
+ }),
+ );
+
+ if (txType === 'Unsigned Transaction') {
+ // delete from field as the Transaction.from method doesn't accept it from unsigned tx
+ delete txObject.from;
+ const transaction = Transaction.from(txObject);
+ console.log('\n' + txTable.toString() + '\n');
+ console.log('Sign this transaction:\n');
+ console.log(`${transaction.unsignedSerialized}\n`);
+ return;
+ }
+
+ if (txType === 'Unbroadcasted Transaction') {
+ const signedTx = await signer.signTransaction(txObject);
+ console.log('\n' + txTable.toString() + '\n');
+ console.log('Broadcast transaction:\n');
+ console.log(`${signedTx}\n`);
+ return;
+ }
+
+ console.log('\n' + txTable.toString() + '\n');
+
+ await promptConfirmation(options.nonInteractive);
+
+ const { hash } = await signer.sendTransaction(txObject);
+
+ console.log(`Sent transaction ${hash}\n`);
+}
+
+export function tornadoProgram() {
+ const { name, version, description } = packageJson as packageJson;
+
+ const program = new Command();
+
+ program.name(name).version(version).description(description);
+
+ program
+ .command('create')
+ .description('Creates Tornado Cash deposit note and deposit invoice')
+ .argument('', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
+ .argument('', 'Currency to deposit on Tornado Cash')
+ .argument('', 'Amount to deposit on Tornado Cash')
+ .action(async (netId: string | number, currency: string, amount: string) => {
+ currency = currency.toLowerCase();
+
+ const config = networkConfig[`netId${netId}`];
+
+ const {
+ routerContract,
+ nativeCurrency,
+ tokens: { [currency]: currencyConfig },
+ } = config;
+
+ const {
+ decimals,
+ tokenAddress,
+ instanceAddress: { [amount]: instanceAddress },
+ } = currencyConfig;
+
+ const isEth = nativeCurrency === currency;
+ const denomination = parseUnits(amount, decimals);
+
+ const deposit = await Deposit.createNote({ currency, amount, netId });
+
+ const { noteHex, note, commitmentHex } = deposit;
+
+ const ERC20Interface = ERC20__factory.createInterface();
+ const TornadoRouterInterface = TornadoRouter__factory.createInterface();
+
+ console.log(`New deposit: ${deposit.toString()}\n`);
+
+ await writeFile(`./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`, note, {
+ encoding: 'utf8',
+ });
+
+ const depositData = TornadoRouterInterface.encodeFunctionData('deposit', [instanceAddress, commitmentHex, '0x']);
+
+ if (!isEth) {
+ const approveData = ERC20Interface.encodeFunctionData('approve', [routerContract, MaxUint256]);
+
+ console.log(`Approve Data: ${JSON.stringify({ to: tokenAddress, data: approveData }, null, 2)}]\n`);
+
+ console.log(`Transaction Data: ${JSON.stringify({ to: routerContract, data: depositData }, null, 2)}\n`);
+ } else {
+ console.log(
+ `Transaction Data: ${JSON.stringify({ to: routerContract, value: denomination.toString(), data: depositData }, null, 2)}`,
+ );
+ }
+
+ process.exit(0);
+ });
+
+ program
+ .command('deposit')
+ .description(
+ 'Submit a deposit of specified currency and amount from default eth account and return the resulting note. \n\n' +
+ 'The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). \n\n' +
+ 'The amount depends on currency, see config.js file or see Tornado Cash UI.',
+ )
+ .argument('', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
+ .argument('', 'Currency to deposit on Tornado Cash')
+ .argument('', 'Amount to deposit on Tornado Cash')
+ .action(async (netId: string | number, currency: string, amount: string, cmdOptions: commonProgramOptions) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ currency = currency.toLowerCase();
+ const { rpc } = options;
+
+ const config = networkConfig[`netId${netId}`];
+
+ const {
+ multicall: multicallAddress,
+ routerContract,
+ nativeCurrency,
+ tokens: { [currency]: currencyConfig },
+ } = config;
+
+ const {
+ decimals,
+ tokenAddress,
+ instanceAddress: { [amount]: instanceAddress },
+ } = currencyConfig;
+
+ const isEth = nativeCurrency === currency;
+ const denomination = parseUnits(amount, decimals);
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const signer = getProgramSigner({
+ options,
+ provider,
+ });
+
+ if (!signer) {
+ throw new Error(
+ 'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured',
+ );
+ }
+
+ const TornadoProxy = TornadoRouter__factory.connect(routerContract, signer);
+ const Multicall = Multicall__factory.connect(multicallAddress, provider);
+ const Token = tokenAddress ? ERC20__factory.connect(tokenAddress, signer) : undefined;
+
+ const [ethBalance, tokenBalance, tokenApprovals] = await multicall(Multicall, [
+ {
+ contract: Multicall,
+ name: 'getEthBalance',
+ params: [signer.address],
+ },
+ /* eslint-disable prettier/prettier */
+ ...(!isEth
+ ? [
+ {
+ contract: Token as ERC20,
+ name: 'balanceOf',
+ params: [signer.address],
+ },
+ {
+ contract: Token as ERC20,
+ name: 'allowance',
+ params: [signer.address, routerContract],
+ },
+ ]
+ : []),
+ /* eslint-enable prettier/prettier */
+ ]);
+
+ if (isEth && denomination > ethBalance) {
+ const errMsg = `Invalid ${currency.toUpperCase()} balance, wants ${amount} have ${formatUnits(ethBalance, decimals)}`;
+ throw new Error(errMsg);
+ } else if (!isEth && denomination > tokenBalance) {
+ const errMsg = `Invalid ${currency.toUpperCase()} balance, wants ${amount} have ${formatUnits(tokenBalance, decimals)}`;
+ throw new Error(errMsg);
+ }
+
+ if (!isEth && denomination > tokenApprovals) {
+ // token approval
+ await programSendTransaction({
+ signer,
+ options,
+ populatedTransaction: await (Token as ERC20).approve.populateTransaction(routerContract, MaxUint256),
+ });
+
+ // wait until signer sends the approve transaction offline
+ if (signer instanceof VoidSigner || options.localRpc) {
+ console.log(
+ 'Signer can not sign or broadcast transactions, please send the token approve transaction first and try again.\n',
+ );
+ process.exit(0);
+ }
+ }
+
+ const deposit = await Deposit.createNote({ currency, amount, netId });
+
+ const { note, noteHex, commitmentHex } = deposit;
+
+ console.log(`New deposit: ${deposit.toString()}\n`);
+
+ await writeFile(`./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`, note, {
+ encoding: 'utf8',
+ });
+
+ await programSendTransaction({
+ signer,
+ options,
+ populatedTransaction: await TornadoProxy.deposit.populateTransaction(instanceAddress, commitmentHex, '0x', {
+ value: isEth ? denomination : BigInt(0),
+ }),
+ });
+
+ process.exit(0);
+ });
+
+ program
+ .command('depositInvoice')
+ .description(
+ 'Submit a deposit of tornado invoice from default eth account and return the resulting note. \n\n' +
+ 'Useful to deposit on online computer without exposing note',
+ )
+ .argument('', 'Tornado Cash Invoice generated from create command')
+ .action(async (invoiceString: string, cmdOptions: commonProgramOptions) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc } = options;
+
+ const { currency, amount, netId, commitment } = new Invoice(invoiceString);
+
+ const config = networkConfig[`netId${netId}`];
+
+ const {
+ multicall: multicallAddress,
+ routerContract,
+ nativeCurrency,
+ tokens: { [currency]: currencyConfig },
+ } = config;
+
+ const {
+ decimals,
+ tokenAddress,
+ instanceAddress: { [amount]: instanceAddress },
+ } = currencyConfig;
+
+ const isEth = nativeCurrency === currency;
+ const denomination = parseUnits(amount, decimals);
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const signer = getProgramSigner({
+ options,
+ provider,
+ });
+
+ if (!signer) {
+ throw new Error(
+ 'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured',
+ );
+ }
+
+ const TornadoProxy = TornadoRouter__factory.connect(routerContract, signer);
+ const Multicall = Multicall__factory.connect(multicallAddress, provider);
+ const Token = tokenAddress ? ERC20__factory.connect(tokenAddress, signer) : undefined;
+
+ const [ethBalance, tokenBalance, tokenApprovals] = await multicall(Multicall, [
+ {
+ contract: Multicall,
+ name: 'getEthBalance',
+ params: [signer.address],
+ },
+ /* eslint-disable prettier/prettier */
+ ...(!isEth
+ ? [
+ {
+ contract: Token as ERC20,
+ name: 'balanceOf',
+ params: [signer.address],
+ },
+ {
+ contract: Token as ERC20,
+ name: 'allowance',
+ params: [signer.address, routerContract],
+ },
+ ]
+ : []),
+ /* eslint-enable prettier/prettier */
+ ]);
+
+ if (isEth && denomination > ethBalance) {
+ const errMsg = `Invalid ${currency.toUpperCase()} balance, wants ${amount} have ${formatUnits(ethBalance, decimals)}`;
+ throw new Error(errMsg);
+ } else if (!isEth && denomination > tokenBalance) {
+ const errMsg = `Invalid ${currency.toUpperCase()} balance, wants ${amount} have ${formatUnits(tokenBalance, decimals)}`;
+ throw new Error(errMsg);
+ }
+
+ if (!isEth && denomination > tokenApprovals) {
+ // token approval
+ await programSendTransaction({
+ signer,
+ options,
+ populatedTransaction: await (Token as ERC20).approve.populateTransaction(routerContract, MaxUint256),
+ });
+
+ // wait until signer sends the approve transaction offline
+ if (signer instanceof VoidSigner) {
+ console.log(
+ 'Signer can not sign transactions, please send the token approve transaction first and try again.\n',
+ );
+ process.exit(0);
+ }
+ }
+
+ await programSendTransaction({
+ signer,
+ options,
+ populatedTransaction: await TornadoProxy.deposit.populateTransaction(instanceAddress, commitment, '0x', {
+ value: isEth ? denomination : BigInt(0),
+ }),
+ });
+
+ process.exit(0);
+ });
+
+ program
+ .command('withdraw')
+ .description(
+ 'Withdraw a note to a recipient account using relayer or specified private key. \n\n' +
+ 'You can exchange some of your deposit`s tokens to ETH during the withdrawal by ' +
+ 'specifing ETH_purchase (e.g. 0.01) to pay for gas in future transactions. \n\n' +
+ 'Also see the --relayer option.\n\n',
+ )
+ .argument('', 'Tornado Cash Deposit Note')
+ .argument('', 'Recipient to receive withdrawn amount', parseAddress)
+ .argument('[ETH_purchase]', 'ETH to purchase', parseNumber)
+ .action(
+ async (note: string, recipient: string, ethPurchase: number | undefined, cmdOptions: commonProgramOptions) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc, walletWithdrawal } = options;
+
+ const deposit = await Deposit.parseNote(note);
+
+ const { netId, currency, amount, commitmentHex, nullifierHex, nullifier, secret } = deposit;
+
+ const config = networkConfig[`netId${netId}`];
+
+ const {
+ tornadoSubgraph,
+ deployedBlock,
+ nativeCurrency,
+ routerContract,
+ multicall: multicallAddress,
+ ovmGasPriceOracleContract,
+ tokens: { [currency]: currencyConfig },
+ } = config;
+
+ const {
+ decimals,
+ tokenAddress,
+ gasLimit: instanceGasLimit,
+ tokenGasLimit,
+ instanceAddress: { [amount]: instanceAddress },
+ } = currencyConfig;
+
+ const isEth = nativeCurrency === currency;
+ const denomination = parseUnits(amount, decimals);
+ const firstAmount = Object.keys(currencyConfig.instanceAddress).sort((a, b) => Number(a) - Number(b))[0];
+ const isFirstAmount = Number(amount) === Number(firstAmount);
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const signer = getProgramSigner({
+ options,
+ provider,
+ });
+ const noSigner = Boolean(!signer || signer instanceof VoidSigner);
+
+ if (walletWithdrawal && noSigner) {
+ throw new Error('Wallet withdrawal is configured however could not find any wallets');
+ }
+
+ const graphApi = getProgramGraphAPI(options, config);
+
+ const Tornado = Tornado__factory.connect(instanceAddress, provider);
+ const TornadoProxy = TornadoRouter__factory.connect(routerContract, !walletWithdrawal ? provider : signer);
+ const Multicall = Multicall__factory.connect(multicallAddress, provider);
+
+ const tornadoFeeOracle = new TornadoFeeOracle(
+ ovmGasPriceOracleContract
+ ? OvmGasPriceOracle__factory.connect(ovmGasPriceOracleContract, provider)
+ : undefined,
+ );
+ const tokenPriceOracle = new TokenPriceOracle(
+ provider,
+ Multicall,
+ OffchainOracle__factory.connect(TOKEN_PRICE_ORACLE, provider),
+ );
+
+ const depositsServiceConstructor = {
+ netId,
+ provider,
+ graphApi,
+ subgraphName: tornadoSubgraph,
+ Tornado,
+ amount,
+ currency,
+ deployedBlock,
+ fetchDataOptions,
+ cacheDirectory: EVENTS_DIR,
+ userDirectory: SAVED_DIR,
+ };
+
+ const depositsService = new NodeDepositsService({
+ ...depositsServiceConstructor,
+ type: 'Deposit',
+ });
+
+ const withdrawalsService = new NodeDepositsService({
+ ...depositsServiceConstructor,
+ type: 'Withdrawal',
+ });
+
+ const merkleTreeService = new MerkleTreeService({
+ netId,
+ amount,
+ currency,
+ Tornado,
+ merkleWorkerPath: MERKLE_WORKER_PATH,
+ });
+
+ const depositEvents = (await depositsService.updateEvents()).events as DepositsEvents[];
+
+ // If we have MERKLE_WORKER_PATH run worker at background otherwise resolve it here
+ const depositTreeInitiator = await (async () => {
+ if (MERKLE_WORKER_PATH) {
+ return () => merkleTreeService.verifyTree({ events: depositEvents }) as Promise;
+ }
+ return (await merkleTreeService.verifyTree({ events: depositEvents })) as MerkleTree;
+ })();
+
+ let depositTreePromise: Promise | MerkleTree;
+
+ if (typeof depositTreeInitiator === 'function') {
+ depositTreePromise = depositTreeInitiator();
+ } else {
+ depositTreePromise = depositTreeInitiator;
+ }
+
+ const withdrawalEvents = (await withdrawalsService.updateEvents()).events as WithdrawalsEvents[];
+
+ const depositEvent = depositEvents.find(({ commitment }) => commitment === commitmentHex);
+
+ const withdrawalEvent = withdrawalEvents.find(({ nullifierHash }) => nullifierHash === nullifierHex);
+
+ if (!depositEvent) {
+ throw new Error('Deposit not found');
+ }
+
+ const complianceTable = new Table();
+
+ const depositDate = new Date(depositEvent.timestamp * 1000);
+
+ complianceTable.push(
+ [{ colSpan: 2, content: 'Deposit', hAlign: 'center' }],
+ ['Deposit', `${amount} ${currency.toUpperCase()}`],
+ [
+ 'Date',
+ `${depositDate.toLocaleDateString()} ${depositDate.toLocaleTimeString()} (${moment.unix(depositEvent.timestamp).fromNow()})`,
+ ],
+ ['From', depositEvent.from],
+ ['Transaction', depositEvent.transactionHash],
+ ['Commitment', commitmentHex],
+ ['Spent', Boolean(withdrawalEvent)],
+ );
+
+ if (withdrawalEvent) {
+ const withdrawalDate = new Date(withdrawalEvent.timestamp * 1000);
+
+ complianceTable.push(
+ [{ colSpan: 2, content: 'Withdraw', hAlign: 'center' }],
+ ['Withdrawal', `${amount} ${currency.toUpperCase()}`],
+ ['Relayer Fee', `${formatUnits(withdrawalEvent.fee, decimals)} ${currency.toUpperCase()}`],
+ [
+ 'Date',
+ `${withdrawalDate.toLocaleDateString()} ${withdrawalDate.toLocaleTimeString()} (${moment.unix(withdrawalEvent.timestamp).fromNow()})`,
+ ],
+ ['To', withdrawalEvent.to],
+ ['Transaction', withdrawalEvent.transactionHash],
+ ['Nullifier', nullifierHex],
+ );
+ }
+
+ console.log('\n\n' + complianceTable.toString() + '\n');
+
+ if (withdrawalEvent) {
+ throw new Error('Note is already spent');
+ }
+
+ const [circuit, provingKey, tree, relayerClient, l1Fee, tokenPriceInWei, feeData] = await Promise.all([
+ readFile(CIRCUIT_PATH, { encoding: 'utf8' }).then((s) => JSON.parse(s)),
+ readFile(KEY_PATH).then((b) => new Uint8Array(b).buffer),
+ depositTreePromise,
+ /* eslint-disable prettier/prettier */
+ !walletWithdrawal
+ ? getProgramRelayer({
+ options,
+ fetchDataOptions,
+ netId,
+ }).then(({ relayerClient }) => relayerClient)
+ : undefined,
+ /* eslint-enable prettier/prettier */
+ tornadoFeeOracle.fetchL1OptimismFee(),
+ !isEth ? tokenPriceOracle.fetchPrices([tokenAddress as string]).then((p) => p[0]) : BigInt(0),
+ provider.getFeeData(),
+ ]);
+
+ if (!walletWithdrawal && !relayerClient?.selectedRelayer) {
+ throw new Error(
+ 'No valid relayer found for the network, you can either try again, or find any relayers using the relayers command and set with --relayer option',
+ );
+ }
+
+ const { url, rewardAccount, tornadoServiceFee } = relayerClient?.selectedRelayer || {};
+
+ let gasPrice: bigint = feeData.maxFeePerGas
+ ? feeData.maxFeePerGas + (feeData.maxPriorityFeePerGas || BigInt(0))
+ : (feeData.gasPrice as bigint);
+
+ if (netId === 56 && gasPrice < parseUnits('3.3', 'gwei')) {
+ gasPrice = parseUnits('3.3', 'gwei');
+ }
+
+ // If the config overrides default gas limit we override
+ const defaultGasLimit = instanceGasLimit ? BigInt(instanceGasLimit) : BigInt(DEFAULT_GAS_LIMIT);
+ let gasLimit = defaultGasLimit;
+
+ // If the denomination is small only refund small amount otherwise use the default value
+ const refundGasLimit = isFirstAmount && tokenGasLimit ? BigInt(tokenGasLimit) : undefined;
+
+ const ethRefund = ethPurchase
+ ? parseEther(`${ethPurchase}`)
+ : !isEth
+ ? tornadoFeeOracle.defaultEthRefund(gasPrice, refundGasLimit)
+ : BigInt(0);
+
+ if (isEth && ethRefund) {
+ throw new Error('Can not purchase native assets on native asset withdrawal');
+ }
+
+ async function getProof() {
+ let relayerFee = BigInt(0);
+
+ if (!walletWithdrawal) {
+ relayerFee = tornadoFeeOracle.calculateRelayerFee({
+ gasPrice,
+ gasLimit,
+ l1Fee,
+ denomination,
+ ethRefund,
+ tokenPriceInWei,
+ tokenDecimals: decimals,
+ relayerFeePercent: tornadoServiceFee,
+ isEth,
+ });
+
+ if (relayerFee > denomination) {
+ const errMsg =
+ `Relayer fee ${formatUnits(relayerFee, decimals)} ${currency.toUpperCase()} ` +
+ `exceeds the deposit amount ${amount} ${currency.toUpperCase()}.`;
+ throw new Error(errMsg);
+ }
+ }
+
+ const { pathElements, pathIndices } = tree.path((depositEvent as DepositsEvents).leafIndex);
+
+ const { proof, args } = await calculateSnarkProof(
+ {
+ root: tree.root,
+ nullifierHex,
+ recipient,
+ relayer: !walletWithdrawal ? (rewardAccount as string) : ZeroAddress,
+ fee: relayerFee,
+ refund: ethRefund,
+ nullifier,
+ secret,
+ pathElements,
+ pathIndices,
+ },
+ circuit,
+ provingKey,
+ );
+
+ return {
+ relayerFee,
+ proof,
+ args,
+ };
+ }
+
+ let { relayerFee, proof, args } = await getProof();
+
+ const withdrawOverrides = {
+ from: !walletWithdrawal ? (rewardAccount as string) : (signer?.address as string),
+ value: args[5] || 0,
+ };
+
+ gasLimit = await TornadoProxy.withdraw.estimateGas(instanceAddress, proof, ...args, withdrawOverrides);
+
+ if (gasLimit > defaultGasLimit) {
+ ({ relayerFee, proof, args } = await getProof());
+ gasLimit = await TornadoProxy.withdraw.estimateGas(instanceAddress, proof, ...args, withdrawOverrides);
+ }
+
+ const withdrawTable = new Table();
+ withdrawTable.push([{ colSpan: 2, content: 'Withdrawal Info', hAlign: 'center' }]);
+
+ // withdraw using relayer
+ if (!walletWithdrawal && relayerClient) {
+ withdrawTable.push(
+ [{ colSpan: 2, content: 'Withdraw', hAlign: 'center' }],
+ ['Withdrawal', `${amount} ${currency.toUpperCase()}`],
+ ['Relayer', `${url}`],
+ [
+ 'Relayer Fee',
+ `${formatUnits(relayerFee, decimals)} ${currency.toUpperCase()} ` +
+ `(${((Number(relayerFee) / Number(denomination)) * 100).toFixed(5)}%)`,
+ ],
+ ['Relayer Fee Percent', `${tornadoServiceFee}%`],
+ [
+ 'Amount to receive',
+ `${Number(formatUnits(denomination - relayerFee, decimals)).toFixed(5)} ${currency.toUpperCase()}`,
+ ],
+ [`${nativeCurrency.toUpperCase()} purchase`, `${formatEther(ethRefund)} ${nativeCurrency.toUpperCase()}`],
+ ['To', recipient],
+ ['Nullifier', nullifierHex],
+ );
+
+ console.log('\n' + withdrawTable.toString() + '\n');
+
+ await promptConfirmation(options.nonInteractive);
+
+ console.log('Sending withdraw transaction through relay\n');
+
+ await relayerClient.tornadoWithdraw({
+ contract: instanceAddress,
+ proof,
+ args,
+ });
+ } else {
+ // withdraw from wallet
+
+ const txFee = gasPrice * gasLimit;
+ const txFeeInToken = !isEth
+ ? tornadoFeeOracle.calculateTokenAmount(txFee, tokenPriceInWei, decimals)
+ : BigInt(0);
+ const txFeeString = !isEth
+ ? `(${Number(formatUnits(txFeeInToken, decimals)).toFixed(5)} ${currency.toUpperCase()} worth) ` +
+ `(${((Number(formatUnits(txFeeInToken, decimals)) / Number(amount)) * 100).toFixed(5)}%)`
+ : `(${((Number(formatUnits(txFee, decimals)) / Number(amount)) * 100).toFixed(5)}%)`;
+
+ withdrawTable.push(
+ [{ colSpan: 2, content: 'Withdraw', hAlign: 'center' }],
+ ['Withdrawal', `${amount} ${currency.toUpperCase()}`],
+ ['From', `${signer?.address}`],
+ [
+ 'Transaction Fee',
+ `${Number(formatEther(txFee)).toFixed(5)} ${nativeCurrency.toUpperCase()} ` + txFeeString,
+ ],
+ ['Amount to receive', `${amount} ${currency.toUpperCase()}`],
+ ['To', recipient],
+ ['Nullifier', nullifierHex],
+ );
+
+ console.log('\n' + withdrawTable.toString() + '\n');
+
+ await promptConfirmation(options.nonInteractive);
+
+ console.log('Sending withdraw transaction through wallet\n');
+
+ await programSendTransaction({
+ signer: signer as TornadoVoidSigner | TornadoWallet,
+ options,
+ populatedTransaction: await TornadoProxy.withdraw.populateTransaction(instanceAddress, proof, ...args),
+ });
+ }
+
+ process.exit(0);
+ },
+ );
+
+ program
+ .command('compliance')
+ .description(
+ 'Shows the deposit and withdrawal of the provided note. \n\n' +
+ 'This might be necessary to show the origin of assets held in your withdrawal address. \n\n',
+ )
+ .argument('', 'Tornado Cash Deposit Note')
+ .action(async (note: string, cmdOptions: commonProgramOptions) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc } = options;
+
+ const deposit = await Deposit.parseNote(note);
+ const { netId, currency, amount, commitmentHex, nullifierHex } = deposit;
+
+ const config = networkConfig[`netId${netId}`];
+
+ const {
+ tornadoSubgraph,
+ deployedBlock,
+ tokens: { [currency]: currencyConfig },
+ } = config;
+
+ const {
+ decimals,
+ instanceAddress: { [amount]: instanceAddress },
+ } = currencyConfig;
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const graphApi = getProgramGraphAPI(options, config);
+
+ const Tornado = Tornado__factory.connect(instanceAddress, provider);
+
+ const depositsServiceConstructor = {
+ netId,
+ provider,
+ graphApi,
+ subgraphName: tornadoSubgraph,
+ Tornado,
+ amount,
+ currency,
+ deployedBlock,
+ fetchDataOptions,
+ cacheDirectory: EVENTS_DIR,
+ userDirectory: SAVED_DIR,
+ };
+
+ const depositsService = new NodeDepositsService({
+ ...depositsServiceConstructor,
+ type: 'Deposit',
+ });
+
+ const withdrawalsService = new NodeDepositsService({
+ ...depositsServiceConstructor,
+ type: 'Withdrawal',
+ });
+
+ const merkleTreeService = new MerkleTreeService({
+ netId,
+ amount,
+ currency,
+ Tornado,
+ merkleWorkerPath: MERKLE_WORKER_PATH,
+ });
+
+ const depositEvents = (await depositsService.updateEvents()).events as DepositsEvents[];
+
+ // If we have MERKLE_WORKER_PATH run worker at background otherwise resolve it here
+ const depositTreePromise = await (async () => {
+ if (MERKLE_WORKER_PATH) {
+ return () => merkleTreeService.verifyTree({ events: depositEvents }) as Promise;
+ }
+ return (await merkleTreeService.verifyTree({ events: depositEvents })) as MerkleTree;
+ })();
+
+ const [withdrawalEvents] = await Promise.all([
+ withdrawalsService.updateEvents().then(({ events }) => events as WithdrawalsEvents[]),
+ typeof depositTreePromise === 'function' ? depositTreePromise() : depositTreePromise,
+ ]);
+
+ const depositEvent = depositEvents.find(({ commitment }) => commitment === commitmentHex);
+
+ const withdrawalEvent = withdrawalEvents.find(({ nullifierHash }) => nullifierHash === nullifierHex);
+
+ const complianceTable = new Table();
+ complianceTable.push([{ colSpan: 2, content: 'Compliance Info', hAlign: 'center' }]);
+
+ if (!depositEvent) {
+ complianceTable.push([{ colSpan: 2, content: 'Deposit', hAlign: 'center' }], ['Deposit', 'Not Found']);
+ } else {
+ const depositDate = new Date(depositEvent.timestamp * 1000);
+
+ complianceTable.push(
+ [{ colSpan: 2, content: 'Deposit', hAlign: 'center' }],
+ ['Deposit', `${amount} ${currency.toUpperCase()}`],
+ [
+ 'Date',
+ `${depositDate.toLocaleDateString()} ${depositDate.toLocaleTimeString()} (${moment.unix(depositEvent.timestamp).fromNow()})`,
+ ],
+ ['From', depositEvent.from],
+ ['Transaction', depositEvent.transactionHash],
+ ['Commitment', commitmentHex],
+ ['Spent', Boolean(withdrawalEvent)],
+ );
+ }
+
+ if (withdrawalEvent) {
+ const withdrawalDate = new Date(withdrawalEvent.timestamp * 1000);
+
+ complianceTable.push(
+ [{ colSpan: 2, content: 'Withdraw', hAlign: 'center' }],
+ ['Withdrawal', `${amount} ${currency.toUpperCase()}`],
+ ['Relayer Fee', `${formatUnits(withdrawalEvent.fee, decimals)} ${currency.toUpperCase()}`],
+ [
+ 'Date',
+ `${withdrawalDate.toLocaleDateString()} ${withdrawalDate.toLocaleTimeString()} (${moment.unix(withdrawalEvent.timestamp).fromNow()})`,
+ ],
+ ['To', withdrawalEvent.to],
+ ['Transaction', withdrawalEvent.transactionHash],
+ ['Nullifier', nullifierHex],
+ );
+ }
+
+ console.log('\n\n' + complianceTable.toString() + '\n');
+
+ process.exit(0);
+ });
+
+ program
+ .command('syncEvents')
+ .description('Sync the local cache file of tornado cash events.\n\n')
+ .argument('[netId]', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
+ .argument('[currency]', 'Currency to sync events')
+ .action(
+ async (
+ netIdOpts: number | string | undefined,
+ currencyOpts: string | undefined,
+ cmdOptions: commonProgramOptions,
+ ) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc } = options;
+
+ const networks = netIdOpts ? [netIdOpts] : enabledChains;
+
+ for (const netId of networks) {
+ const config = networkConfig[`netId${netId}`];
+ const {
+ tornadoSubgraph,
+ registrySubgraph,
+ tokens,
+ routerContract,
+ registryContract,
+ ['governance.contract.tornadocash.eth']: governanceContract,
+ deployedBlock,
+ constants: { GOVERNANCE_BLOCK, REGISTRY_BLOCK, ENCRYPTED_NOTES_BLOCK },
+ } = config;
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+ const graphApi = getProgramGraphAPI(options, config);
+
+ if (governanceContract) {
+ const governanceService = new NodeGovernanceService({
+ netId,
+ provider,
+ // to-do connect governance with subgraph
+ graphApi: '',
+ subgraphName: '',
+ Governance: Governance__factory.connect(governanceContract, provider),
+ deployedBlock: GOVERNANCE_BLOCK,
+ fetchDataOptions,
+ cacheDirectory: EVENTS_DIR,
+ userDirectory: SAVED_DIR,
+ });
+
+ await governanceService.updateEvents();
+ }
+
+ if (registryContract) {
+ const registryService = new NodeRegistryService({
+ netId,
+ provider,
+ graphApi,
+ subgraphName: registrySubgraph,
+ RelayerRegistry: RelayerRegistry__factory.connect(registryContract, provider),
+ deployedBlock: REGISTRY_BLOCK,
+ fetchDataOptions,
+ cacheDirectory: EVENTS_DIR,
+ userDirectory: SAVED_DIR,
+ });
+
+ await registryService.updateEvents();
+ }
+
+ const encryptedNotesService = new NodeEncryptedNotesService({
+ netId,
+ provider,
+ graphApi,
+ subgraphName: tornadoSubgraph,
+ Router: TornadoRouter__factory.connect(routerContract, provider),
+ deployedBlock: ENCRYPTED_NOTES_BLOCK,
+ fetchDataOptions,
+ cacheDirectory: EVENTS_DIR,
+ userDirectory: SAVED_DIR,
+ });
+
+ await encryptedNotesService.updateEvents();
+
+ const currencies = currencyOpts ? [currencyOpts.toLowerCase()] : Object.keys(tokens);
+
+ for (const currency of currencies) {
+ const currencyConfig = tokens[currency];
+ // Now load the denominations and address
+ const amounts = Object.keys(currencyConfig.instanceAddress);
+
+ // And now sync
+ for (const amount of amounts) {
+ const instanceAddress = currencyConfig.instanceAddress[amount];
+ const Tornado = Tornado__factory.connect(instanceAddress, provider);
+
+ const depositsServiceConstructor = {
+ netId,
+ provider,
+ graphApi,
+ subgraphName: tornadoSubgraph,
+ Tornado,
+ amount,
+ currency,
+ deployedBlock,
+ fetchDataOptions,
+ cacheDirectory: EVENTS_DIR,
+ userDirectory: SAVED_DIR,
+ };
+
+ const depositsService = new NodeDepositsService({
+ ...depositsServiceConstructor,
+ type: 'Deposit',
+ });
+
+ const withdrawalsService = new NodeDepositsService({
+ ...depositsServiceConstructor,
+ type: 'Withdrawal',
+ });
+
+ const merkleTreeService = new MerkleTreeService({
+ netId,
+ amount,
+ currency,
+ Tornado,
+ merkleWorkerPath: MERKLE_WORKER_PATH,
+ });
+
+ const depositEvents = (await depositsService.updateEvents()).events;
+
+ // If we have MERKLE_WORKER_PATH run worker at background otherwise resolve it here
+ const depositTreePromise = await (async () => {
+ if (MERKLE_WORKER_PATH) {
+ return () =>
+ merkleTreeService.verifyTree({ events: depositEvents as DepositsEvents[] }) as Promise;
+ }
+ return (await merkleTreeService.verifyTree({
+ events: depositEvents as DepositsEvents[],
+ })) as MerkleTree;
+ })();
+
+ await Promise.all([
+ withdrawalsService.updateEvents(),
+ typeof depositTreePromise === 'function' ? depositTreePromise() : depositTreePromise,
+ ]);
+ }
+ }
+ }
+
+ process.exit(0);
+ },
+ );
+
+ program
+ .command('relayers')
+ .description('List all registered relayers from the tornado cash registry.\n\n')
+ .argument('', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
+ .action(async (netIdOpts: number | string, cmdOptions: commonProgramOptions) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+
+ const allRelayers = await getProgramRelayer({
+ options,
+ fetchDataOptions,
+ netId: netIdOpts,
+ });
+
+ const validRelayers = allRelayers.validRelayers as RelayerInfo[];
+ const invalidRelayers = allRelayers.invalidRelayers as RelayerError[];
+
+ const relayersTable = new Table();
+
+ relayersTable.push(
+ [{ colSpan: 8, content: 'Relayers', hAlign: 'center' }],
+ [
+ 'netId',
+ 'url',
+ 'ensName',
+ 'stakeBalance',
+ 'relayerAddress',
+ 'rewardAccount',
+ 'currentQueue',
+ 'serviceFee',
+ ].map((content) => ({ content: colors.red.bold(content) })),
+ ...validRelayers.map(
+ ({ netId, url, ensName, stakeBalance, relayerAddress, rewardAccount, currentQueue, tornadoServiceFee }) => {
+ return [
+ netId,
+ url,
+ ensName,
+ stakeBalance ? `${Number(formatEther(stakeBalance)).toFixed(5)} TORN` : '',
+ relayerAddress,
+ rewardAccount,
+ currentQueue,
+ `${tornadoServiceFee}%`,
+ ];
+ },
+ ),
+ );
+
+ const invalidRelayersTable = new Table();
+
+ invalidRelayersTable.push(
+ [{ colSpan: 3, content: 'Invalid Relayers', hAlign: 'center' }],
+ ['hostname', 'relayerAddress', 'errorMessage'].map((content) => ({ content: colors.red.bold(content) })),
+ ...invalidRelayers.map(({ hostname, relayerAddress, errorMessage }) => {
+ return [hostname, relayerAddress, errorMessage ? substring(errorMessage, 40) : ''];
+ }),
+ );
+
+ console.log(relayersTable.toString() + '\n');
+ console.log(invalidRelayersTable.toString() + '\n');
+
+ process.exit(0);
+ });
+
+ program
+ .command('send')
+ .description('Send ETH or ERC20 token to address.\n\n')
+ .argument('', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
+ .argument('', 'To address', parseAddress)
+ .argument('[amount]', 'Sending amounts', parseNumber)
+ .argument('[token]', 'ERC20 Token Contract to check Token Balance', parseAddress)
+ .action(
+ async (
+ netId: string | number,
+ to: string,
+ amountArgs: number | undefined,
+ tokenArgs: string | undefined,
+ cmdOptions: commonProgramOptions,
+ ) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc, token: tokenOpts } = options;
+
+ const config = networkConfig[`netId${netId}`];
+
+ const { currencyName, multicall: multicallAddress } = config;
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const signer = getProgramSigner({ options, provider });
+
+ if (!signer) {
+ throw new Error(
+ 'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured',
+ );
+ }
+
+ const tokenAddress = tokenArgs ? parseAddress(tokenArgs) : tokenOpts;
+
+ const Multicall = Multicall__factory.connect(multicallAddress, provider);
+ const Token = (tokenAddress ? ERC20__factory.connect(tokenAddress, signer) : undefined) as ERC20;
+
+ // Fetching feeData or nonce is unnecessary however we do this to estimate transfer amounts
+ const [feeData, nonce, [{ balance: ethBalance }, tokenResults]] = await Promise.all([
+ provider.getFeeData(),
+ provider.getTransactionCount(signer.address, 'pending'),
+ getTokenBalances({
+ provider,
+ Multicall,
+ currencyName,
+ userAddress: signer.address,
+ tokenAddresses: tokenAddress ? [tokenAddress] : [],
+ }),
+ ]);
+
+ const {
+ symbol: tokenSymbol,
+ decimals: tokenDecimals,
+ balance: tokenBalance,
+ }: tokenBalances = tokenResults || {};
+
+ const txType = feeData.maxFeePerGas ? 2 : 0;
+ const txGasPrice = feeData.maxFeePerGas
+ ? feeData.maxFeePerGas + (feeData.maxPriorityFeePerGas || BigInt(0))
+ : feeData.gasPrice || BigInt(0);
+ /* eslint-disable prettier/prettier */
+ const txFees = feeData.maxFeePerGas
+ ? {
+ maxFeePerGas: feeData.maxFeePerGas,
+ maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
+ }
+ : {
+ gasPrice: feeData.gasPrice,
+ };
+
+ let toSend: bigint;
+
+ if (amountArgs) {
+ if (tokenAddress) {
+ toSend = parseUnits(`${amountArgs}`, tokenDecimals);
+
+ if (toSend > tokenBalance) {
+ const errMsg = `Invalid ${tokenSymbol} balance, wants ${amountArgs} have ${formatUnits(tokenBalance, tokenDecimals)}`;
+ throw new Error(errMsg);
+ }
+ } else {
+ toSend = parseEther(`${amountArgs}`);
+
+ if (toSend > ethBalance) {
+ const errMsg = `Invalid ${currencyName} balance, wants ${amountArgs} have ${formatEther(ethBalance)}`;
+ throw new Error(errMsg);
+ }
+ }
+ } else {
+ if (tokenAddress) {
+ toSend = tokenBalance;
+ } else {
+ const initCost = txGasPrice * BigInt('400000');
+ toSend = ethBalance - initCost;
+
+ const estimatedGas = await provider.estimateGas({
+ type: txType,
+ from: signer.address,
+ to,
+ value: toSend,
+ nonce,
+ ...txFees,
+ });
+
+ const bumpedGas = (estimatedGas !== BigInt(21000) && signer.gasLimitBump
+ ? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000)
+ : estimatedGas
+ );
+
+ toSend = ethBalance - txGasPrice * bumpedGas;
+ }
+ }
+
+ await programSendTransaction({
+ signer,
+ options,
+ populatedTransaction: tokenAddress
+ ? await Token.transfer.populateTransaction(to, toSend)
+ : await signer.populateTransaction({
+ type: txType,
+ from: signer.address,
+ to,
+ value: toSend,
+ nonce,
+ ...txFees,
+ }),
+ });
+ /* eslint-enable prettier/prettier */
+
+ process.exit(0);
+ },
+ );
+
+ program
+ .command('balance')
+ .description('Check ETH and ERC20 balance.\n\n')
+ .argument('', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber)
+ .argument('[address]', 'ETH Address to check balance', parseAddress)
+ .argument('[token]', 'ERC20 Token Contract to check Token Balance', parseAddress)
+ .action(
+ async (
+ netId: string | number,
+ addressArgs: string | undefined,
+ tokenArgs: string | undefined,
+ cmdOptions: commonProgramOptions,
+ ) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc, token: tokenOpts } = options;
+
+ const config = networkConfig[`netId${netId}`];
+
+ const {
+ currencyName,
+ multicall: multicallAddress,
+ ['torn.contract.tornadocash.eth']: tornTokenAddress,
+ tokens,
+ } = config;
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const userAddress = addressArgs ? parseAddress(addressArgs) : getProgramSigner({ options, provider })?.address;
+ const tokenAddress = tokenArgs ? parseAddress(tokenArgs) : tokenOpts;
+
+ if (!userAddress) {
+ throw new Error('Address is required however no user address is supplied');
+ }
+
+ const Multicall = Multicall__factory.connect(multicallAddress, provider);
+
+ const tokenAddresses = Object.values(tokens)
+ .map(({ tokenAddress }) => tokenAddress)
+ .filter((t) => t) as string[];
+
+ if (tornTokenAddress) {
+ tokenAddresses.push(tornTokenAddress);
+ }
+
+ const tokenBalances = await getTokenBalances({
+ provider,
+ Multicall,
+ currencyName,
+ userAddress,
+ tokenAddresses: [...(tokenAddress ? [tokenAddress] : tokenAddresses)],
+ });
+
+ const balanceTable = new Table({ head: ['Token', 'Contract Address', 'Balance'] });
+
+ balanceTable.push(
+ [{ colSpan: 3, content: `User: ${userAddress}`, hAlign: 'center' }],
+ ...tokenBalances.map(({ address, name, symbol, decimals, balance }) => {
+ return [`${name} (${symbol})`, address, `${formatUnits(balance, decimals)} ${symbol}`];
+ }),
+ );
+
+ console.log(balanceTable.toString());
+
+ process.exit(0);
+ },
+ );
+
+ program
+ .command('sign')
+ .description('Sign unsigned transaction with signer.\n\n')
+ .argument('', 'Unsigned Transaction')
+ .action(async (unsignedTx: string, cmdOptions: commonProgramOptions) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc } = options;
+
+ const deserializedTx = Transaction.from(unsignedTx).toJSON();
+
+ const netId = Number(deserializedTx.chainId);
+
+ const config = networkConfig[`netId${netId}`];
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const signer = getProgramSigner({ options, provider });
+
+ if (!signer || signer instanceof VoidSigner) {
+ throw new Error('Signer not defined or not signable signer');
+ }
+
+ await programSendTransaction({
+ signer,
+ options,
+ populatedTransaction: deserializedTx,
+ });
+ /* eslint-enable prettier/prettier */
+
+ process.exit(0);
+ });
+
+ program
+ .command('broadcast')
+ .description('Broadcast signed transaction.\n\n')
+ .argument('', 'Signed Transaction')
+ .action(async (signedTx: string, cmdOptions: commonProgramOptions) => {
+ const { options, fetchDataOptions } = await getProgramOptions(cmdOptions);
+ const { rpc } = options;
+
+ const netId = Number(Transaction.from(signedTx).chainId);
+
+ if (!netId) {
+ throw new Error('NetId for the transaction is invalid, this command only supports EIP-155 transactions');
+ }
+
+ const config = networkConfig[`netId${netId}`];
+
+ const provider = getProgramProvider(netId, rpc, config, {
+ ...fetchDataOptions,
+ });
+
+ const { hash } = await provider.broadcastTransaction(signedTx);
+
+ console.log(`\nBroadcastd tx: ${hash}\n`);
+
+ process.exit(0);
+ });
+
+ // common options
+ /* eslint-disable prettier/prettier */
+ program.commands.forEach((cmd) => {
+ cmd.option('-r, --rpc ', 'The RPC that CLI should interact with', parseUrl);
+ cmd.option('-e, --eth-rpc ', 'The Ethereum Mainnet RPC that CLI should interact with', parseUrl);
+ cmd.option('-g, --graph ', 'The Subgraph API that CLI should interact with', parseUrl);
+ cmd.option('-G, --eth-graph ', 'The Ethereum Mainnet Subgraph API that CLI should interact with', parseUrl);
+ cmd.option('-d, --disable-graph', 'Disable Graph API - Does not enable Subgraph API and use only local RPC as an event source');
+ cmd.option('-R, --relayer ', 'Withdraw via relayer (Should be either .eth name or URL)', parseRelayer);
+ cmd.option('-w, --wallet-withdrawal', 'Withdrawal via wallet (Should not be linked with deposits)');
+ cmd.option('-T, --tor-port ', 'Optional tor port', parseNumber);
+ cmd.option('-t, --token ', 'Token Contract address to view token balance', parseAddress);
+ cmd.option(
+ '-v, --view-only ',
+ 'Wallet address to view balance or to create unsigned transactions',
+ parseAddress,
+ );
+ cmd.option(
+ '-m, --mnemonic ',
+ 'Wallet BIP39 Mnemonic Phrase - If you didn\'t add it to .env file and it is needed for operation',
+ parseMnemonic,
+ );
+ cmd.option('-i, --mnemonic-index ', 'Optional wallet mnemonic index', parseNumber);
+ cmd.option(
+ '-p, --private-key ',
+ 'Wallet private key - If you didn\'t add it to .env file and it is needed for operation',
+ parseKey,
+ );
+ cmd.option(
+ '-n, --non-interactive',
+ 'No confirmation mode - Does not show prompt for confirmation and allow to use scripts non-interactive',
+ );
+ cmd.option('-l, --local-rpc', 'Local node mode - Does not submit signed transaction to the node');
+ });
+ /* eslint-enable prettier/prettier */
+
+ return program;
+}
diff --git a/src/services/batch.ts b/src/services/batch.ts
new file mode 100644
index 0000000..aee3532
--- /dev/null
+++ b/src/services/batch.ts
@@ -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 {
+ 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[] {
+ 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 {
+ 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 {
+ 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[] {
+ 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 {
+ 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 {
+ 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[] {
+ return batchArray.map(async (event: EventInput, index: number) => {
+ await sleep(20 * index);
+
+ return this.getPastEvents(event);
+ });
+ }
+
+ async getBatchEvents({ fromBlock, toBlock, type = '*' }: EventInput): Promise {
+ 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;
+ }
+}
diff --git a/src/services/data.ts b/src/services/data.ts
new file mode 100644
index 0000000..e8b186c
--- /dev/null
+++ b/src/services/data.ts
@@ -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 {
+ try {
+ await stat(fileOrDir);
+
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function zipAsync(file: AsyncZippable): Promise {
+ 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 {
+ return new Promise((res, rej) => {
+ unzip(data, {}, (err, data) => {
+ if (err) {
+ rej(err);
+ return;
+ }
+ res(data);
+ });
+ });
+}
+
+export async function saveEvents({
+ 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({
+ name,
+ userDirectory,
+ deployedBlock,
+}: {
+ name: string;
+ userDirectory: string;
+ deployedBlock: number;
+}): Promise> {
+ 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({
+ name,
+ cacheDirectory,
+ deployedBlock,
+}: {
+ name: string;
+ cacheDirectory: string;
+ deployedBlock: number;
+}): Promise> {
+ 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,
+ };
+ }
+}
diff --git a/src/services/deposits.ts b/src/services/deposits.ts
new file mode 100644
index 0000000..1edf379
--- /dev/null
+++ b/src/services/deposits.ts
@@ -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 {
+ 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 {
+ 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 {
+ const noteRegex = /tornado-(?\w+)-(?[\d.]+)-(?\d+)-0x(?[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-(?\w+)-(?[\d.]+)-(?\d+)-0x(?[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,
+ );
+ }
+}
diff --git a/src/services/events/base.ts b/src/services/events/base.ts
new file mode 100644
index 0000000..21183fd
--- /dev/null
+++ b/src/services/events/base.ts
@@ -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 {
+ 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[0]) {}
+
+ updateBlockProgress({ percentage, currentIndex, totalIndex }: Parameters[0]) {}
+
+ updateTransactionProgress({ percentage, currentIndex, totalIndex }: Parameters[0]) {}
+
+ updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters[0]) {}
+ /* eslint-enable @typescript-eslint/no-unused-vars */
+
+ async formatEvents(events: EventLog[]): Promise {
+ // 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> {
+ return {
+ events: [],
+ lastBlock: null,
+ };
+ }
+
+ async getEventsFromCache(): Promise> {
+ return {
+ events: [],
+ lastBlock: null,
+ };
+ }
+
+ async getSavedEvents(): Promise> {
+ 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> {
+ 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;
+ return {
+ events,
+ lastBlock: lastSyncBlock,
+ };
+ }
+
+ async getEventsFromRpc({
+ fromBlock,
+ toBlock,
+ }: {
+ fromBlock: number;
+ toBlock?: number;
+ }): Promise> {
+ 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> {
+ 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) {}
+
+ /**
+ * Handle saving events
+ */
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ async saveEvents({ events, lastBlock }: BaseEvents) {}
+
+ /**
+ * 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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> {
+ // 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 {
+ 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 {
+ return (await this.updateEvents()).events;
+ }
+}
diff --git a/src/services/events/index.ts b/src/services/events/index.ts
new file mode 100644
index 0000000..c386db6
--- /dev/null
+++ b/src/services/events/index.ts
@@ -0,0 +1,3 @@
+export * from './types';
+export * from './base';
+export * from './node';
diff --git a/src/services/events/node.ts b/src/services/events/node.ts
new file mode 100644
index 0000000..9efc4a2
--- /dev/null
+++ b/src/services/events/node.ts
@@ -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[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[0]) {
+ if (totalIndex) {
+ console.log(`Fetched ${currentIndex} deposit txs of ${totalIndex}`);
+ }
+ }
+
+ updateBlockProgress({ currentIndex, totalIndex }: Parameters[0]) {
+ if (totalIndex) {
+ console.log(`Fetched ${currentIndex} withdrawal blocks of ${totalIndex}`);
+ }
+ }
+
+ updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters[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({
+ 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({
+ 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) {
+ 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({
+ 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[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[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({
+ 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({
+ 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) {
+ 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({
+ 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[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[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[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({
+ 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({
+ 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) {
+ 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({
+ 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[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[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({
+ 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({
+ 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) {
+ 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({
+ name: instanceName,
+ userDirectory: this.userDirectory,
+ events,
+ });
+ }
+ }
+}
diff --git a/src/services/events/types.ts b/src/services/events/types.ts
new file mode 100644
index 0000000..00f8ac5
--- /dev/null
+++ b/src/services/events/types.ts
@@ -0,0 +1,69 @@
+import { RelayerParams } from '../relayerClient';
+
+export interface BaseEvents {
+ events: T[];
+ lastBlock: number | null;
+}
+
+export interface BaseGraphEvents {
+ 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;
+};
diff --git a/src/services/fees.ts b/src/services/fees.ts
new file mode 100644
index 0000000..d7716bf
--- /dev/null
+++ b/src/services/fees.ts
@@ -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 {
+ 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)
+ );
+ }
+}
diff --git a/src/services/graphql/index.ts b/src/services/graphql/index.ts
new file mode 100644
index 0000000..2fb69f9
--- /dev/null
+++ b/src/services/graphql/index.ts
@@ -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({
+ graphApi,
+ subgraphName,
+ query,
+ variables,
+ fetchDataOptions,
+}: queryGraphParams): Promise {
+ 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 {
+ try {
+ const {
+ deposits,
+ _meta: {
+ block: { number: lastSyncBlock },
+ },
+ } = await queryGraph({
+ 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 {
+ try {
+ const {
+ _meta: {
+ block: { number: lastSyncBlock },
+ hasIndexingErrors,
+ },
+ } = await queryGraph({
+ 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 {
+ return queryGraph({
+ graphApi,
+ subgraphName,
+ query: GET_REGISTERED,
+ variables: {
+ first,
+ fromBlock,
+ },
+ fetchDataOptions,
+ });
+}
+
+export async function getAllRegisters({
+ graphApi,
+ subgraphName,
+ fromBlock,
+ fetchDataOptions,
+ onProgress,
+}: getRegistersParams): Promise> {
+ 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 {
+ return queryGraph({
+ graphApi,
+ subgraphName,
+ query: GET_DEPOSITS,
+ variables: {
+ currency,
+ amount,
+ first,
+ fromBlock,
+ },
+ fetchDataOptions,
+ });
+}
+
+export async function getAllDeposits({
+ graphApi,
+ subgraphName,
+ currency,
+ amount,
+ fromBlock,
+ fetchDataOptions,
+ onProgress,
+}: getDepositsParams): Promise> {
+ 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 {
+ return queryGraph({
+ graphApi,
+ subgraphName,
+ query: GET_WITHDRAWALS,
+ variables: {
+ currency,
+ amount,
+ first,
+ fromBlock,
+ },
+ fetchDataOptions,
+ });
+}
+
+export async function getAllWithdrawals({
+ graphApi,
+ subgraphName,
+ currency,
+ amount,
+ fromBlock,
+ fetchDataOptions,
+ onProgress,
+}: getWithdrawalParams): Promise> {
+ 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 {
+ try {
+ const {
+ noteAccounts: events,
+ _meta: {
+ block: { number: lastSyncBlock },
+ },
+ } = await queryGraph({
+ 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 {
+ return queryGraph({
+ graphApi,
+ subgraphName,
+ query: GET_ENCRYPTED_NOTES,
+ variables: {
+ first,
+ fromBlock,
+ },
+ fetchDataOptions,
+ });
+}
+
+export async function getAllEncryptedNotes({
+ graphApi,
+ subgraphName,
+ fromBlock,
+ fetchDataOptions,
+ onProgress,
+}: getEncryptedNotesParams): Promise> {
+ 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,
+ };
+ }
+}
diff --git a/src/services/graphql/queries.ts b/src/services/graphql/queries.ts
new file mode 100644
index 0000000..fcb3a97
--- /dev/null
+++ b/src/services/graphql/queries.ts
@@ -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
+ }
+ }
+`;
diff --git a/src/services/index.ts b/src/services/index.ts
new file mode 100644
index 0000000..764eed9
--- /dev/null
+++ b/src/services/index.ts
@@ -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';
diff --git a/src/services/merkleTree.ts b/src/services/merkleTree.ts
new file mode 100644
index 0000000..b186119
--- /dev/null
+++ b/src/services/merkleTree.ts
@@ -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;
+
+ 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;
+
+ 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;
+ }
+}
diff --git a/src/services/mimc.ts b/src/services/mimc.ts
new file mode 100644
index 0000000..e731137
--- /dev/null
+++ b/src/services/mimc.ts
@@ -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;
+ mimcPromise: Promise;
+
+ 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();
diff --git a/src/services/multicall.ts b/src/services/multicall.ts
new file mode 100644
index 0000000..61c0ba2
--- /dev/null
+++ b/src/services/multicall.ts
@@ -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;
+}
diff --git a/src/services/networkConfig.ts b/src/services/networkConfig.ts
new file mode 100644
index 0000000..425a969
--- /dev/null
+++ b/src/services/networkConfig.ts
@@ -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;
diff --git a/src/services/parser.ts b/src/services/parser.ts
new file mode 100644
index 0000000..af068a4
--- /dev/null
+++ b/src/services/parser.ts
@@ -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;
+}
diff --git a/src/services/pedersen.ts b/src/services/pedersen.ts
new file mode 100644
index 0000000..8440621
--- /dev/null
+++ b/src/services/pedersen.ts
@@ -0,0 +1,32 @@
+import { BabyJub, PedersenHash, Point, buildPedersenHash } from 'circomlibjs';
+
+export class Pedersen {
+ pedersenHash?: PedersenHash;
+ babyJub?: BabyJub;
+ pedersenPromise: Promise;
+
+ 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 {
+ const [hash] = (await pedersen.unpackPoint(buffer)) as Point;
+ return pedersen.toStringBuffer(hash);
+}
diff --git a/src/services/prices.ts b/src/services/prices.ts
new file mode 100644
index 0000000..c63efdc
--- /dev/null
+++ b/src/services/prices.ts
@@ -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 {
+ // 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],
+ })),
+ );
+ }
+}
diff --git a/src/services/providers.ts b/src/services/providers.ts
new file mode 100644
index 0000000..d91900a
--- /dev/null
+++ b/src/services/providers.ts
@@ -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;
+
+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 {
+ 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 = (
+ _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;
+
+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 {
+ 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);
+ }
+}
diff --git a/src/services/relayerClient.ts b/src/services/relayerClient.ts
new file mode 100644
index 0000000..2ad6ef1
--- /dev/null
+++ b/src/services/relayerClient.ts
@@ -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 =
+ /^(?0|[1-9]\d*)\.(?0|[1-9]\d*)\.(?0|[1-9]\d*)(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?[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 {
+ 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 {
+ 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);
+ }
+ }
+}
diff --git a/src/services/schemas/index.ts b/src/services/schemas/index.ts
new file mode 100644
index 0000000..b7059a2
--- /dev/null
+++ b/src/services/schemas/index.ts
@@ -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';
diff --git a/src/services/schemas/jobs.ts b/src/services/schemas/jobs.ts
new file mode 100644
index 0000000..30624a1
--- /dev/null
+++ b/src/services/schemas/jobs.ts
@@ -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'],
+};
diff --git a/src/services/schemas/status.ts b/src/services/schemas/status.ts
new file mode 100644
index 0000000..ea0ef49
--- /dev/null
+++ b/src/services/schemas/status.ts
@@ -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;
+}
diff --git a/src/services/tokens.ts b/src/services/tokens.ts
new file mode 100644
index 0000000..590bc06
--- /dev/null
+++ b/src/services/tokens.ts
@@ -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 {
+ 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,
+ ];
+}
diff --git a/src/services/utils.ts b/src/services/utils.ts
new file mode 100644
index 0000000..a1b8411
--- /dev/null
+++ b/src/services/utils.ts
@@ -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 = (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)}`;
+}
diff --git a/src/services/websnark.ts b/src/services/websnark.ts
new file mode 100644
index 0000000..99cb49a
--- /dev/null
+++ b/src/services/websnark.ts
@@ -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 {
+ 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 };
+}
diff --git a/tsconfig.json b/tsconfig.json
index 18b7c6d..171aecd 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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 ''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/**/*"]
}