forked from tornadocash/tornado-cli
Added basic service
This commit is contained in:
parent
ef1c99bf57
commit
e29be4a1e6
26
.env.example
26
.env.example
@ -1,11 +1,15 @@
|
|||||||
MERKLE_TREE_HEIGHT=20
|
RPC_URL=https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
|
||||||
# in wei
|
ETHRPC_URL=https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
|
||||||
ETH_AMOUNT=100000000000000000
|
GRAPH_URL=https://api.thegraph.com
|
||||||
# check config.js
|
ETHGRAPH_URL=https://api.thegraph.com
|
||||||
TOKEN_AMOUNT=100000000000000000
|
DISABLE_GRAPH=true
|
||||||
ERC20_TOKEN=
|
RELAYER=torn-city.eth
|
||||||
|
WALLET_WITHDRAWAL=true
|
||||||
PRIVATE_KEY=
|
TOR_PORT=9150
|
||||||
|
TOKEN=0x77777FeDdddFfC19Ff86DB637967013e6C6A116C
|
||||||
#ERC20_TOKEN=0xf3e0d7bf58c5d455d31ef1c2d5375904df525105
|
VIEW_ONLY=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
||||||
#TOKEN_AMOUNT=1000000
|
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
|
20
.eslintrc.js
20
.eslintrc.js
@ -5,7 +5,11 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended"
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"prettier",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
@ -26,9 +30,18 @@ module.exports = {
|
|||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@typescript-eslint"
|
"@typescript-eslint",
|
||||||
|
"prettier"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
singleQuote: true,
|
||||||
|
printWidth: 120
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"import/order": ["error"],
|
||||||
"indent": [
|
"indent": [
|
||||||
"error",
|
"error",
|
||||||
2
|
2
|
||||||
@ -44,6 +57,7 @@ module.exports = {
|
|||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
"always"
|
"always"
|
||||||
]
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
dist/* linguist-vendored
|
||||||
|
static/* linguist-vendored
|
565
README.md
565
README.md
@ -1,18 +1,38 @@
|
|||||||
# Tornado-CLI
|
<div class="hero" align="center">
|
||||||
|
|
||||||
Command line tool to interact with [Tornado Cash](https://tornado.ws).
|
<img src="./logo2.png">
|
||||||
|
|
||||||
### How to install tornado cli
|
# Tornado CLI
|
||||||
|
|
||||||
Download and install [node.js](https://nodejs.org/en/download/).
|
Modern Toolsets for [Privacy Pools](https://www.forbes.com/sites/tomerniv/2023/09/07/privacy-pools-bridging-the-gap-between-blockchain-and-regulatory-compliance) on Ethereum
|
||||||
|
|
||||||
You also need to install C++ build tools in order to do 'npm install', for more information please checkout https://github.com/nodejs/node-gyp#on-unix.
|
[![Telegram Badge](https://img.shields.io/badge/Join%20Group-telegram?style=flat&logo=telegram&color=blue&link=https%3A%2F%2Ft.me%2Ftornadocli)](https://t.me/tornadocli) [![Element Badge](https://img.shields.io/badge/Join%20Element%20Chat-Element?style=flat&logo=element&color=green&link=https%3A%2F%2Felement.tornadocash.social%2F)](https://element.tornadocash.social) [![Discourse Badge](https://img.shields.io/badge/Discourse-Discourse?style=flat&logo=Discourse&color=black&link=https%3A%2F%2Fforum.tornado.ws%2F)](https://forum.tornado.ws/)
|
||||||
|
|
||||||
- For Windows: https://stackoverflow.com/a/64224475
|
</div>
|
||||||
|
|
||||||
- For MacOS: Install XCode Command Line Tools
|
### About Tornado CLI
|
||||||
|
|
||||||
- For Linux: Install make & gcc, for ubuntu `$ sudo apt-get install -y build-essentials`
|
Tornado CLI is a complete rewrite of old tornado-cli command line tool with the following changes
|
||||||
|
|
||||||
|
+ Rewritten to [TypeScript](https://www.typescriptlang.org/)
|
||||||
|
|
||||||
|
+ Built on top of modern tech stacks like [Ethers.js V6](https://docs.ethers.org/v6/) or [TypeChain](https://github.com/dethcrypto/TypeChain)
|
||||||
|
|
||||||
|
+ Creates resource heavy Merkle Trees on a separate thread using Web Workers / Worker Threads
|
||||||
|
|
||||||
|
+ Resilient API requests made by [cross-fetch](https://www.npmjs.com/package/cross-fetch) and retries, especially for Tor Users
|
||||||
|
|
||||||
|
+ Modular design
|
||||||
|
|
||||||
|
### How to install Tornado CLI
|
||||||
|
|
||||||
|
Download and install the latest [Node.js](https://nodejs.org/en/download/) LTS version (Current: 20.x).
|
||||||
|
|
||||||
|
You also need to install [Yarn](https://yarnpkg.com/) 1.x package manager using following command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm i -g yarn
|
||||||
|
```
|
||||||
|
|
||||||
If you have git installed on your system, clone the master branch.
|
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
|
```bash
|
||||||
$ cd tornado-cli
|
$ 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.
|
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`
|
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? ).
|
||||||
2. If you want to use secure, anonymous tor connection add `--tor <torPort>` behind the command.
|
|
||||||
|
You can apply those values with two options
|
||||||
|
|
||||||
|
1. Refer the `VIEW_ONLY` or `MNEMONIC` or `PRIVATE_KEY` value from the `.env.example` file and create a new `.env` file with the appropriate value.
|
||||||
|
|
||||||
|
2. Supply command-line options `--view-only` or `--mnemonic` or `--private-key` during the commands.
|
||||||
|
|
||||||
|
### How to start
|
||||||
|
|
||||||
|
1. `yarn start --help`
|
||||||
|
2. If you want to use a secure, anonymous tor connection add `--tor-port <torPort>` behind the command or add `TOR_PORT` at the `.env` file.
|
||||||
3. Add `PRIVATE_KEY` to `.env` file (optional, only if you want to use it for many operations) - open `.env.example` file, add private key after `PRIVATE_KEY=` and rename file to `.env`.
|
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:
|
#### To deposit:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ node cli.js deposit <currency> <amount> --rpc <rpc url> --tor <torPort> --private-key <private key>
|
$ yarn deposit <netId> <currency> <amount>
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that `--tor <torPort>` is optional, and use `--private-key <private key>` only if you didn't add it to `.env` file.
|
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)
|
||||||
|
|
||||||
For RPC nodes please refer to the list of public RPC nodes below.
|
|
||||||
|
|
||||||
##### Example:
|
##### Example:
|
||||||
|
|
||||||
```bash
|
```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:
|
#### To withdraw:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ node cli.js withdraw <note> <recipient> --rpc <rpc url> --relayer <relayer url> --tor <torPort> --private-key <private key>
|
$ yarn withdraw <note> <recipient>
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that `--relayer <relayer url>`, `--tor <torPort>` and `--rpc <rpc url>` are optional parameters, and use `--private-key <private key>` only if you withdraw without relayer.
|
Note that `--relayer <relayer url>`, `--tor-port <torPort>` and `--rpc-url <rpc url>` are optional parameters, and use `--wallet-withdrawal --private-key <private key>` only if you withdraw without the relayer.
|
||||||
You can don't provide RPC link and withdrawal will be made via default RPC for the chain to which note belongs.
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
##### Example:
|
##### Example:
|
||||||
|
|
||||||
```bash
|
```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
|
$ yarn withdraw tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16
|
||||||
|
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### (Optional) Creating Deposit Notes & Invoices offline
|
### (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.
|
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
|
```bash
|
||||||
$ node cli.js createNote <currency> <amount> <chainId>
|
$ yarn createDeposit <chainId> <currency> <amount>
|
||||||
```
|
```
|
||||||
|
|
||||||
To find out chainId value for your network, refer to https://chainlist.org/.
|
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:
|
##### Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ node cli.js createNote ETH 0.1 5
|
$ yarn createDeposit 1 ETH 0.1
|
||||||
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
|
_____ _ ____ _ ___
|
||||||
|
|_ _|__ _ __ _ __ __ _ __| | ___ / ___| | |_ _|
|
||||||
|
| |/ _ \| '__| '_ \ / _` |/ _` |/ _ \ | | | | | |
|
||||||
|
| | (_) | | | | | | (_| | (_| | (_) | | |___| |___ | |
|
||||||
|
|_|\___/|_| |_| |_|\__,_|\__,_|\___/ \____|_____|___|
|
||||||
|
|
||||||
|
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
#### 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.
|
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
|
```bash
|
||||||
$ node cli.js depositInvoice <invoice> --rpc <rpc url> --tor <tor port>
|
$ node cli.js depositInvoice <invoice>
|
||||||
```
|
```
|
||||||
|
|
||||||
Parameter `--rpc <rpc url>` is optional, if you don't provide it, default RPC (corresponding to note chain) will be used.
|
Parameter `--rpc-url <rpc url>` is optional, if you don't provide it, default RPC (corresponding to note chain) will be used.
|
||||||
|
|
||||||
##### Example:
|
##### Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node cli.js depositInvoice tornadoInvoice-eth-0.1-5-0x1b680c7dda0c2dd1b85f0fe126d49b16ed594b3cd6d5114db5f4593877a6b84f --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --tor 9150
|
yarn depositInvoice tornadoInvoice-eth-0.1-1-0x24d26c7d0381dc34941b6fe9e0d622c7efadc0bfdc9d3f7e8dcb1e490e6ce9ea
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### List of rpc & relayers for withdrawal
|
### List of rpc & relayers for withdrawal
|
||||||
|
|
||||||
```json
|
Refer https://chainlist.org for a full list of available public RPC URLs.
|
||||||
{
|
|
||||||
"netId1": {
|
Note that most of the RPC would censor sanctionded pool contracts.
|
||||||
"rpcUrls": {
|
|
||||||
"publicRpc1": {
|
So either you can use the default RPC or find yourself a suitable one.
|
||||||
"name": "1RPC",
|
|
||||||
"url": "https://1rpc.io/eth"
|
For the list of avaiable relayers, use the `yarn relayers 1` command.
|
||||||
},
|
|
||||||
"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": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
BIN
logo.png
Normal file
BIN
logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
BIN
logo2.png
Normal file
BIN
logo2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
25
package.json
25
package.json
@ -1,19 +1,31 @@
|
|||||||
{
|
{
|
||||||
"name": "tornado-cli",
|
"name": "tornado-cli",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1-alpha",
|
||||||
"description": "",
|
"description": "Modern Toolsets for Privacy Pools on Ethereum",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tornado": "./dist/index.js"
|
"tornado-cli": "./dist/cli.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typechain": "typechain --target ethers-v6 --out-dir src/typechain src/abi/*.json",
|
"typechain": "typechain --target ethers-v6 --out-dir src/typechain src/abi/*.json",
|
||||||
"types": "tsc --declaration --emitDeclarationOnly",
|
"types": "tsc --declaration --emitDeclarationOnly",
|
||||||
"lint": "eslint src/**/*.ts --ext .ts --ignore-pattern src/typechain --fix",
|
"lint": "eslint src/**/*.ts --ext .ts --ignore-pattern src/typechain",
|
||||||
"build": "rollup -c",
|
"build": "yarn types && rollup -c",
|
||||||
"start": "ts-node src/index.ts"
|
"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": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -26,6 +38,7 @@
|
|||||||
".eslintrc.js",
|
".eslintrc.js",
|
||||||
".gitattributes",
|
".gitattributes",
|
||||||
".gitignore",
|
".gitignore",
|
||||||
|
".npmrc",
|
||||||
"rollup.config.mjs",
|
"rollup.config.mjs",
|
||||||
"tsconfig.json",
|
"tsconfig.json",
|
||||||
"yarn.lock"
|
"yarn.lock"
|
||||||
|
@ -2,19 +2,25 @@ import esbuild from 'rollup-plugin-esbuild';
|
|||||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
import json from '@rollup/plugin-json';
|
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 = [
|
const config = [
|
||||||
{
|
{
|
||||||
input: 'src/index.ts',
|
input: 'src/index.ts',
|
||||||
output: [
|
output: [
|
||||||
{
|
{
|
||||||
file: 'dist/index.js',
|
file: pkgJson.main,
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
esModule: false,
|
esModule: false,
|
||||||
banner: '#!/usr/bin/env node\n'
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
external: [],
|
external,
|
||||||
plugins: [
|
plugins: [
|
||||||
esbuild({
|
esbuild({
|
||||||
include: /\.[jt]sx?$/,
|
include: /\.[jt]sx?$/,
|
||||||
@ -26,6 +32,96 @@ const config = [
|
|||||||
commonjs(),
|
commonjs(),
|
||||||
json()
|
json()
|
||||||
],
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/index.ts',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: pkgJson.module,
|
||||||
|
format: "esm",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
external,
|
||||||
|
plugins: [
|
||||||
|
esbuild({
|
||||||
|
include: /\.[jt]sx?$/,
|
||||||
|
minify: false,
|
||||||
|
sourceMap: true,
|
||||||
|
target: 'es2016',
|
||||||
|
}),
|
||||||
|
nodeResolve(),
|
||||||
|
json()
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/cli.ts',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: pkgJson.bin[pkgJson.name],
|
||||||
|
format: "cjs",
|
||||||
|
esModule: false,
|
||||||
|
banner: '#!/usr/bin/env node\n'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
esbuild({
|
||||||
|
include: /\.[jt]sx?$/,
|
||||||
|
minify: false,
|
||||||
|
sourceMap: true,
|
||||||
|
target: 'es2016',
|
||||||
|
}),
|
||||||
|
nodeResolve(),
|
||||||
|
commonjs(),
|
||||||
|
json()
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/merkleTreeWorker.ts',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: 'static/merkleTreeWorker.js',
|
||||||
|
format: "cjs",
|
||||||
|
esModule: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
treeshake: 'smallest',
|
||||||
|
plugins: [
|
||||||
|
esbuild({
|
||||||
|
include: /\.[jt]sx?$/,
|
||||||
|
minify: false,
|
||||||
|
sourceMap: true,
|
||||||
|
target: 'es2016',
|
||||||
|
}),
|
||||||
|
nodeResolve(),
|
||||||
|
commonjs(),
|
||||||
|
json()
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/merkleTreeWorker.ts',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: 'static/merkleTreeWorker.umd.js',
|
||||||
|
format: "umd",
|
||||||
|
esModule: false
|
||||||
|
},
|
||||||
|
],
|
||||||
|
treeshake: 'smallest',
|
||||||
|
external: ['web-worker'],
|
||||||
|
plugins: [
|
||||||
|
esbuild({
|
||||||
|
include: /\.[jt]sx?$/,
|
||||||
|
minify: false,
|
||||||
|
sourceMap: true,
|
||||||
|
target: 'es2016',
|
||||||
|
}),
|
||||||
|
nodeResolve(),
|
||||||
|
commonjs(),
|
||||||
|
json(),
|
||||||
|
replace({
|
||||||
|
'process.browser': 'true'
|
||||||
|
})
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
12
scripts/figlet-font.ts
Normal file
12
scripts/figlet-font.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
let figletStandard = fs.readFileSync('./node_modules/figlet/importable-fonts/Standard.js', { encoding: 'utf8' }) as string;
|
||||||
|
|
||||||
|
figletStandard = figletStandard.replace('export default `', '')
|
||||||
|
|
||||||
|
figletStandard = figletStandard.replace(' `', '')
|
||||||
|
|
||||||
|
fs.writeFileSync('./src/fonts/figletStandard.ts', `
|
||||||
|
export const figletStandard: string = \`${figletStandard}\`;
|
||||||
|
export default figletStandard;
|
||||||
|
`)
|
15
src/cli.ts
Normal file
15
src/cli.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import figlet from 'figlet';
|
||||||
|
import Standard from './fonts/figletStandard';
|
||||||
|
import { tornadoProgram } from './program';
|
||||||
|
|
||||||
|
figlet.parseFont('Standard', Standard);
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
====================================================================
|
||||||
|
|
||||||
|
${figlet.textSync('Tornado CLI', { font: 'Standard' })}
|
||||||
|
|
||||||
|
====================================================================\n
|
||||||
|
`);
|
||||||
|
|
||||||
|
tornadoProgram().parse();
|
2238
src/fonts/figletStandard.ts
Normal file
2238
src/fonts/figletStandard.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,3 +1,2 @@
|
|||||||
import { ethers } from 'ethers';
|
export * from './services';
|
||||||
|
export * from './typechain';
|
||||||
export const provider = new ethers.JsonRpcProvider('http://localhost:8545');
|
|
||||||
|
69
src/merkleTreeWorker.ts
Normal file
69
src/merkleTreeWorker.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any, prettier/prettier */
|
||||||
|
import workerThreads from 'worker_threads';
|
||||||
|
import { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree';
|
||||||
|
import { mimc, isNode } from './services';
|
||||||
|
|
||||||
|
interface WorkData {
|
||||||
|
merkleTreeHeight: number;
|
||||||
|
edge?: TreeEdge;
|
||||||
|
elements: Element[];
|
||||||
|
zeroElement: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nodePostWork() {
|
||||||
|
const { hash: hashFunction } = await mimc.getHash();
|
||||||
|
const { merkleTreeHeight, edge, elements, zeroElement } = workerThreads.workerData as WorkData;
|
||||||
|
|
||||||
|
if (edge) {
|
||||||
|
const merkleTree = new PartialMerkleTree(merkleTreeHeight, edge, elements, {
|
||||||
|
zeroElement,
|
||||||
|
hashFunction,
|
||||||
|
});
|
||||||
|
|
||||||
|
(workerThreads.parentPort as workerThreads.MessagePort).postMessage(merkleTree.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const merkleTree = new MerkleTree(merkleTreeHeight, elements, {
|
||||||
|
zeroElement,
|
||||||
|
hashFunction,
|
||||||
|
});
|
||||||
|
|
||||||
|
(workerThreads.parentPort as workerThreads.MessagePort).postMessage(merkleTree.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNode && workerThreads) {
|
||||||
|
nodePostWork();
|
||||||
|
} else if (!isNode && typeof addEventListener === 'function' && typeof postMessage === 'function') {
|
||||||
|
addEventListener('message', async (e: any) => {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (e.data) {
|
||||||
|
data = e.data;
|
||||||
|
} else {
|
||||||
|
data = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hash: hashFunction } = await mimc.getHash();
|
||||||
|
const { merkleTreeHeight, edge, elements, zeroElement } = data as WorkData;
|
||||||
|
|
||||||
|
if (edge) {
|
||||||
|
const merkleTree = new PartialMerkleTree(merkleTreeHeight, edge, elements, {
|
||||||
|
zeroElement,
|
||||||
|
hashFunction,
|
||||||
|
});
|
||||||
|
|
||||||
|
postMessage(merkleTree.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const merkleTree = new MerkleTree(merkleTreeHeight, elements, {
|
||||||
|
zeroElement,
|
||||||
|
hashFunction,
|
||||||
|
});
|
||||||
|
|
||||||
|
postMessage(merkleTree.toString());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('This browser / environment doesn\'t support workers!');
|
||||||
|
}
|
1751
src/program.ts
Normal file
1751
src/program.ts
Normal file
File diff suppressed because it is too large
Load Diff
339
src/services/batch.ts
Normal file
339
src/services/batch.ts
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
import type { Provider, BlockTag, Block, TransactionResponse, BaseContract, ContractEventName, EventLog } from 'ethers';
|
||||||
|
import { chunk, sleep } from './utils';
|
||||||
|
|
||||||
|
export interface BatchBlockServiceConstructor {
|
||||||
|
provider: Provider;
|
||||||
|
onProgress?: BatchBlockOnProgress;
|
||||||
|
concurrencySize?: number;
|
||||||
|
batchSize?: number;
|
||||||
|
shouldRetry?: boolean;
|
||||||
|
retryMax?: number;
|
||||||
|
retryOn?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BatchBlockOnProgress = ({
|
||||||
|
percentage,
|
||||||
|
currentIndex,
|
||||||
|
totalIndex,
|
||||||
|
}: {
|
||||||
|
percentage: number;
|
||||||
|
currentIndex?: number;
|
||||||
|
totalIndex?: number;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch blocks from web3 provider on batches
|
||||||
|
*/
|
||||||
|
export class BatchBlockService {
|
||||||
|
provider: Provider;
|
||||||
|
onProgress?: BatchBlockOnProgress;
|
||||||
|
concurrencySize: number;
|
||||||
|
batchSize: number;
|
||||||
|
shouldRetry: boolean;
|
||||||
|
retryMax: number;
|
||||||
|
retryOn: number;
|
||||||
|
constructor({
|
||||||
|
provider,
|
||||||
|
onProgress,
|
||||||
|
concurrencySize = 10,
|
||||||
|
batchSize = 10,
|
||||||
|
shouldRetry = true,
|
||||||
|
retryMax = 5,
|
||||||
|
retryOn = 500,
|
||||||
|
}: BatchBlockServiceConstructor) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.onProgress = onProgress;
|
||||||
|
this.concurrencySize = concurrencySize;
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
this.shouldRetry = shouldRetry;
|
||||||
|
this.retryMax = retryMax;
|
||||||
|
this.retryOn = retryOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlock(blockTag: BlockTag): Promise<Block> {
|
||||||
|
const blockObject = await this.provider.getBlock(blockTag);
|
||||||
|
|
||||||
|
// if the provider returns null (which they have corrupted block data for one of their nodes) throw and retry
|
||||||
|
if (!blockObject) {
|
||||||
|
const errMsg = `No block for ${blockTag}`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBatchRequest(batchArray: BlockTag[][]): Promise<Block[]>[] {
|
||||||
|
return batchArray.map(async (blocks: BlockTag[], index: number) => {
|
||||||
|
// send batch requests on milliseconds to avoid including them on a single batch request
|
||||||
|
await sleep(20 * index);
|
||||||
|
|
||||||
|
return (async () => {
|
||||||
|
let retries = 0;
|
||||||
|
let err;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||||||
|
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
|
||||||
|
try {
|
||||||
|
return await Promise.all(blocks.map((b) => this.getBlock(b)));
|
||||||
|
} catch (e) {
|
||||||
|
retries++;
|
||||||
|
err = e;
|
||||||
|
|
||||||
|
// retry on 0.5 seconds
|
||||||
|
await sleep(this.retryOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBatchBlocks(blocks: BlockTag[]): Promise<Block[]> {
|
||||||
|
let blockCount = 0;
|
||||||
|
const results: Block[] = [];
|
||||||
|
|
||||||
|
for (const chunks of chunk(blocks, this.concurrencySize * this.batchSize)) {
|
||||||
|
const chunksResult = (await Promise.all(this.createBatchRequest(chunk(chunks, this.batchSize)))).flat();
|
||||||
|
|
||||||
|
results.push(...chunksResult);
|
||||||
|
|
||||||
|
blockCount += chunks.length;
|
||||||
|
|
||||||
|
if (typeof this.onProgress === 'function') {
|
||||||
|
this.onProgress({
|
||||||
|
percentage: blockCount / blocks.length,
|
||||||
|
currentIndex: blockCount,
|
||||||
|
totalIndex: blocks.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch transactions from web3 provider on batches
|
||||||
|
*/
|
||||||
|
export class BatchTransactionService {
|
||||||
|
provider: Provider;
|
||||||
|
onProgress?: BatchBlockOnProgress;
|
||||||
|
concurrencySize: number;
|
||||||
|
batchSize: number;
|
||||||
|
shouldRetry: boolean;
|
||||||
|
retryMax: number;
|
||||||
|
retryOn: number;
|
||||||
|
constructor({
|
||||||
|
provider,
|
||||||
|
onProgress,
|
||||||
|
concurrencySize = 10,
|
||||||
|
batchSize = 10,
|
||||||
|
shouldRetry = true,
|
||||||
|
retryMax = 5,
|
||||||
|
retryOn = 500,
|
||||||
|
}: BatchBlockServiceConstructor) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.onProgress = onProgress;
|
||||||
|
this.concurrencySize = concurrencySize;
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
this.shouldRetry = shouldRetry;
|
||||||
|
this.retryMax = retryMax;
|
||||||
|
this.retryOn = retryOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTransaction(txHash: string): Promise<TransactionResponse> {
|
||||||
|
const txObject = await this.provider.getTransaction(txHash);
|
||||||
|
|
||||||
|
if (!txObject) {
|
||||||
|
const errMsg = `No transaction for ${txHash}`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return txObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBatchRequest(batchArray: string[][]): Promise<TransactionResponse[]>[] {
|
||||||
|
return batchArray.map(async (txs: string[], index: number) => {
|
||||||
|
await sleep(20 * index);
|
||||||
|
|
||||||
|
return (async () => {
|
||||||
|
let retries = 0;
|
||||||
|
let err;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||||||
|
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
|
||||||
|
try {
|
||||||
|
return await Promise.all(txs.map((tx) => this.getTransaction(tx)));
|
||||||
|
} catch (e) {
|
||||||
|
retries++;
|
||||||
|
err = e;
|
||||||
|
|
||||||
|
// retry on 0.5 seconds
|
||||||
|
await sleep(this.retryOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBatchTransactions(txs: string[]): Promise<TransactionResponse[]> {
|
||||||
|
let txCount = 0;
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
for (const chunks of chunk(txs, this.concurrencySize * this.batchSize)) {
|
||||||
|
const chunksResult = (await Promise.all(this.createBatchRequest(chunk(chunks, this.batchSize)))).flat();
|
||||||
|
|
||||||
|
results.push(...chunksResult);
|
||||||
|
|
||||||
|
txCount += chunks.length;
|
||||||
|
|
||||||
|
if (typeof this.onProgress === 'function') {
|
||||||
|
this.onProgress({ percentage: txCount / txs.length, currentIndex: txCount, totalIndex: txs.length });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchEventServiceConstructor {
|
||||||
|
provider: Provider;
|
||||||
|
contract: BaseContract;
|
||||||
|
onProgress?: BatchEventOnProgress;
|
||||||
|
concurrencySize?: number;
|
||||||
|
blocksPerRequest?: number;
|
||||||
|
shouldRetry?: boolean;
|
||||||
|
retryMax?: number;
|
||||||
|
retryOn?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BatchEventOnProgress = ({
|
||||||
|
percentage,
|
||||||
|
type,
|
||||||
|
fromBlock,
|
||||||
|
toBlock,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
percentage: number;
|
||||||
|
type?: ContractEventName;
|
||||||
|
fromBlock?: number;
|
||||||
|
toBlock?: number;
|
||||||
|
count?: number;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
// To enable iteration only numbers are accepted for fromBlock input
|
||||||
|
export type EventInput = {
|
||||||
|
fromBlock: number;
|
||||||
|
toBlock: number;
|
||||||
|
type: ContractEventName;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch events from web3 provider on bulk
|
||||||
|
*/
|
||||||
|
export class BatchEventsService {
|
||||||
|
provider: Provider;
|
||||||
|
contract: BaseContract;
|
||||||
|
onProgress?: BatchEventOnProgress;
|
||||||
|
concurrencySize: number;
|
||||||
|
blocksPerRequest: number;
|
||||||
|
shouldRetry: boolean;
|
||||||
|
retryMax: number;
|
||||||
|
retryOn: number;
|
||||||
|
constructor({
|
||||||
|
provider,
|
||||||
|
contract,
|
||||||
|
onProgress,
|
||||||
|
concurrencySize = 10,
|
||||||
|
blocksPerRequest = 2000,
|
||||||
|
shouldRetry = true,
|
||||||
|
retryMax = 5,
|
||||||
|
retryOn = 500,
|
||||||
|
}: BatchEventServiceConstructor) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.contract = contract;
|
||||||
|
this.onProgress = onProgress;
|
||||||
|
this.concurrencySize = concurrencySize;
|
||||||
|
this.blocksPerRequest = blocksPerRequest;
|
||||||
|
this.shouldRetry = shouldRetry;
|
||||||
|
this.retryMax = retryMax;
|
||||||
|
this.retryOn = retryOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPastEvents({ fromBlock, toBlock, type }: EventInput): Promise<EventLog[]> {
|
||||||
|
let err;
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unmodified-loop-condition
|
||||||
|
while ((!this.shouldRetry && retries === 0) || (this.shouldRetry && retries < this.retryMax)) {
|
||||||
|
try {
|
||||||
|
return (await this.contract.queryFilter(type, fromBlock, toBlock)) as EventLog[];
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (e: any) {
|
||||||
|
err = e;
|
||||||
|
retries++;
|
||||||
|
|
||||||
|
// If provider.getBlockNumber returned last block that isn't accepted (happened on Avalanche/Gnosis),
|
||||||
|
// get events to last accepted block
|
||||||
|
if (e.message.includes('after last accepted block')) {
|
||||||
|
const acceptedBlock = parseInt(e.message.split('after last accepted block ')[1]);
|
||||||
|
toBlock = acceptedBlock;
|
||||||
|
}
|
||||||
|
|
||||||
|
// retry on 0.5 seconds
|
||||||
|
await sleep(this.retryOn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
createBatchRequest(batchArray: EventInput[]): Promise<EventLog[]>[] {
|
||||||
|
return batchArray.map(async (event: EventInput, index: number) => {
|
||||||
|
await sleep(20 * index);
|
||||||
|
|
||||||
|
return this.getPastEvents(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBatchEvents({ fromBlock, toBlock, type = '*' }: EventInput): Promise<EventLog[]> {
|
||||||
|
if (!toBlock) {
|
||||||
|
toBlock = await this.provider.getBlockNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsToSync = [];
|
||||||
|
|
||||||
|
for (let i = fromBlock; i < toBlock; i += this.blocksPerRequest) {
|
||||||
|
const j = i + this.blocksPerRequest - 1 > toBlock ? toBlock : i + this.blocksPerRequest - 1;
|
||||||
|
|
||||||
|
eventsToSync.push({ fromBlock: i, toBlock: j, type });
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = [];
|
||||||
|
const eventChunk = chunk(eventsToSync, this.concurrencySize);
|
||||||
|
|
||||||
|
let chunkCount = 0;
|
||||||
|
|
||||||
|
for (const chunk of eventChunk) {
|
||||||
|
chunkCount++;
|
||||||
|
|
||||||
|
const fetchedEvents = (await Promise.all(this.createBatchRequest(chunk))).flat();
|
||||||
|
events.push(...fetchedEvents);
|
||||||
|
|
||||||
|
if (typeof this.onProgress === 'function') {
|
||||||
|
this.onProgress({
|
||||||
|
percentage: chunkCount / eventChunk.length,
|
||||||
|
type,
|
||||||
|
fromBlock: chunk[0].fromBlock,
|
||||||
|
toBlock: chunk[chunk.length - 1].toBlock,
|
||||||
|
count: fetchedEvents.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
}
|
147
src/services/data.ts
Normal file
147
src/services/data.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { stat, mkdir, readFile, writeFile } from 'fs/promises';
|
||||||
|
import { zip, unzip, AsyncZippable, Unzipped } from 'fflate';
|
||||||
|
import { BaseEvents, MinimalEvents } from './events';
|
||||||
|
|
||||||
|
export async function existsAsync(fileOrDir: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await stat(fileOrDir);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function zipAsync(file: AsyncZippable): Promise<Uint8Array> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
zip(file, { mtime: new Date('1/1/1980') }, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
rej(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unzipAsync(data: Uint8Array): Promise<Unzipped> {
|
||||||
|
return new Promise((res, rej) => {
|
||||||
|
unzip(data, {}, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
rej(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveEvents<T extends MinimalEvents>({
|
||||||
|
name,
|
||||||
|
userDirectory,
|
||||||
|
events,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
userDirectory: string;
|
||||||
|
events: T[];
|
||||||
|
}) {
|
||||||
|
const fileName = `${name}.json`.toLowerCase();
|
||||||
|
const filePath = path.join(userDirectory, fileName);
|
||||||
|
|
||||||
|
const stringEvents = JSON.stringify(events, null, 2) + '\n';
|
||||||
|
|
||||||
|
const payload = await zipAsync({
|
||||||
|
[fileName]: new TextEncoder().encode(stringEvents),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!(await existsAsync(userDirectory))) {
|
||||||
|
await mkdir(userDirectory, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(filePath + '.zip', payload);
|
||||||
|
await writeFile(filePath, stringEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSavedEvents<T extends MinimalEvents>({
|
||||||
|
name,
|
||||||
|
userDirectory,
|
||||||
|
deployedBlock,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
userDirectory: string;
|
||||||
|
deployedBlock: number;
|
||||||
|
}): Promise<BaseEvents<T>> {
|
||||||
|
const filePath = path.join(userDirectory, `${name}.json`.toLowerCase());
|
||||||
|
|
||||||
|
if (!(await existsAsync(filePath))) {
|
||||||
|
return {
|
||||||
|
events: [] as T[],
|
||||||
|
lastBlock: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const events = JSON.parse(await readFile(filePath, { encoding: 'utf8' })) as T[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
lastBlock: events && events.length ? events[events.length - 1].blockNumber : deployedBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Method loadSavedEvents has error');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function download({ name, cacheDirectory }: { name: string; cacheDirectory: string }) {
|
||||||
|
const fileName = `${name}.json`.toLowerCase();
|
||||||
|
const zipName = `${fileName}.zip`;
|
||||||
|
const zipPath = path.join(cacheDirectory, zipName);
|
||||||
|
|
||||||
|
const data = await readFile(zipPath);
|
||||||
|
const { [fileName]: content } = await unzipAsync(data);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadCachedEvents<T extends MinimalEvents>({
|
||||||
|
name,
|
||||||
|
cacheDirectory,
|
||||||
|
deployedBlock,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
cacheDirectory: string;
|
||||||
|
deployedBlock: number;
|
||||||
|
}): Promise<BaseEvents<T>> {
|
||||||
|
try {
|
||||||
|
const module = await download({ cacheDirectory, name });
|
||||||
|
|
||||||
|
if (module) {
|
||||||
|
const events = JSON.parse(module);
|
||||||
|
|
||||||
|
const lastBlock = events && events.length ? events[events.length - 1].blockNumber : deployedBlock;
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
lastBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: deployedBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Method loadCachedEvents has error');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
246
src/services/deposits.ts
Normal file
246
src/services/deposits.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { bnToBytes, bytesToBN, leBuff2Int, leInt2Buff, rBigInt, toFixedHex } from './utils';
|
||||||
|
import { buffPedersenHash } from './pedersen';
|
||||||
|
|
||||||
|
export type DepositType = {
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
netId: string | number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type createDepositParams = {
|
||||||
|
nullifier: bigint;
|
||||||
|
secret: bigint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type createDepositObject = {
|
||||||
|
preimage: Uint8Array;
|
||||||
|
noteHex: string;
|
||||||
|
commitment: bigint;
|
||||||
|
commitmentHex: string;
|
||||||
|
nullifierHash: bigint;
|
||||||
|
nullifierHex: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type createNoteParams = DepositType & {
|
||||||
|
nullifier?: bigint;
|
||||||
|
secret?: bigint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type parsedNoteExec = DepositType & {
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type depositTx = {
|
||||||
|
from: string;
|
||||||
|
transactionHash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type withdrawalTx = {
|
||||||
|
to: string;
|
||||||
|
transactionHash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createDeposit({ nullifier, secret }: createDepositParams): Promise<createDepositObject> {
|
||||||
|
const preimage = new Uint8Array([...leInt2Buff(nullifier), ...leInt2Buff(secret)]);
|
||||||
|
const noteHex = toFixedHex(bytesToBN(preimage), 62);
|
||||||
|
const commitment = BigInt(await buffPedersenHash(preimage));
|
||||||
|
const commitmentHex = toFixedHex(commitment);
|
||||||
|
const nullifierHash = BigInt(await buffPedersenHash(leInt2Buff(nullifier)));
|
||||||
|
const nullifierHex = toFixedHex(nullifierHash);
|
||||||
|
|
||||||
|
return {
|
||||||
|
preimage,
|
||||||
|
noteHex,
|
||||||
|
commitment,
|
||||||
|
commitmentHex,
|
||||||
|
nullifierHash,
|
||||||
|
nullifierHex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DepositConstructor {
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
netId: number;
|
||||||
|
nullifier: bigint;
|
||||||
|
secret: bigint;
|
||||||
|
note: string;
|
||||||
|
noteHex: string;
|
||||||
|
invoice: string;
|
||||||
|
commitmentHex: string;
|
||||||
|
nullifierHex: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Deposit {
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
netId: number;
|
||||||
|
|
||||||
|
nullifier: bigint;
|
||||||
|
secret: bigint;
|
||||||
|
|
||||||
|
note: string;
|
||||||
|
noteHex: string;
|
||||||
|
invoice: string;
|
||||||
|
|
||||||
|
commitmentHex: string;
|
||||||
|
nullifierHex: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
netId,
|
||||||
|
nullifier,
|
||||||
|
secret,
|
||||||
|
note,
|
||||||
|
noteHex,
|
||||||
|
invoice,
|
||||||
|
commitmentHex,
|
||||||
|
nullifierHex,
|
||||||
|
}: DepositConstructor) {
|
||||||
|
this.currency = currency;
|
||||||
|
this.amount = amount;
|
||||||
|
this.netId = netId;
|
||||||
|
|
||||||
|
this.nullifier = nullifier;
|
||||||
|
this.secret = secret;
|
||||||
|
|
||||||
|
this.note = note;
|
||||||
|
this.noteHex = noteHex;
|
||||||
|
this.invoice = invoice;
|
||||||
|
|
||||||
|
this.commitmentHex = commitmentHex;
|
||||||
|
this.nullifierHex = nullifierHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
currency: this.currency,
|
||||||
|
amount: this.amount,
|
||||||
|
netId: this.netId,
|
||||||
|
nullifier: this.nullifier,
|
||||||
|
secret: this.secret,
|
||||||
|
note: this.note,
|
||||||
|
noteHex: this.noteHex,
|
||||||
|
invoice: this.invoice,
|
||||||
|
commitmentHex: this.commitmentHex,
|
||||||
|
nullifierHex: this.nullifierHex,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async createNote({ currency, amount, netId, nullifier, secret }: createNoteParams): Promise<Deposit> {
|
||||||
|
if (!nullifier) {
|
||||||
|
nullifier = rBigInt(31);
|
||||||
|
}
|
||||||
|
if (!secret) {
|
||||||
|
secret = rBigInt(31);
|
||||||
|
}
|
||||||
|
|
||||||
|
const depositObject = await createDeposit({
|
||||||
|
nullifier,
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newDeposit = new Deposit({
|
||||||
|
currency: currency.toLowerCase(),
|
||||||
|
amount: amount,
|
||||||
|
netId: Number(netId),
|
||||||
|
note: `tornado-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.noteHex}`,
|
||||||
|
noteHex: depositObject.noteHex,
|
||||||
|
invoice: `tornadoInvoice-${currency.toLowerCase()}-${amount}-${netId}-${depositObject.commitmentHex}`,
|
||||||
|
nullifier: nullifier,
|
||||||
|
secret: secret,
|
||||||
|
commitmentHex: depositObject.commitmentHex,
|
||||||
|
nullifierHex: depositObject.nullifierHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newDeposit;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async parseNote(noteString: string): Promise<Deposit> {
|
||||||
|
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g;
|
||||||
|
const match = noteRegex.exec(noteString);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('The note has invalid format');
|
||||||
|
}
|
||||||
|
const matchGroup = match?.groups as unknown as parsedNoteExec;
|
||||||
|
|
||||||
|
const currency = matchGroup.currency.toLowerCase();
|
||||||
|
const amount = matchGroup.amount;
|
||||||
|
const netId = Number(matchGroup.netId);
|
||||||
|
|
||||||
|
const bytes = bnToBytes('0x' + matchGroup.note);
|
||||||
|
const nullifier = BigInt(leBuff2Int(bytes.slice(0, 31)).toString());
|
||||||
|
const secret = BigInt(leBuff2Int(bytes.slice(31, 62)).toString());
|
||||||
|
|
||||||
|
const depositObject = await createDeposit({ nullifier, secret });
|
||||||
|
|
||||||
|
const invoice = `tornadoInvoice-${currency}-${amount}-${netId}-${depositObject.commitmentHex}`;
|
||||||
|
|
||||||
|
const newDeposit = new Deposit({
|
||||||
|
currency: currency,
|
||||||
|
amount: amount,
|
||||||
|
netId: netId,
|
||||||
|
note: noteString,
|
||||||
|
noteHex: depositObject.noteHex,
|
||||||
|
invoice: invoice,
|
||||||
|
nullifier: nullifier,
|
||||||
|
secret: secret,
|
||||||
|
commitmentHex: depositObject.commitmentHex,
|
||||||
|
nullifierHex: depositObject.nullifierHex,
|
||||||
|
});
|
||||||
|
|
||||||
|
return newDeposit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type parsedInvoiceExec = DepositType & {
|
||||||
|
commitment: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Invoice {
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
netId: number;
|
||||||
|
commitment: string;
|
||||||
|
invoice: string;
|
||||||
|
|
||||||
|
constructor(invoiceString: string) {
|
||||||
|
const invoiceRegex =
|
||||||
|
/tornadoInvoice-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<commitment>[0-9a-fA-F]{64})/g;
|
||||||
|
const match = invoiceRegex.exec(invoiceString);
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('The note has invalid format');
|
||||||
|
}
|
||||||
|
const matchGroup = match?.groups as unknown as parsedInvoiceExec;
|
||||||
|
|
||||||
|
const currency = matchGroup.currency.toLowerCase();
|
||||||
|
const amount = matchGroup.amount;
|
||||||
|
const netId = Number(matchGroup.netId);
|
||||||
|
|
||||||
|
this.currency = currency;
|
||||||
|
this.amount = amount;
|
||||||
|
this.netId = netId;
|
||||||
|
|
||||||
|
this.commitment = '0x' + matchGroup.commitment;
|
||||||
|
this.invoice = invoiceString;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
currency: this.currency,
|
||||||
|
amount: this.amount,
|
||||||
|
netId: this.netId,
|
||||||
|
commitment: this.commitment,
|
||||||
|
invoice: this.invoice,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
719
src/services/events/base.ts
Normal file
719
src/services/events/base.ts
Normal file
@ -0,0 +1,719 @@
|
|||||||
|
import { BaseContract, Provider, EventLog, TransactionResponse, getAddress, Block, ContractEventName } from 'ethers';
|
||||||
|
import type { Tornado, TornadoRouter, TornadoProxyLight, Governance, RelayerRegistry } from '@tornado/contracts';
|
||||||
|
import * as graph from '../graphql';
|
||||||
|
import {
|
||||||
|
BatchEventsService,
|
||||||
|
BatchBlockService,
|
||||||
|
BatchTransactionService,
|
||||||
|
BatchEventOnProgress,
|
||||||
|
BatchBlockOnProgress,
|
||||||
|
} from '../batch';
|
||||||
|
import { fetchDataOptions } from '../providers';
|
||||||
|
import type {
|
||||||
|
BaseEvents,
|
||||||
|
MinimalEvents,
|
||||||
|
DepositsEvents,
|
||||||
|
WithdrawalsEvents,
|
||||||
|
EncryptedNotesEvents,
|
||||||
|
GovernanceProposalCreatedEvents,
|
||||||
|
GovernanceVotedEvents,
|
||||||
|
GovernanceDelegatedEvents,
|
||||||
|
GovernanceUndelegatedEvents,
|
||||||
|
RegistersEvents,
|
||||||
|
BaseGraphEvents,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export const DEPOSIT = 'deposit';
|
||||||
|
export const WITHDRAWAL = 'withdrawal';
|
||||||
|
|
||||||
|
export type BaseEventsServiceConstructor = {
|
||||||
|
netId: number | string;
|
||||||
|
provider: Provider;
|
||||||
|
graphApi?: string;
|
||||||
|
subgraphName?: string;
|
||||||
|
contract: BaseContract;
|
||||||
|
type?: string;
|
||||||
|
deployedBlock?: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BatchGraphOnProgress = ({
|
||||||
|
type,
|
||||||
|
fromBlock,
|
||||||
|
toBlock,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
type?: ContractEventName;
|
||||||
|
fromBlock?: number;
|
||||||
|
toBlock?: number;
|
||||||
|
count?: number;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
export type BaseGraphParams = {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
onProgress?: BatchGraphOnProgress;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BaseEventsService<EventType extends MinimalEvents> {
|
||||||
|
netId: number | string;
|
||||||
|
provider: Provider;
|
||||||
|
graphApi?: string;
|
||||||
|
subgraphName?: string;
|
||||||
|
contract: BaseContract;
|
||||||
|
type: string;
|
||||||
|
deployedBlock: number;
|
||||||
|
batchEventsService: BatchEventsService;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
contract,
|
||||||
|
type = '',
|
||||||
|
deployedBlock = 0,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: BaseEventsServiceConstructor) {
|
||||||
|
this.netId = netId;
|
||||||
|
this.provider = provider;
|
||||||
|
this.graphApi = graphApi;
|
||||||
|
this.subgraphName = subgraphName;
|
||||||
|
this.fetchDataOptions = fetchDataOptions;
|
||||||
|
|
||||||
|
this.contract = contract;
|
||||||
|
this.type = type;
|
||||||
|
this.deployedBlock = deployedBlock;
|
||||||
|
|
||||||
|
this.batchEventsService = new BatchEventsService({
|
||||||
|
provider,
|
||||||
|
contract,
|
||||||
|
onProgress: this.updateEventProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstanceName(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getType(): string {
|
||||||
|
return this.type || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphMethod(): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphParams(): BaseGraphParams {
|
||||||
|
return {
|
||||||
|
graphApi: this.graphApi || '',
|
||||||
|
subgraphName: this.subgraphName || '',
|
||||||
|
fetchDataOptions: this.fetchDataOptions,
|
||||||
|
onProgress: this.updateGraphProgress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
updateEventProgress({ percentage, type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {}
|
||||||
|
|
||||||
|
updateBlockProgress({ percentage, currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {}
|
||||||
|
|
||||||
|
updateTransactionProgress({ percentage, currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {}
|
||||||
|
|
||||||
|
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchGraphOnProgress>[0]) {}
|
||||||
|
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
|
async formatEvents(events: EventLog[]): Promise<EventType[]> {
|
||||||
|
// eslint-disable-next-line no-return-await
|
||||||
|
return await new Promise((resolve) => resolve(events as unknown as EventType[]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get saved or cached events
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getEventsFromDB(): Promise<BaseEvents<EventType>> {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromCache(): Promise<BaseEvents<EventType>> {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSavedEvents(): Promise<BaseEvents<EventType>> {
|
||||||
|
let cachedEvents = await this.getEventsFromDB();
|
||||||
|
|
||||||
|
if (!cachedEvents || !cachedEvents.events.length) {
|
||||||
|
cachedEvents = await this.getEventsFromCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return cachedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get latest events
|
||||||
|
*/
|
||||||
|
|
||||||
|
async getEventsFromGraph({
|
||||||
|
fromBlock,
|
||||||
|
methodName = '',
|
||||||
|
}: {
|
||||||
|
fromBlock: number;
|
||||||
|
methodName?: string;
|
||||||
|
}): Promise<BaseEvents<EventType>> {
|
||||||
|
if (!this.graphApi || !this.subgraphName) {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: fromBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const { events, lastSyncBlock } = (await (graph as any)[methodName || this.getGraphMethod()]({
|
||||||
|
fromBlock,
|
||||||
|
...this.getGraphParams(),
|
||||||
|
})) as BaseGraphEvents<EventType>;
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
lastBlock: lastSyncBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromRpc({
|
||||||
|
fromBlock,
|
||||||
|
toBlock,
|
||||||
|
}: {
|
||||||
|
fromBlock: number;
|
||||||
|
toBlock?: number;
|
||||||
|
}): Promise<BaseEvents<EventType>> {
|
||||||
|
try {
|
||||||
|
if (!toBlock) {
|
||||||
|
toBlock = await this.provider.getBlockNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromBlock >= toBlock) {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: toBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateEventProgress({ percentage: 0, type: this.getType() });
|
||||||
|
|
||||||
|
const events = await this.formatEvents(
|
||||||
|
await this.batchEventsService.getBatchEvents({ fromBlock, toBlock, type: this.getType() }),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
lastBlock: toBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
lastBlock: toBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: fromBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLatestEvents({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<EventType>> {
|
||||||
|
const allEvents = [];
|
||||||
|
const graphEvents = await this.getEventsFromGraph({ fromBlock });
|
||||||
|
const lastSyncBlock =
|
||||||
|
graphEvents.lastBlock && graphEvents.lastBlock >= fromBlock ? graphEvents.lastBlock : fromBlock;
|
||||||
|
const rpcEvents = await this.getEventsFromRpc({ fromBlock: lastSyncBlock });
|
||||||
|
allEvents.push(...graphEvents.events);
|
||||||
|
allEvents.push(...rpcEvents.events);
|
||||||
|
const lastBlock = rpcEvents
|
||||||
|
? rpcEvents.lastBlock
|
||||||
|
: allEvents[allEvents.length - 1]
|
||||||
|
? allEvents[allEvents.length - 1].blockNumber
|
||||||
|
: fromBlock;
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: allEvents,
|
||||||
|
lastBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
validateEvents({ events, lastBlock }: BaseEvents<EventType>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle saving events
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
async saveEvents({ events, lastBlock }: BaseEvents<EventType>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger saving and receiving latest events
|
||||||
|
*/
|
||||||
|
|
||||||
|
async updateEvents() {
|
||||||
|
const savedEvents = await this.getSavedEvents();
|
||||||
|
|
||||||
|
let fromBlock = this.deployedBlock;
|
||||||
|
|
||||||
|
if (savedEvents && savedEvents.lastBlock) {
|
||||||
|
fromBlock = savedEvents.lastBlock + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEvents = await this.getLatestEvents({ fromBlock });
|
||||||
|
|
||||||
|
const eventSet = new Set();
|
||||||
|
|
||||||
|
let allEvents: EventType[] = [];
|
||||||
|
|
||||||
|
allEvents.push(...savedEvents.events);
|
||||||
|
allEvents.push(...newEvents.events);
|
||||||
|
|
||||||
|
allEvents = allEvents
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.blockNumber === b.blockNumber) {
|
||||||
|
return a.logIndex - b.logIndex;
|
||||||
|
}
|
||||||
|
return a.blockNumber - b.blockNumber;
|
||||||
|
})
|
||||||
|
.filter(({ transactionHash, logIndex }) => {
|
||||||
|
const eventKey = `${transactionHash}_${logIndex}`;
|
||||||
|
const hasEvent = eventSet.has(eventKey);
|
||||||
|
eventSet.add(eventKey);
|
||||||
|
return !hasEvent;
|
||||||
|
});
|
||||||
|
const lastBlock = newEvents
|
||||||
|
? newEvents.lastBlock
|
||||||
|
: allEvents[allEvents.length - 1]
|
||||||
|
? allEvents[allEvents.length - 1].blockNumber
|
||||||
|
: null;
|
||||||
|
|
||||||
|
this.validateEvents({ events: allEvents, lastBlock });
|
||||||
|
|
||||||
|
await this.saveEvents({ events: allEvents, lastBlock });
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: allEvents,
|
||||||
|
lastBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseDepositsServiceConstructor = {
|
||||||
|
netId: number | string;
|
||||||
|
provider: Provider;
|
||||||
|
graphApi?: string;
|
||||||
|
subgraphName?: string;
|
||||||
|
Tornado: Tornado;
|
||||||
|
type: string;
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
deployedBlock?: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DepositsGraphParams = BaseGraphParams & {
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BaseDepositsService extends BaseEventsService<DepositsEvents | WithdrawalsEvents> {
|
||||||
|
amount: string;
|
||||||
|
currency: string;
|
||||||
|
batchTransactionService: BatchTransactionService;
|
||||||
|
batchBlockService: BatchBlockService;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Tornado,
|
||||||
|
type,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: BaseDepositsServiceConstructor) {
|
||||||
|
super({ netId, provider, graphApi, subgraphName, contract: Tornado, type, deployedBlock, fetchDataOptions });
|
||||||
|
|
||||||
|
this.amount = amount;
|
||||||
|
this.currency = currency;
|
||||||
|
|
||||||
|
this.batchTransactionService = new BatchTransactionService({
|
||||||
|
provider,
|
||||||
|
onProgress: this.updateTransactionProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.batchBlockService = new BatchBlockService({
|
||||||
|
provider,
|
||||||
|
onProgress: this.updateBlockProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstanceName(): string {
|
||||||
|
return `${this.getType().toLowerCase()}s_${this.netId}_${this.currency}_${this.amount}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphMethod(): string {
|
||||||
|
return `getAll${this.getType()}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphParams(): DepositsGraphParams {
|
||||||
|
return {
|
||||||
|
graphApi: this.graphApi || '',
|
||||||
|
subgraphName: this.subgraphName || '',
|
||||||
|
amount: this.amount,
|
||||||
|
currency: this.currency,
|
||||||
|
fetchDataOptions: this.fetchDataOptions,
|
||||||
|
onProgress: this.updateGraphProgress,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async formatEvents(events: EventLog[]): Promise<(DepositsEvents | WithdrawalsEvents)[]> {
|
||||||
|
const type = this.getType().toLowerCase();
|
||||||
|
if (type === DEPOSIT) {
|
||||||
|
const formattedEvents = events.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
|
||||||
|
const { commitment, leafIndex, timestamp } = args;
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockNumber,
|
||||||
|
logIndex,
|
||||||
|
transactionHash,
|
||||||
|
commitment: commitment as string,
|
||||||
|
leafIndex: Number(leafIndex),
|
||||||
|
timestamp: Number(timestamp),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const txs = await this.batchTransactionService.getBatchTransactions([
|
||||||
|
...new Set(formattedEvents.map(({ transactionHash }) => transactionHash)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return formattedEvents.map((event) => {
|
||||||
|
const { from } = txs.find(({ hash }) => hash === event.transactionHash) as TransactionResponse;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
from,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const formattedEvents = events.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
|
||||||
|
const { nullifierHash, to, fee } = args;
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockNumber,
|
||||||
|
logIndex,
|
||||||
|
transactionHash,
|
||||||
|
nullifierHash: String(nullifierHash),
|
||||||
|
to: getAddress(to),
|
||||||
|
fee: String(fee),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const blocks = await this.batchBlockService.getBatchBlocks([
|
||||||
|
...new Set(formattedEvents.map(({ blockNumber }) => blockNumber)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return formattedEvents.map((event) => {
|
||||||
|
const { timestamp } = blocks.find(({ number }) => number === event.blockNumber) as Block;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...event,
|
||||||
|
timestamp,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateEvents({ events }: { events: (DepositsEvents | WithdrawalsEvents)[] }) {
|
||||||
|
if (events.length && this.getType().toLowerCase() === DEPOSIT) {
|
||||||
|
const lastEvent = events[events.length - 1] as DepositsEvents;
|
||||||
|
|
||||||
|
if (lastEvent.leafIndex !== events.length - 1) {
|
||||||
|
const errMsg = `Deposit events invalid wants ${events.length - 1} leafIndex have ${lastEvent.leafIndex}`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseEncryptedNotesServiceConstructor = {
|
||||||
|
netId: number | string;
|
||||||
|
provider: Provider;
|
||||||
|
graphApi?: string;
|
||||||
|
subgraphName?: string;
|
||||||
|
Router: TornadoRouter | TornadoProxyLight;
|
||||||
|
deployedBlock?: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BaseEncryptedNotesService extends BaseEventsService<EncryptedNotesEvents> {
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Router,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: BaseEncryptedNotesServiceConstructor) {
|
||||||
|
super({ netId, provider, graphApi, subgraphName, contract: Router, deployedBlock, fetchDataOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstanceName(): string {
|
||||||
|
return `encrypted_notes_${this.netId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getType(): string {
|
||||||
|
return 'EncryptedNote';
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphMethod(): string {
|
||||||
|
return 'getAllEncryptedNotes';
|
||||||
|
}
|
||||||
|
|
||||||
|
async formatEvents(events: EventLog[]) {
|
||||||
|
return events
|
||||||
|
.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
|
||||||
|
const { encryptedNote } = args;
|
||||||
|
|
||||||
|
if (encryptedNote) {
|
||||||
|
const eventObjects = {
|
||||||
|
blockNumber,
|
||||||
|
logIndex,
|
||||||
|
transactionHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...eventObjects,
|
||||||
|
encryptedNote,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((e) => e) as EncryptedNotesEvents[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseGovernanceEventTypes =
|
||||||
|
| GovernanceProposalCreatedEvents
|
||||||
|
| GovernanceVotedEvents
|
||||||
|
| GovernanceDelegatedEvents
|
||||||
|
| GovernanceUndelegatedEvents;
|
||||||
|
|
||||||
|
export type BaseGovernanceServiceConstructor = {
|
||||||
|
netId: number | string;
|
||||||
|
provider: Provider;
|
||||||
|
graphApi?: string;
|
||||||
|
subgraphName?: string;
|
||||||
|
Governance: Governance;
|
||||||
|
deployedBlock?: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BaseGovernanceService extends BaseEventsService<BaseGovernanceEventTypes> {
|
||||||
|
batchTransactionService: BatchTransactionService;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Governance,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: BaseGovernanceServiceConstructor) {
|
||||||
|
super({ netId, provider, graphApi, subgraphName, contract: Governance, deployedBlock, fetchDataOptions });
|
||||||
|
|
||||||
|
this.batchTransactionService = new BatchTransactionService({
|
||||||
|
provider,
|
||||||
|
onProgress: this.updateTransactionProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstanceName() {
|
||||||
|
return `governance_${this.netId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getType() {
|
||||||
|
return '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
getGraphMethod() {
|
||||||
|
return 'governanceEvents';
|
||||||
|
}
|
||||||
|
|
||||||
|
async formatEvents(events: EventLog[]): Promise<BaseGovernanceEventTypes[]> {
|
||||||
|
const formattedEvents = events
|
||||||
|
.map(({ blockNumber, index: logIndex, transactionHash, args, eventName: event }) => {
|
||||||
|
const eventObjects = {
|
||||||
|
blockNumber,
|
||||||
|
logIndex,
|
||||||
|
transactionHash,
|
||||||
|
event,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event === 'ProposalCreated') {
|
||||||
|
const { id, proposer, target, startTime, endTime, description } = args;
|
||||||
|
return {
|
||||||
|
...eventObjects,
|
||||||
|
id,
|
||||||
|
proposer,
|
||||||
|
target,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
description,
|
||||||
|
} as GovernanceProposalCreatedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'Voted') {
|
||||||
|
const { proposalId, voter, support, votes } = args;
|
||||||
|
return {
|
||||||
|
...eventObjects,
|
||||||
|
proposalId,
|
||||||
|
voter,
|
||||||
|
support,
|
||||||
|
votes,
|
||||||
|
} as GovernanceVotedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'Delegated') {
|
||||||
|
const { account, to: delegateTo } = args;
|
||||||
|
return {
|
||||||
|
...eventObjects,
|
||||||
|
account,
|
||||||
|
delegateTo,
|
||||||
|
} as GovernanceDelegatedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event === 'Undelegated') {
|
||||||
|
const { account, from: delegateFrom } = args;
|
||||||
|
return {
|
||||||
|
...eventObjects,
|
||||||
|
account,
|
||||||
|
delegateFrom,
|
||||||
|
} as GovernanceUndelegatedEvents;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((e) => e) as BaseGovernanceEventTypes[];
|
||||||
|
|
||||||
|
type GovernanceVotedEventsIndexed = GovernanceVotedEvents & {
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const votedEvents = formattedEvents
|
||||||
|
.map((event, index) => ({ ...event, index }))
|
||||||
|
.filter(({ event }) => event === 'Voted') as GovernanceVotedEventsIndexed[];
|
||||||
|
|
||||||
|
if (votedEvents.length) {
|
||||||
|
this.updateTransactionProgress({ percentage: 0 });
|
||||||
|
|
||||||
|
const txs = await this.batchTransactionService.getBatchTransactions([
|
||||||
|
...new Set(votedEvents.map(({ transactionHash }) => transactionHash)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
votedEvents.forEach((event) => {
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let { data: input, from } = txs.find((t) => t.hash === event.transactionHash) as TransactionResponse;
|
||||||
|
|
||||||
|
// Filter spammy txs
|
||||||
|
if (!input || input.length > 2048) {
|
||||||
|
input = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error check formattedEvents types later
|
||||||
|
formattedEvents[event.index].from = from;
|
||||||
|
// @ts-expect-error check formattedEvents types later
|
||||||
|
formattedEvents[event.index].input = input;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return formattedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromGraph({ fromBlock }: { fromBlock: number }): Promise<BaseEvents<BaseGovernanceEventTypes>> {
|
||||||
|
// TheGraph doesn't support governance subgraphs
|
||||||
|
if (!this.graphApi || this.graphApi.includes('api.thegraph.com')) {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: fromBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getEventsFromGraph({ fromBlock });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BaseRegistryServiceConstructor = {
|
||||||
|
netId: number | string;
|
||||||
|
provider: Provider;
|
||||||
|
graphApi?: string;
|
||||||
|
subgraphName?: string;
|
||||||
|
RelayerRegistry: RelayerRegistry;
|
||||||
|
deployedBlock?: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class BaseRegistryService extends BaseEventsService<RegistersEvents> {
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
RelayerRegistry,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: BaseRegistryServiceConstructor) {
|
||||||
|
super({ netId, provider, graphApi, subgraphName, contract: RelayerRegistry, deployedBlock, fetchDataOptions });
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstanceName() {
|
||||||
|
return `registered_${this.netId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of type used for events
|
||||||
|
getType() {
|
||||||
|
return 'RelayerRegistered';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of method used for graph
|
||||||
|
getGraphMethod() {
|
||||||
|
return 'getAllRegisters';
|
||||||
|
}
|
||||||
|
|
||||||
|
async formatEvents(events: EventLog[]) {
|
||||||
|
return events.map(({ blockNumber, index: logIndex, transactionHash, args }) => {
|
||||||
|
const eventObjects = {
|
||||||
|
blockNumber,
|
||||||
|
logIndex,
|
||||||
|
transactionHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...eventObjects,
|
||||||
|
ensName: args.ensName,
|
||||||
|
relayerAddress: args.relayerAddress,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchRelayers(): Promise<RegistersEvents[]> {
|
||||||
|
return (await this.updateEvents()).events;
|
||||||
|
}
|
||||||
|
}
|
3
src/services/events/index.ts
Normal file
3
src/services/events/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './base';
|
||||||
|
export * from './node';
|
626
src/services/events/node.ts
Normal file
626
src/services/events/node.ts
Normal file
@ -0,0 +1,626 @@
|
|||||||
|
import Table from 'cli-table3';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { BatchBlockOnProgress, BatchEventOnProgress } from '../batch';
|
||||||
|
import { saveEvents, loadSavedEvents, loadCachedEvents } from '../data';
|
||||||
|
import {
|
||||||
|
BaseDepositsService,
|
||||||
|
BaseEncryptedNotesService,
|
||||||
|
BaseGovernanceService,
|
||||||
|
BaseRegistryService,
|
||||||
|
BaseDepositsServiceConstructor,
|
||||||
|
BaseEncryptedNotesServiceConstructor,
|
||||||
|
BaseGovernanceServiceConstructor,
|
||||||
|
BaseRegistryServiceConstructor,
|
||||||
|
BaseGovernanceEventTypes,
|
||||||
|
} from './base';
|
||||||
|
import type { BaseEvents, DepositsEvents, WithdrawalsEvents, EncryptedNotesEvents, RegistersEvents } from './types';
|
||||||
|
|
||||||
|
export type NodeDepositsServiceConstructor = BaseDepositsServiceConstructor & {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NodeDepositsService extends BaseDepositsService {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Tornado,
|
||||||
|
type,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
cacheDirectory,
|
||||||
|
userDirectory,
|
||||||
|
}: NodeDepositsServiceConstructor) {
|
||||||
|
super({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Tornado,
|
||||||
|
type,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cacheDirectory = cacheDirectory;
|
||||||
|
this.userDirectory = userDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTransactionProgress({ currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {
|
||||||
|
if (totalIndex) {
|
||||||
|
console.log(`Fetched ${currentIndex} deposit txs of ${totalIndex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBlockProgress({ currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {
|
||||||
|
if (totalIndex) {
|
||||||
|
console.log(`Fetched ${currentIndex} withdrawal blocks of ${totalIndex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events from graph node count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromDB() {
|
||||||
|
if (!this.userDirectory) {
|
||||||
|
console.log(
|
||||||
|
'Updating events for',
|
||||||
|
this.amount,
|
||||||
|
this.currency.toUpperCase(),
|
||||||
|
`${this.getType().toLowerCase()}s\n`,
|
||||||
|
);
|
||||||
|
console.log(`savedEvents count - ${0}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedEvents = await loadSavedEvents<DepositsEvents | WithdrawalsEvents>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Updating events for', this.amount, this.currency.toUpperCase(), `${this.getType().toLowerCase()}s\n`);
|
||||||
|
console.log(`savedEvents count - ${savedEvents.events.length}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return savedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromCache() {
|
||||||
|
if (!this.cacheDirectory) {
|
||||||
|
console.log(`cachedEvents count - ${0}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedEvents = await loadCachedEvents<DepositsEvents | WithdrawalsEvents>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
cacheDirectory: this.cacheDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return cachedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEvents({ events, lastBlock }: BaseEvents<DepositsEvents | WithdrawalsEvents>) {
|
||||||
|
const instanceName = this.getInstanceName();
|
||||||
|
|
||||||
|
console.log('\ntotalEvents count - ', events.length);
|
||||||
|
console.log(
|
||||||
|
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventTable = new Table();
|
||||||
|
|
||||||
|
eventTable.push(
|
||||||
|
[{ colSpan: 2, content: `${this.getType()}s`, hAlign: 'center' }],
|
||||||
|
['Instance', `${this.netId} chain ${this.amount} ${this.currency.toUpperCase()}`],
|
||||||
|
['Anonymity set', `${events.length} equal user ${this.getType().toLowerCase()}s`],
|
||||||
|
[{ colSpan: 2, content: `Latest ${this.getType().toLowerCase()}s` }],
|
||||||
|
...events
|
||||||
|
.slice(events.length - 10)
|
||||||
|
.reverse()
|
||||||
|
.map(({ timestamp }, index) => {
|
||||||
|
const eventIndex = events.length - index;
|
||||||
|
const eventTime = moment.unix(timestamp).fromNow();
|
||||||
|
|
||||||
|
return [eventIndex, eventTime];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(eventTable.toString() + '\n');
|
||||||
|
|
||||||
|
if (this.userDirectory) {
|
||||||
|
await saveEvents<DepositsEvents | WithdrawalsEvents>({
|
||||||
|
name: instanceName,
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeEncryptedNotesServiceConstructor = BaseEncryptedNotesServiceConstructor & {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NodeEncryptedNotesService extends BaseEncryptedNotesService {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Router,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
cacheDirectory,
|
||||||
|
userDirectory,
|
||||||
|
}: NodeEncryptedNotesServiceConstructor) {
|
||||||
|
super({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Router,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cacheDirectory = cacheDirectory;
|
||||||
|
this.userDirectory = userDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events from graph node count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromDB() {
|
||||||
|
if (!this.userDirectory) {
|
||||||
|
console.log(`Updating events for ${this.netId} chain encrypted events\n`);
|
||||||
|
console.log(`savedEvents count - ${0}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedEvents = await loadSavedEvents<EncryptedNotesEvents>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Updating events for ${this.netId} chain encrypted events\n`);
|
||||||
|
console.log(`savedEvents count - ${savedEvents.events.length}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return savedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromCache() {
|
||||||
|
if (!this.cacheDirectory) {
|
||||||
|
console.log(`cachedEvents count - ${0}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedEvents = await loadCachedEvents<EncryptedNotesEvents>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
cacheDirectory: this.cacheDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return cachedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEvents({ events, lastBlock }: BaseEvents<EncryptedNotesEvents>) {
|
||||||
|
const instanceName = this.getInstanceName();
|
||||||
|
|
||||||
|
console.log('\ntotalEvents count - ', events.length);
|
||||||
|
console.log(
|
||||||
|
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventTable = new Table();
|
||||||
|
|
||||||
|
eventTable.push(
|
||||||
|
[{ colSpan: 2, content: 'Encrypted Notes', hAlign: 'center' }],
|
||||||
|
['Network', `${this.netId} chain`],
|
||||||
|
['Events', `${events.length} events`],
|
||||||
|
[{ colSpan: 2, content: 'Latest events' }],
|
||||||
|
...events
|
||||||
|
.slice(events.length - 10)
|
||||||
|
.reverse()
|
||||||
|
.map(({ blockNumber }, index) => {
|
||||||
|
const eventIndex = events.length - index;
|
||||||
|
|
||||||
|
return [eventIndex, blockNumber];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(eventTable.toString() + '\n');
|
||||||
|
|
||||||
|
if (this.userDirectory) {
|
||||||
|
await saveEvents<EncryptedNotesEvents>({
|
||||||
|
name: instanceName,
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeGovernanceServiceConstructor = BaseGovernanceServiceConstructor & {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NodeGovernanceService extends BaseGovernanceService {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Governance,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
cacheDirectory,
|
||||||
|
userDirectory,
|
||||||
|
}: NodeGovernanceServiceConstructor) {
|
||||||
|
super({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
Governance,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cacheDirectory = cacheDirectory;
|
||||||
|
this.userDirectory = userDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events from graph node count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTransactionProgress({ currentIndex, totalIndex }: Parameters<BatchBlockOnProgress>[0]) {
|
||||||
|
if (totalIndex) {
|
||||||
|
console.log(`Fetched ${currentIndex} governance txs of ${totalIndex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromDB() {
|
||||||
|
if (!this.userDirectory) {
|
||||||
|
console.log(`Updating events for ${this.netId} chain governance events\n`);
|
||||||
|
console.log(`savedEvents count - ${0}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedEvents = await loadSavedEvents<BaseGovernanceEventTypes>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Updating events for ${this.netId} chain governance events\n`);
|
||||||
|
console.log(`savedEvents count - ${savedEvents.events.length}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return savedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromCache() {
|
||||||
|
if (!this.cacheDirectory) {
|
||||||
|
console.log(`cachedEvents count - ${0}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedEvents = await loadCachedEvents<BaseGovernanceEventTypes>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
cacheDirectory: this.cacheDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return cachedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEvents({ events, lastBlock }: BaseEvents<BaseGovernanceEventTypes>) {
|
||||||
|
const instanceName = this.getInstanceName();
|
||||||
|
|
||||||
|
console.log('\ntotalEvents count - ', events.length);
|
||||||
|
console.log(
|
||||||
|
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventTable = new Table();
|
||||||
|
|
||||||
|
eventTable.push(
|
||||||
|
[{ colSpan: 2, content: 'Governance Events', hAlign: 'center' }],
|
||||||
|
['Network', `${this.netId} chain`],
|
||||||
|
['Events', `${events.length} events`],
|
||||||
|
[{ colSpan: 2, content: 'Latest events' }],
|
||||||
|
...events
|
||||||
|
.slice(events.length - 10)
|
||||||
|
.reverse()
|
||||||
|
.map(({ blockNumber }, index) => {
|
||||||
|
const eventIndex = events.length - index;
|
||||||
|
|
||||||
|
return [eventIndex, blockNumber];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(eventTable.toString() + '\n');
|
||||||
|
|
||||||
|
if (this.userDirectory) {
|
||||||
|
await saveEvents<BaseGovernanceEventTypes>({
|
||||||
|
name: instanceName,
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodeRegistryServiceConstructor = BaseRegistryServiceConstructor & {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NodeRegistryService extends BaseRegistryService {
|
||||||
|
cacheDirectory?: string;
|
||||||
|
userDirectory?: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
RelayerRegistry,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
cacheDirectory,
|
||||||
|
userDirectory,
|
||||||
|
}: NodeRegistryServiceConstructor) {
|
||||||
|
super({
|
||||||
|
netId,
|
||||||
|
provider,
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
RelayerRegistry,
|
||||||
|
deployedBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cacheDirectory = cacheDirectory;
|
||||||
|
this.userDirectory = userDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEventProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateGraphProgress({ type, fromBlock, toBlock, count }: Parameters<BatchEventOnProgress>[0]) {
|
||||||
|
if (toBlock) {
|
||||||
|
console.log(`fromBlock - ${fromBlock}`);
|
||||||
|
console.log(`toBlock - ${toBlock}`);
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
console.log(`downloaded ${type} events from graph node count - ${count}`);
|
||||||
|
console.log('____________________________________________');
|
||||||
|
console.log(`Fetched ${type} events from graph node ${fromBlock} to ${toBlock}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromDB() {
|
||||||
|
if (!this.userDirectory) {
|
||||||
|
console.log(`Updating events for ${this.netId} chain registry events\n`);
|
||||||
|
console.log(`savedEvents count - ${0}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedEvents = await loadSavedEvents<RegistersEvents>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Updating events for ${this.netId} chain registry events\n`);
|
||||||
|
console.log(`savedEvents count - ${savedEvents.events.length}`);
|
||||||
|
console.log(`savedEvents lastBlock - ${savedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return savedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEventsFromCache() {
|
||||||
|
if (!this.cacheDirectory) {
|
||||||
|
console.log(`cachedEvents count - ${0}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${this.deployedBlock}\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastBlock: this.deployedBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedEvents = await loadCachedEvents<RegistersEvents>({
|
||||||
|
name: this.getInstanceName(),
|
||||||
|
cacheDirectory: this.cacheDirectory,
|
||||||
|
deployedBlock: this.deployedBlock,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`cachedEvents count - ${cachedEvents.events.length}`);
|
||||||
|
console.log(`cachedEvents lastBlock - ${cachedEvents.lastBlock}\n`);
|
||||||
|
|
||||||
|
return cachedEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveEvents({ events, lastBlock }: BaseEvents<RegistersEvents>) {
|
||||||
|
const instanceName = this.getInstanceName();
|
||||||
|
|
||||||
|
console.log('\ntotalEvents count - ', events.length);
|
||||||
|
console.log(
|
||||||
|
`totalEvents lastBlock - ${events[events.length - 1] ? events[events.length - 1].blockNumber : lastBlock}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const eventTable = new Table();
|
||||||
|
|
||||||
|
eventTable.push(
|
||||||
|
[{ colSpan: 2, content: 'Registered Relayers', hAlign: 'center' }],
|
||||||
|
['Network', `${this.netId} chain`],
|
||||||
|
['Events', `${events.length} events`],
|
||||||
|
[{ colSpan: 2, content: 'Latest events' }],
|
||||||
|
...events
|
||||||
|
.slice(events.length - 10)
|
||||||
|
.reverse()
|
||||||
|
.map(({ blockNumber }, index) => {
|
||||||
|
const eventIndex = events.length - index;
|
||||||
|
|
||||||
|
return [eventIndex, blockNumber];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(eventTable.toString() + '\n');
|
||||||
|
|
||||||
|
if (this.userDirectory) {
|
||||||
|
await saveEvents<RegistersEvents>({
|
||||||
|
name: instanceName,
|
||||||
|
userDirectory: this.userDirectory,
|
||||||
|
events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
src/services/events/types.ts
Normal file
69
src/services/events/types.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { RelayerParams } from '../relayerClient';
|
||||||
|
|
||||||
|
export interface BaseEvents<T> {
|
||||||
|
events: T[];
|
||||||
|
lastBlock: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BaseGraphEvents<T> {
|
||||||
|
events: T[];
|
||||||
|
lastSyncBlock: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MinimalEvents {
|
||||||
|
blockNumber: number;
|
||||||
|
logIndex: number;
|
||||||
|
transactionHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GovernanceEvents = MinimalEvents & {
|
||||||
|
event: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GovernanceProposalCreatedEvents = GovernanceEvents & {
|
||||||
|
id: number;
|
||||||
|
proposer: string;
|
||||||
|
target: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GovernanceVotedEvents = GovernanceEvents & {
|
||||||
|
proposalId: number;
|
||||||
|
voter: string;
|
||||||
|
support: boolean;
|
||||||
|
votes: string;
|
||||||
|
from: string;
|
||||||
|
input: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GovernanceDelegatedEvents = GovernanceEvents & {
|
||||||
|
account: string;
|
||||||
|
delegateTo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GovernanceUndelegatedEvents = GovernanceEvents & {
|
||||||
|
account: string;
|
||||||
|
delegateFrom: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegistersEvents = MinimalEvents & RelayerParams;
|
||||||
|
|
||||||
|
export type DepositsEvents = MinimalEvents & {
|
||||||
|
commitment: string;
|
||||||
|
leafIndex: number;
|
||||||
|
timestamp: number;
|
||||||
|
from: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WithdrawalsEvents = MinimalEvents & {
|
||||||
|
nullifierHash: string;
|
||||||
|
to: string;
|
||||||
|
fee: string;
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EncryptedNotesEvents = MinimalEvents & {
|
||||||
|
encryptedNote: string;
|
||||||
|
};
|
124
src/services/fees.ts
Normal file
124
src/services/fees.ts
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { Transaction, parseUnits } from 'ethers';
|
||||||
|
import type { BigNumberish, TransactionLike } from 'ethers';
|
||||||
|
import { OvmGasPriceOracle } from '../typechain';
|
||||||
|
|
||||||
|
const DUMMY_ADDRESS = '0x1111111111111111111111111111111111111111';
|
||||||
|
|
||||||
|
const DUMMY_NONCE = '0x1111111111111111111111111111111111111111111111111111111111111111';
|
||||||
|
|
||||||
|
const DUMMY_WITHDRAW_DATA =
|
||||||
|
'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* amountInWei (0.1 ETH) * tokenDecimals (18) * tokenPriceInWei (0.0008) = 125 TOKEN
|
||||||
|
*/
|
||||||
|
export function convertETHToTokenAmount(
|
||||||
|
amountInWei: BigNumberish,
|
||||||
|
tokenPriceInWei: BigNumberish,
|
||||||
|
tokenDecimals: number = 18,
|
||||||
|
): bigint {
|
||||||
|
const tokenDecimalsMultiplier = BigInt(10 ** Number(tokenDecimals));
|
||||||
|
return (BigInt(amountInWei) * tokenDecimalsMultiplier) / BigInt(tokenPriceInWei);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayerFeeParams {
|
||||||
|
gasPrice: BigNumberish;
|
||||||
|
gasLimit?: BigNumberish;
|
||||||
|
l1Fee?: BigNumberish;
|
||||||
|
denomination: BigNumberish;
|
||||||
|
ethRefund: BigNumberish;
|
||||||
|
tokenPriceInWei: BigNumberish;
|
||||||
|
tokenDecimals: number;
|
||||||
|
relayerFeePercent?: number;
|
||||||
|
isEth?: boolean;
|
||||||
|
premiumPercent?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TornadoFeeOracle {
|
||||||
|
ovmGasPriceOracle?: OvmGasPriceOracle;
|
||||||
|
|
||||||
|
constructor(ovmGasPriceOracle?: OvmGasPriceOracle) {
|
||||||
|
if (ovmGasPriceOracle) {
|
||||||
|
this.ovmGasPriceOracle = ovmGasPriceOracle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate L1 fee for op-stack chains
|
||||||
|
*
|
||||||
|
* This is required since relayers would pay the full transaction fees for users
|
||||||
|
*/
|
||||||
|
fetchL1OptimismFee(tx?: TransactionLike): Promise<bigint> {
|
||||||
|
if (!this.ovmGasPriceOracle) {
|
||||||
|
return new Promise((resolve) => resolve(BigInt(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tx) {
|
||||||
|
// this tx is only used to simulate bytes size of the encoded tx so has nothing to with the accuracy
|
||||||
|
// inspired by the old style classic-ui calculation
|
||||||
|
tx = {
|
||||||
|
type: 0,
|
||||||
|
gasLimit: 1_000_000,
|
||||||
|
nonce: Number(DUMMY_NONCE),
|
||||||
|
data: DUMMY_WITHDRAW_DATA,
|
||||||
|
gasPrice: parseUnits('1', 'gwei'),
|
||||||
|
from: DUMMY_ADDRESS,
|
||||||
|
to: DUMMY_ADDRESS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ovmGasPriceOracle.getL1Fee.staticCall(Transaction.from(tx).unsignedSerialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We don't need to distinguish default refunds by tokens since most users interact with other defi protocols after withdrawal
|
||||||
|
* So we default with 1M gas which is enough for two or three swaps
|
||||||
|
* Using 30 gwei for default but it is recommended to supply cached gasPrice value from the UI
|
||||||
|
*/
|
||||||
|
defaultEthRefund(gasPrice?: BigNumberish, gasLimit?: BigNumberish): bigint {
|
||||||
|
return (gasPrice ? BigInt(gasPrice) : parseUnits('30', 'gwei')) * BigInt(gasLimit || 1_000_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates token amount for required ethRefund purchases required to calculate fees
|
||||||
|
*/
|
||||||
|
calculateTokenAmount(ethRefund: BigNumberish, tokenPriceInEth: BigNumberish, tokenDecimals?: number): bigint {
|
||||||
|
return convertETHToTokenAmount(ethRefund, tokenPriceInEth, tokenDecimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warning: For tokens you need to check if the fees are above denomination
|
||||||
|
* (Usually happens for small denomination pool or if the gas price is high)
|
||||||
|
*/
|
||||||
|
calculateRelayerFee({
|
||||||
|
gasPrice,
|
||||||
|
gasLimit = 600_000,
|
||||||
|
l1Fee = 0,
|
||||||
|
denomination,
|
||||||
|
ethRefund = BigInt(0),
|
||||||
|
tokenPriceInWei,
|
||||||
|
tokenDecimals = 18,
|
||||||
|
relayerFeePercent = 0.33,
|
||||||
|
isEth = true,
|
||||||
|
premiumPercent = 20,
|
||||||
|
}: RelayerFeeParams): bigint {
|
||||||
|
const gasCosts = BigInt(gasPrice) * BigInt(gasLimit) + BigInt(l1Fee);
|
||||||
|
|
||||||
|
const relayerFee = (BigInt(denomination) * BigInt(Math.floor(10000 * relayerFeePercent))) / BigInt(10000 * 100);
|
||||||
|
|
||||||
|
if (isEth) {
|
||||||
|
// Add 20% premium
|
||||||
|
return ((gasCosts + relayerFee) * BigInt(premiumPercent ? 100 + premiumPercent : 100)) / BigInt(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const feeInEth = gasCosts + BigInt(ethRefund);
|
||||||
|
|
||||||
|
return (
|
||||||
|
((convertETHToTokenAmount(feeInEth, tokenPriceInWei, tokenDecimals) + relayerFee) *
|
||||||
|
BigInt(premiumPercent ? 100 + premiumPercent : 100)) /
|
||||||
|
BigInt(100)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
784
src/services/graphql/index.ts
Normal file
784
src/services/graphql/index.ts
Normal file
@ -0,0 +1,784 @@
|
|||||||
|
import { getAddress } from 'ethers';
|
||||||
|
import { fetchData, fetchDataOptions } from '../providers';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BaseGraphEvents,
|
||||||
|
RegistersEvents,
|
||||||
|
DepositsEvents,
|
||||||
|
WithdrawalsEvents,
|
||||||
|
EncryptedNotesEvents,
|
||||||
|
BatchGraphOnProgress,
|
||||||
|
} from '../events';
|
||||||
|
import {
|
||||||
|
_META,
|
||||||
|
GET_DEPOSITS,
|
||||||
|
GET_STATISTIC,
|
||||||
|
GET_REGISTERED,
|
||||||
|
GET_WITHDRAWALS,
|
||||||
|
GET_NOTE_ACCOUNTS,
|
||||||
|
GET_ENCRYPTED_NOTES,
|
||||||
|
} from './queries';
|
||||||
|
|
||||||
|
export * from './queries';
|
||||||
|
|
||||||
|
const isEmptyArray = (arr: object) => !Array.isArray(arr) || !arr.length;
|
||||||
|
|
||||||
|
const first = 1000;
|
||||||
|
|
||||||
|
export type queryGraphParams = {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
query: string;
|
||||||
|
variables?: {
|
||||||
|
[key: string]: string | number;
|
||||||
|
};
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function queryGraph<T>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: queryGraphParams): Promise<T> {
|
||||||
|
const graphUrl = `${graphApi}/subgraphs/name/${subgraphName}`;
|
||||||
|
|
||||||
|
const { data, errors } = await fetchData(graphUrl, {
|
||||||
|
...fetchDataOptions,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
throw new Error(JSON.stringify(errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data?._meta?.hasIndexingErrors) {
|
||||||
|
throw new Error('Subgraph has indexing errors');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphStatistic {
|
||||||
|
deposits: {
|
||||||
|
index: string;
|
||||||
|
timestamp: string;
|
||||||
|
blockNumber: string;
|
||||||
|
}[];
|
||||||
|
_meta: {
|
||||||
|
block: {
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
hasIndexingErrors: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getStatisticParams {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getStatisticReturns {
|
||||||
|
events: {
|
||||||
|
timestamp: number;
|
||||||
|
leafIndex: number;
|
||||||
|
blockNumber: number;
|
||||||
|
}[];
|
||||||
|
lastSyncBlock: null | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStatistic({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: getStatisticParams): Promise<getStatisticReturns> {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
deposits,
|
||||||
|
_meta: {
|
||||||
|
block: { number: lastSyncBlock },
|
||||||
|
},
|
||||||
|
} = await queryGraph<GraphStatistic>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query: GET_STATISTIC,
|
||||||
|
variables: {
|
||||||
|
currency,
|
||||||
|
first: 10,
|
||||||
|
orderBy: 'index',
|
||||||
|
orderDirection: 'desc',
|
||||||
|
amount,
|
||||||
|
},
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const events = deposits
|
||||||
|
.map((e) => ({
|
||||||
|
timestamp: Number(e.timestamp),
|
||||||
|
leafIndex: Number(e.index),
|
||||||
|
blockNumber: Number(e.blockNumber),
|
||||||
|
}))
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
const [lastEvent] = events.slice(-1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error from getStatistic query');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphMeta {
|
||||||
|
_meta: {
|
||||||
|
block: {
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
hasIndexingErrors: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getMetaParams {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getMetaReturns {
|
||||||
|
lastSyncBlock: null | number;
|
||||||
|
hasIndexingErrors: null | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMeta({ graphApi, subgraphName, fetchDataOptions }: getMetaParams): Promise<getMetaReturns> {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
_meta: {
|
||||||
|
block: { number: lastSyncBlock },
|
||||||
|
hasIndexingErrors,
|
||||||
|
},
|
||||||
|
} = await queryGraph<GraphMeta>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query: _META,
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastSyncBlock,
|
||||||
|
hasIndexingErrors,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error from getMeta query');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
lastSyncBlock: null,
|
||||||
|
hasIndexingErrors: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphRegisters {
|
||||||
|
relayers: {
|
||||||
|
id: string;
|
||||||
|
address: string;
|
||||||
|
ensName: string;
|
||||||
|
blockRegistration: string;
|
||||||
|
}[];
|
||||||
|
_meta: {
|
||||||
|
block: {
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
hasIndexingErrors: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getRegistersParams {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
fromBlock: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
onProgress?: BatchGraphOnProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRegisters({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: getRegistersParams): Promise<GraphRegisters> {
|
||||||
|
return queryGraph<GraphRegisters>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query: GET_REGISTERED,
|
||||||
|
variables: {
|
||||||
|
first,
|
||||||
|
fromBlock,
|
||||||
|
},
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllRegisters({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
onProgress,
|
||||||
|
}: getRegistersParams): Promise<BaseGraphEvents<RegistersEvents>> {
|
||||||
|
try {
|
||||||
|
const events = [];
|
||||||
|
let lastSyncBlock = fromBlock;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
let {
|
||||||
|
relayers: result,
|
||||||
|
_meta: {
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
block: { number: currentBlock },
|
||||||
|
},
|
||||||
|
} = await getRegisters({ graphApi, subgraphName, fromBlock, fetchDataOptions });
|
||||||
|
|
||||||
|
lastSyncBlock = currentBlock;
|
||||||
|
|
||||||
|
if (isEmptyArray(result)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstEvent] = result;
|
||||||
|
const [lastEvent] = result.slice(-1);
|
||||||
|
|
||||||
|
if (typeof onProgress === 'function') {
|
||||||
|
onProgress({
|
||||||
|
type: 'Registers',
|
||||||
|
fromBlock: Number(firstEvent.blockRegistration),
|
||||||
|
toBlock: Number(lastEvent.blockRegistration),
|
||||||
|
count: result.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length < 900) {
|
||||||
|
events.push(...result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.filter(({ blockRegistration }) => blockRegistration !== lastEvent.blockRegistration);
|
||||||
|
fromBlock = Number(lastEvent.blockRegistration);
|
||||||
|
|
||||||
|
events.push(...result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = events.map(({ id, address, ensName, blockRegistration }) => {
|
||||||
|
const [transactionHash, logIndex] = id.split('-');
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockNumber: Number(blockRegistration),
|
||||||
|
logIndex: Number(logIndex),
|
||||||
|
transactionHash,
|
||||||
|
ensName,
|
||||||
|
relayerAddress: getAddress(address),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: result,
|
||||||
|
lastSyncBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error from getAllRegisters query');
|
||||||
|
console.log(err);
|
||||||
|
return { events: [], lastSyncBlock: fromBlock };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphDeposits {
|
||||||
|
deposits: {
|
||||||
|
id: string;
|
||||||
|
blockNumber: string;
|
||||||
|
commitment: string;
|
||||||
|
index: string;
|
||||||
|
timestamp: string;
|
||||||
|
from: string;
|
||||||
|
}[];
|
||||||
|
_meta: {
|
||||||
|
block: {
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
hasIndexingErrors: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getDepositsParams {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
fromBlock: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
onProgress?: BatchGraphOnProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeposits({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: getDepositsParams): Promise<GraphDeposits> {
|
||||||
|
return queryGraph<GraphDeposits>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query: GET_DEPOSITS,
|
||||||
|
variables: {
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
first,
|
||||||
|
fromBlock,
|
||||||
|
},
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllDeposits({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
onProgress,
|
||||||
|
}: getDepositsParams): Promise<BaseGraphEvents<DepositsEvents>> {
|
||||||
|
try {
|
||||||
|
const events = [];
|
||||||
|
let lastSyncBlock = fromBlock;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
let {
|
||||||
|
deposits: result,
|
||||||
|
_meta: {
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
block: { number: currentBlock },
|
||||||
|
},
|
||||||
|
} = await getDeposits({ graphApi, subgraphName, currency, amount, fromBlock, fetchDataOptions });
|
||||||
|
|
||||||
|
lastSyncBlock = currentBlock;
|
||||||
|
|
||||||
|
if (isEmptyArray(result)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstEvent] = result;
|
||||||
|
const [lastEvent] = result.slice(-1);
|
||||||
|
|
||||||
|
if (typeof onProgress === 'function') {
|
||||||
|
onProgress({
|
||||||
|
type: 'Deposits',
|
||||||
|
fromBlock: Number(firstEvent.blockNumber),
|
||||||
|
toBlock: Number(lastEvent.blockNumber),
|
||||||
|
count: result.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length < 900) {
|
||||||
|
events.push(...result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
|
||||||
|
fromBlock = Number(lastEvent.blockNumber);
|
||||||
|
|
||||||
|
events.push(...result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = events.map(({ id, blockNumber, commitment, index, timestamp, from }) => {
|
||||||
|
const [transactionHash, logIndex] = id.split('-');
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockNumber: Number(blockNumber),
|
||||||
|
logIndex: Number(logIndex),
|
||||||
|
transactionHash,
|
||||||
|
commitment,
|
||||||
|
leafIndex: Number(index),
|
||||||
|
timestamp: Number(timestamp),
|
||||||
|
from: getAddress(from),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const [lastEvent] = result.slice(-1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: result,
|
||||||
|
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error from getAllDeposits query');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock: fromBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphWithdrawals {
|
||||||
|
withdrawals: {
|
||||||
|
id: string;
|
||||||
|
blockNumber: string;
|
||||||
|
nullifier: string;
|
||||||
|
to: string;
|
||||||
|
fee: string;
|
||||||
|
timestamp: string;
|
||||||
|
}[];
|
||||||
|
_meta: {
|
||||||
|
block: {
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
hasIndexingErrors: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getWithdrawalParams {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
fromBlock: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
onProgress?: BatchGraphOnProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWithdrawals({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: getWithdrawalParams): Promise<GraphWithdrawals> {
|
||||||
|
return queryGraph<GraphWithdrawals>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query: GET_WITHDRAWALS,
|
||||||
|
variables: {
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
first,
|
||||||
|
fromBlock,
|
||||||
|
},
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllWithdrawals({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
currency,
|
||||||
|
amount,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
onProgress,
|
||||||
|
}: getWithdrawalParams): Promise<BaseGraphEvents<WithdrawalsEvents>> {
|
||||||
|
try {
|
||||||
|
const events = [];
|
||||||
|
let lastSyncBlock = fromBlock;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
let {
|
||||||
|
withdrawals: result,
|
||||||
|
_meta: {
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
block: { number: currentBlock },
|
||||||
|
},
|
||||||
|
} = await getWithdrawals({ graphApi, subgraphName, currency, amount, fromBlock, fetchDataOptions });
|
||||||
|
|
||||||
|
lastSyncBlock = currentBlock;
|
||||||
|
|
||||||
|
if (isEmptyArray(result)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstEvent] = result;
|
||||||
|
const [lastEvent] = result.slice(-1);
|
||||||
|
|
||||||
|
if (typeof onProgress === 'function') {
|
||||||
|
onProgress({
|
||||||
|
type: 'Withdrawals',
|
||||||
|
fromBlock: Number(firstEvent.blockNumber),
|
||||||
|
toBlock: Number(lastEvent.blockNumber),
|
||||||
|
count: result.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length < 900) {
|
||||||
|
events.push(...result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
|
||||||
|
fromBlock = Number(lastEvent.blockNumber);
|
||||||
|
|
||||||
|
events.push(...result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = events.map(({ id, blockNumber, nullifier, to, fee, timestamp }) => {
|
||||||
|
const [transactionHash, logIndex] = id.split('-');
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockNumber: Number(blockNumber),
|
||||||
|
logIndex: Number(logIndex),
|
||||||
|
transactionHash,
|
||||||
|
nullifierHash: nullifier,
|
||||||
|
to: getAddress(to),
|
||||||
|
fee,
|
||||||
|
timestamp: Number(timestamp),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const [lastEvent] = result.slice(-1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: result,
|
||||||
|
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error from getAllWithdrawals query');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock: fromBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphNoteAccounts {
|
||||||
|
noteAccounts: {
|
||||||
|
id: string;
|
||||||
|
index: string;
|
||||||
|
address: string;
|
||||||
|
encryptedAccount: string;
|
||||||
|
}[];
|
||||||
|
_meta: {
|
||||||
|
block: {
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
hasIndexingErrors: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getNoteAccountsParams {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
address: string;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getNoteAccountsReturns {
|
||||||
|
events: {
|
||||||
|
id: string;
|
||||||
|
index: string;
|
||||||
|
address: string;
|
||||||
|
encryptedAccount: string;
|
||||||
|
}[];
|
||||||
|
lastSyncBlock: null | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getNoteAccounts({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
address,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: getNoteAccountsParams): Promise<getNoteAccountsReturns> {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
noteAccounts: events,
|
||||||
|
_meta: {
|
||||||
|
block: { number: lastSyncBlock },
|
||||||
|
},
|
||||||
|
} = await queryGraph<GraphNoteAccounts>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query: GET_NOTE_ACCOUNTS,
|
||||||
|
variables: {
|
||||||
|
address,
|
||||||
|
},
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
events,
|
||||||
|
lastSyncBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error from getNoteAccounts query');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphEncryptedNotes {
|
||||||
|
encryptedNotes: {
|
||||||
|
blockNumber: string;
|
||||||
|
index: string;
|
||||||
|
transactionHash: string;
|
||||||
|
encryptedNote: string;
|
||||||
|
}[];
|
||||||
|
_meta: {
|
||||||
|
block: {
|
||||||
|
number: number;
|
||||||
|
};
|
||||||
|
hasIndexingErrors: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface getEncryptedNotesParams {
|
||||||
|
graphApi: string;
|
||||||
|
subgraphName: string;
|
||||||
|
fromBlock: number;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
onProgress?: BatchGraphOnProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEncryptedNotes({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
}: getEncryptedNotesParams): Promise<GraphEncryptedNotes> {
|
||||||
|
return queryGraph<GraphEncryptedNotes>({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
query: GET_ENCRYPTED_NOTES,
|
||||||
|
variables: {
|
||||||
|
first,
|
||||||
|
fromBlock,
|
||||||
|
},
|
||||||
|
fetchDataOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllEncryptedNotes({
|
||||||
|
graphApi,
|
||||||
|
subgraphName,
|
||||||
|
fromBlock,
|
||||||
|
fetchDataOptions,
|
||||||
|
onProgress,
|
||||||
|
}: getEncryptedNotesParams): Promise<BaseGraphEvents<EncryptedNotesEvents>> {
|
||||||
|
try {
|
||||||
|
const events = [];
|
||||||
|
let lastSyncBlock = fromBlock;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
let {
|
||||||
|
encryptedNotes: result,
|
||||||
|
_meta: {
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
block: { number: currentBlock },
|
||||||
|
},
|
||||||
|
} = await getEncryptedNotes({ graphApi, subgraphName, fromBlock, fetchDataOptions });
|
||||||
|
|
||||||
|
lastSyncBlock = currentBlock;
|
||||||
|
|
||||||
|
if (isEmptyArray(result)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstEvent] = result;
|
||||||
|
const [lastEvent] = result.slice(-1);
|
||||||
|
|
||||||
|
if (typeof onProgress === 'function') {
|
||||||
|
onProgress({
|
||||||
|
type: 'EncryptedNotes',
|
||||||
|
fromBlock: Number(firstEvent.blockNumber),
|
||||||
|
toBlock: Number(lastEvent.blockNumber),
|
||||||
|
count: result.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length < 900) {
|
||||||
|
events.push(...result);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = result.filter(({ blockNumber }) => blockNumber !== lastEvent.blockNumber);
|
||||||
|
fromBlock = Number(lastEvent.blockNumber);
|
||||||
|
|
||||||
|
events.push(...result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = events.map((e) => ({
|
||||||
|
blockNumber: Number(e.blockNumber),
|
||||||
|
logIndex: Number(e.index),
|
||||||
|
transactionHash: e.transactionHash,
|
||||||
|
encryptedNote: e.encryptedNote,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [lastEvent] = result.slice(-1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
events: result,
|
||||||
|
lastSyncBlock: lastEvent && lastEvent.blockNumber >= lastSyncBlock ? lastEvent.blockNumber + 1 : lastSyncBlock,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Error from getAllEncryptedNotes query');
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
events: [],
|
||||||
|
lastSyncBlock: fromBlock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
125
src/services/graphql/queries.ts
Normal file
125
src/services/graphql/queries.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
export const GET_STATISTIC = `
|
||||||
|
query getStatistic($currency: String!, $amount: String!, $first: Int, $orderBy: BigInt, $orderDirection: String) {
|
||||||
|
deposits(first: $first, orderBy: $orderBy, orderDirection: $orderDirection, where: { currency: $currency, amount: $amount }) {
|
||||||
|
index
|
||||||
|
timestamp
|
||||||
|
blockNumber
|
||||||
|
}
|
||||||
|
_meta {
|
||||||
|
block {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
hasIndexingErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const _META = `
|
||||||
|
query getMeta {
|
||||||
|
_meta {
|
||||||
|
block {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
hasIndexingErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_REGISTERED = `
|
||||||
|
query getRegistered($first: Int, $fromBlock: Int) {
|
||||||
|
relayers(first: $first, orderBy: blockRegistration, orderDirection: asc, where: {
|
||||||
|
blockRegistration_gte: $fromBlock
|
||||||
|
}) {
|
||||||
|
id
|
||||||
|
address
|
||||||
|
ensName
|
||||||
|
blockRegistration
|
||||||
|
}
|
||||||
|
_meta {
|
||||||
|
block {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
hasIndexingErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_DEPOSITS = `
|
||||||
|
query getDeposits($currency: String!, $amount: String!, $first: Int, $fromBlock: Int) {
|
||||||
|
deposits(first: $first, orderBy: index, orderDirection: asc, where: {
|
||||||
|
amount: $amount,
|
||||||
|
currency: $currency,
|
||||||
|
blockNumber_gte: $fromBlock
|
||||||
|
}) {
|
||||||
|
id
|
||||||
|
blockNumber
|
||||||
|
commitment
|
||||||
|
index
|
||||||
|
timestamp
|
||||||
|
from
|
||||||
|
}
|
||||||
|
_meta {
|
||||||
|
block {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
hasIndexingErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_WITHDRAWALS = `
|
||||||
|
query getWithdrawals($currency: String!, $amount: String!, $first: Int, $fromBlock: Int!) {
|
||||||
|
withdrawals(first: $first, orderBy: blockNumber, orderDirection: asc, where: {
|
||||||
|
currency: $currency,
|
||||||
|
amount: $amount,
|
||||||
|
blockNumber_gte: $fromBlock
|
||||||
|
}) {
|
||||||
|
id
|
||||||
|
blockNumber
|
||||||
|
nullifier
|
||||||
|
to
|
||||||
|
fee
|
||||||
|
timestamp
|
||||||
|
}
|
||||||
|
_meta {
|
||||||
|
block {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
hasIndexingErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_NOTE_ACCOUNTS = `
|
||||||
|
query getNoteAccount($address: String!) {
|
||||||
|
noteAccounts(where: { address: $address }) {
|
||||||
|
id
|
||||||
|
index
|
||||||
|
address
|
||||||
|
encryptedAccount
|
||||||
|
}
|
||||||
|
_meta {
|
||||||
|
block {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
hasIndexingErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_ENCRYPTED_NOTES = `
|
||||||
|
query getEncryptedNotes($first: Int, $fromBlock: Int) {
|
||||||
|
encryptedNotes(first: $first, orderBy: blockNumber, orderDirection: asc, where: { blockNumber_gte: $fromBlock }) {
|
||||||
|
blockNumber
|
||||||
|
index
|
||||||
|
transactionHash
|
||||||
|
encryptedNote
|
||||||
|
}
|
||||||
|
_meta {
|
||||||
|
block {
|
||||||
|
number
|
||||||
|
}
|
||||||
|
hasIndexingErrors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
19
src/services/index.ts
Normal file
19
src/services/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export * from './events';
|
||||||
|
export * from './graphql';
|
||||||
|
export * from './schemas';
|
||||||
|
export * from './batch';
|
||||||
|
export * from './data';
|
||||||
|
export * from './deposits';
|
||||||
|
export * from './fees';
|
||||||
|
export * from './merkleTree';
|
||||||
|
export * from './mimc';
|
||||||
|
export * from './multicall';
|
||||||
|
export * from './networkConfig';
|
||||||
|
export * from './parser';
|
||||||
|
export * from './pedersen';
|
||||||
|
export * from './prices';
|
||||||
|
export * from './providers';
|
||||||
|
export * from './relayerClient';
|
||||||
|
export * from './tokens';
|
||||||
|
export * from './utils';
|
||||||
|
export * from './websnark';
|
135
src/services/merkleTree.ts
Normal file
135
src/services/merkleTree.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { Worker as NodeWorker } from 'worker_threads';
|
||||||
|
import { MerkleTree, Element } from '@tornado/fixed-merkle-tree';
|
||||||
|
import type { Tornado } from '@tornado/contracts';
|
||||||
|
import { isNode, toFixedHex } from './utils';
|
||||||
|
import { mimc } from './mimc';
|
||||||
|
import type { DepositType } from './deposits';
|
||||||
|
import type { DepositsEvents } from './events';
|
||||||
|
|
||||||
|
export type MerkleTreeConstructor = DepositType & {
|
||||||
|
Tornado: Tornado;
|
||||||
|
commitment?: string;
|
||||||
|
merkleTreeHeight?: number;
|
||||||
|
emptyElement?: string;
|
||||||
|
merkleWorkerPath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MerkleTreeService {
|
||||||
|
currency: string;
|
||||||
|
amount: string;
|
||||||
|
netId: number;
|
||||||
|
Tornado: Tornado;
|
||||||
|
commitment?: string;
|
||||||
|
instanceName: string;
|
||||||
|
|
||||||
|
merkleTreeHeight: number;
|
||||||
|
emptyElement: string;
|
||||||
|
|
||||||
|
merkleWorkerPath?: string;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
netId,
|
||||||
|
amount,
|
||||||
|
currency,
|
||||||
|
Tornado,
|
||||||
|
commitment,
|
||||||
|
merkleTreeHeight = 20,
|
||||||
|
emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
merkleWorkerPath,
|
||||||
|
}: MerkleTreeConstructor) {
|
||||||
|
const instanceName = `${netId}_${currency}_${amount}`;
|
||||||
|
|
||||||
|
this.currency = currency;
|
||||||
|
this.amount = amount;
|
||||||
|
this.netId = Number(netId);
|
||||||
|
|
||||||
|
this.Tornado = Tornado;
|
||||||
|
this.instanceName = instanceName;
|
||||||
|
this.commitment = commitment;
|
||||||
|
|
||||||
|
this.merkleTreeHeight = merkleTreeHeight;
|
||||||
|
this.emptyElement = emptyElement;
|
||||||
|
this.merkleWorkerPath = merkleWorkerPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTree({ events }: { events: Element[] }) {
|
||||||
|
const { hash: hashFunction } = await mimc.getHash();
|
||||||
|
|
||||||
|
if (this.merkleWorkerPath) {
|
||||||
|
console.log('Using merkleWorker\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isNode) {
|
||||||
|
const merkleWorkerPromise = new Promise((resolve, reject) => {
|
||||||
|
const worker = new NodeWorker(this.merkleWorkerPath as string, {
|
||||||
|
workerData: {
|
||||||
|
merkleTreeHeight: this.merkleTreeHeight,
|
||||||
|
elements: events,
|
||||||
|
zeroElement: this.emptyElement,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
worker.on('message', resolve);
|
||||||
|
worker.on('error', reject);
|
||||||
|
worker.on('exit', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`Worker stopped with exit code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}) as Promise<string>;
|
||||||
|
|
||||||
|
return MerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction);
|
||||||
|
} else {
|
||||||
|
const merkleWorkerPromise = new Promise((resolve, reject) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const worker = new (Worker as any)(this.merkleWorkerPath);
|
||||||
|
|
||||||
|
worker.onmessage = (e: { data: string }) => {
|
||||||
|
resolve(e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
worker.onerror = (e: any) => {
|
||||||
|
reject(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
merkleTreeHeight: this.merkleTreeHeight,
|
||||||
|
elements: events,
|
||||||
|
zeroElement: this.emptyElement,
|
||||||
|
});
|
||||||
|
}) as Promise<string>;
|
||||||
|
|
||||||
|
return MerkleTree.deserialize(JSON.parse(await merkleWorkerPromise), hashFunction);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('merkleWorker failed, falling back to synchronous merkle tree');
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MerkleTree(this.merkleTreeHeight, events, {
|
||||||
|
zeroElement: this.emptyElement,
|
||||||
|
hashFunction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyTree({ events }: { events: DepositsEvents[] }) {
|
||||||
|
console.log(
|
||||||
|
`\nCreating deposit tree for ${this.netId} ${this.amount} ${this.currency.toUpperCase()} would take a while\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.time('Created tree in');
|
||||||
|
const tree = await this.createTree({ events: events.map(({ commitment }) => BigInt(commitment).toString()) });
|
||||||
|
console.timeEnd('Created tree in');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const isKnownRoot = await this.Tornado.isKnownRoot(toFixedHex(BigInt(tree.root)));
|
||||||
|
|
||||||
|
if (!isKnownRoot) {
|
||||||
|
const errMsg = `Deposit Event ${this.netId} ${this.amount} ${this.currency} is invalid`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
}
|
28
src/services/mimc.ts
Normal file
28
src/services/mimc.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { MimcSponge, buildMimcSponge } from 'circomlibjs';
|
||||||
|
import type { Element, HashFunction } from '@tornado/fixed-merkle-tree';
|
||||||
|
|
||||||
|
export class Mimc {
|
||||||
|
sponge?: MimcSponge;
|
||||||
|
hash?: HashFunction<Element>;
|
||||||
|
mimcPromise: Promise<void>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.mimcPromise = this.initMimc();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initMimc() {
|
||||||
|
this.sponge = await buildMimcSponge();
|
||||||
|
this.hash = (left, right) => this.sponge?.F.toString(this.sponge?.multiHash([BigInt(left), BigInt(right)]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHash() {
|
||||||
|
await this.mimcPromise;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sponge: this.sponge,
|
||||||
|
hash: this.hash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mimc = new Mimc();
|
37
src/services/multicall.ts
Normal file
37
src/services/multicall.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { BaseContract, Interface } from 'ethers';
|
||||||
|
import { Multicall } from '../typechain';
|
||||||
|
|
||||||
|
export interface Call3 {
|
||||||
|
contract?: BaseContract;
|
||||||
|
address?: string;
|
||||||
|
interface?: Interface;
|
||||||
|
name: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
params?: any[];
|
||||||
|
allowFailure?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function multicall(Multicall: Multicall, calls: Call3[]) {
|
||||||
|
const calldata = calls.map((call) => {
|
||||||
|
const target = (call.contract?.target || call.address) as string;
|
||||||
|
const callInterface = (call.contract?.interface || call.interface) as Interface;
|
||||||
|
|
||||||
|
return {
|
||||||
|
target,
|
||||||
|
callData: callInterface.encodeFunctionData(call.name, call.params),
|
||||||
|
allowFailure: call.allowFailure ?? false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const returnData = await Multicall.aggregate3.staticCall(calldata);
|
||||||
|
|
||||||
|
const res = returnData.map((call, i) => {
|
||||||
|
const callInterface = (calls[i].contract?.interface || calls[i].interface) as Interface;
|
||||||
|
const [result, data] = call;
|
||||||
|
const decodeResult =
|
||||||
|
result && data && data !== '0x' ? callInterface.decodeFunctionResult(calls[i].name, data) : null;
|
||||||
|
return !decodeResult ? null : decodeResult.length === 1 ? decodeResult[0] : decodeResult;
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
741
src/services/networkConfig.ts
Normal file
741
src/services/networkConfig.ts
Normal file
@ -0,0 +1,741 @@
|
|||||||
|
export interface RpcUrl {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RpcUrls = {
|
||||||
|
[key in string]: RpcUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SubgraphUrl {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SubgraphUrls = {
|
||||||
|
[key in string]: SubgraphUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TornadoInstance = {
|
||||||
|
instanceAddress: {
|
||||||
|
[key in string]: string;
|
||||||
|
};
|
||||||
|
optionalInstances?: string[];
|
||||||
|
tokenAddress?: string;
|
||||||
|
tokenGasLimit?: number;
|
||||||
|
symbol: string;
|
||||||
|
decimals: number;
|
||||||
|
gasLimit?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TokenInstances = {
|
||||||
|
[key in string]: TornadoInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Config = {
|
||||||
|
rpcCallRetryAttempt?: number;
|
||||||
|
// Should be in gwei
|
||||||
|
gasPrices: {
|
||||||
|
// fallback gasPrice / maxFeePerGas value
|
||||||
|
instant: number;
|
||||||
|
fast?: number;
|
||||||
|
standard?: number;
|
||||||
|
low?: number;
|
||||||
|
// fallback EIP-1559 params
|
||||||
|
maxPriorityFeePerGas?: number;
|
||||||
|
};
|
||||||
|
nativeCurrency: string;
|
||||||
|
currencyName: string;
|
||||||
|
explorerUrl: {
|
||||||
|
tx: string;
|
||||||
|
address: string;
|
||||||
|
block: string;
|
||||||
|
};
|
||||||
|
merkleTreeHeight: number;
|
||||||
|
emptyElement: string;
|
||||||
|
networkName: string;
|
||||||
|
deployedBlock: number;
|
||||||
|
rpcUrls: RpcUrls;
|
||||||
|
multicall: string;
|
||||||
|
routerContract: string;
|
||||||
|
registryContract?: string;
|
||||||
|
echoContract: string;
|
||||||
|
aggregatorContract?: string;
|
||||||
|
reverseRecordsContract?: string;
|
||||||
|
gasPriceOracleContract?: string;
|
||||||
|
gasStationApi?: string;
|
||||||
|
ovmGasPriceOracleContract?: string;
|
||||||
|
tornadoSubgraph: string;
|
||||||
|
registrySubgraph?: string;
|
||||||
|
subgraphs: SubgraphUrls;
|
||||||
|
tokens: TokenInstances;
|
||||||
|
optionalTokens?: string[];
|
||||||
|
ensSubdomainKey: string;
|
||||||
|
// Should be in seconds
|
||||||
|
pollInterval: number;
|
||||||
|
constants: {
|
||||||
|
GOVERNANCE_BLOCK?: number;
|
||||||
|
NOTE_ACCOUNT_BLOCK?: number;
|
||||||
|
ENCRYPTED_NOTES_BLOCK?: number;
|
||||||
|
REGISTRY_BLOCK?: number;
|
||||||
|
// Should be in seconds
|
||||||
|
MINING_BLOCK_TIME?: number;
|
||||||
|
};
|
||||||
|
'torn.contract.tornadocash.eth'?: string;
|
||||||
|
'governance.contract.tornadocash.eth'?: string;
|
||||||
|
'staking-rewards.contract.tornadocash.eth'?: string;
|
||||||
|
'tornado-router.contract.tornadocash.eth'?: string;
|
||||||
|
'tornado-proxy-light.contract.tornadocash.eth'?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type networkConfig = {
|
||||||
|
[key in string]: Config;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const blockSyncInterval = 10000;
|
||||||
|
export const enabledChains = ['1', '10', '56', '100', '137', '42161', '43114', '11155111'];
|
||||||
|
|
||||||
|
const theGraph = {
|
||||||
|
name: 'Hosted Graph',
|
||||||
|
url: 'https://api.thegraph.com',
|
||||||
|
};
|
||||||
|
const tornado = {
|
||||||
|
name: 'Tornado Subgraphs',
|
||||||
|
url: 'https://tornadocash-rpc.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const networkConfig: networkConfig = {
|
||||||
|
netId1: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 80,
|
||||||
|
fast: 50,
|
||||||
|
standard: 25,
|
||||||
|
low: 8,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'eth',
|
||||||
|
currencyName: 'ETH',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://etherscan.io/tx/',
|
||||||
|
address: 'https://etherscan.io/address/',
|
||||||
|
block: 'https://etherscan.io/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Ethereum Mainnet',
|
||||||
|
deployedBlock: 9116966,
|
||||||
|
rpcUrls: {
|
||||||
|
tornado: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://tornadocash-rpc.com',
|
||||||
|
},
|
||||||
|
chainnodes: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
|
||||||
|
},
|
||||||
|
mevblockerRPC: {
|
||||||
|
name: 'MevblockerRPC',
|
||||||
|
url: 'https://rpc.mevblocker.io',
|
||||||
|
},
|
||||||
|
stackup: {
|
||||||
|
name: 'Stackup RPC',
|
||||||
|
url: 'https://public.stackup.sh/api/v1/node/ethereum-mainnet',
|
||||||
|
},
|
||||||
|
noderealRPC: {
|
||||||
|
name: 'NodeReal RPC',
|
||||||
|
url: 'https://eth-mainnet.nodereal.io/v1/1659dfb40aa24bbb8153a677b98064d7',
|
||||||
|
},
|
||||||
|
notadegenRPC: {
|
||||||
|
name: 'NotADegen RPC',
|
||||||
|
url: 'https://rpc.notadegen.com/eth',
|
||||||
|
},
|
||||||
|
keydonixRPC: {
|
||||||
|
name: 'Keydonix RPC',
|
||||||
|
url: 'https://ethereum.keydonix.com/v1/mainnet',
|
||||||
|
},
|
||||||
|
oneRPC: {
|
||||||
|
name: '1RPC',
|
||||||
|
url: 'https://1rpc.io/eth',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
routerContract: '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
|
||||||
|
registryContract: '0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2',
|
||||||
|
echoContract: '0x9B27DD5Bb15d42DC224FCD0B7caEbBe16161Df42',
|
||||||
|
aggregatorContract: '0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49',
|
||||||
|
reverseRecordsContract: '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C',
|
||||||
|
tornadoSubgraph: 'tornadocash/mainnet-tornado-subgraph',
|
||||||
|
registrySubgraph: 'tornadocash/tornado-relayer-registry',
|
||||||
|
subgraphs: {
|
||||||
|
tornado,
|
||||||
|
theGraph,
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
eth: {
|
||||||
|
instanceAddress: {
|
||||||
|
'0.1': '0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc',
|
||||||
|
'1': '0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936',
|
||||||
|
'10': '0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF',
|
||||||
|
'100': '0xA160cdAB225685dA1d56aa342Ad8841c3b53f291',
|
||||||
|
},
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
dai: {
|
||||||
|
instanceAddress: {
|
||||||
|
'100': '0xD4B88Df4D29F5CedD6857912842cff3b20C8Cfa3',
|
||||||
|
'1000': '0xFD8610d20aA15b7B2E3Be39B396a1bC3516c7144',
|
||||||
|
'10000': '0x07687e702b410Fa43f4cB4Af7FA097918ffD2730',
|
||||||
|
'100000': '0x23773E65ed146A459791799d01336DB287f25334',
|
||||||
|
},
|
||||||
|
tokenAddress: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
|
||||||
|
tokenGasLimit: 70_000,
|
||||||
|
symbol: 'DAI',
|
||||||
|
decimals: 18,
|
||||||
|
gasLimit: 700_000,
|
||||||
|
},
|
||||||
|
cdai: {
|
||||||
|
instanceAddress: {
|
||||||
|
'5000': '0x22aaA7720ddd5388A3c0A3333430953C68f1849b',
|
||||||
|
'50000': '0x03893a7c7463AE47D46bc7f091665f1893656003',
|
||||||
|
'500000': '0x2717c5e28cf931547B621a5dddb772Ab6A35B701',
|
||||||
|
'5000000': '0xD21be7248e0197Ee08E0c20D4a96DEBdaC3D20Af',
|
||||||
|
},
|
||||||
|
tokenAddress: '0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643',
|
||||||
|
tokenGasLimit: 200_000,
|
||||||
|
symbol: 'cDAI',
|
||||||
|
decimals: 8,
|
||||||
|
gasLimit: 700_000,
|
||||||
|
},
|
||||||
|
usdc: {
|
||||||
|
instanceAddress: {
|
||||||
|
'100': '0xd96f2B1c14Db8458374d9Aca76E26c3D18364307',
|
||||||
|
'1000': '0x4736dCf1b7A3d580672CcE6E7c65cd5cc9cFBa9D',
|
||||||
|
},
|
||||||
|
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||||
|
tokenGasLimit: 70_000,
|
||||||
|
symbol: 'USDC',
|
||||||
|
decimals: 6,
|
||||||
|
gasLimit: 700_000,
|
||||||
|
},
|
||||||
|
usdt: {
|
||||||
|
instanceAddress: {
|
||||||
|
'100': '0x169AD27A470D064DEDE56a2D3ff727986b15D52B',
|
||||||
|
'1000': '0x0836222F2B2B24A3F36f98668Ed8F0B38D1a872f',
|
||||||
|
},
|
||||||
|
tokenAddress: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
|
||||||
|
tokenGasLimit: 70_000,
|
||||||
|
symbol: 'USDT',
|
||||||
|
decimals: 6,
|
||||||
|
gasLimit: 700_000,
|
||||||
|
},
|
||||||
|
wbtc: {
|
||||||
|
instanceAddress: {
|
||||||
|
'0.1': '0x178169B423a011fff22B9e3F3abeA13414dDD0F1',
|
||||||
|
'1': '0x610B717796ad172B316836AC95a2ffad065CeaB4',
|
||||||
|
'10': '0xbB93e510BbCD0B7beb5A853875f9eC60275CF498',
|
||||||
|
},
|
||||||
|
tokenAddress: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
|
||||||
|
tokenGasLimit: 70_000,
|
||||||
|
symbol: 'WBTC',
|
||||||
|
decimals: 8,
|
||||||
|
gasLimit: 700_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'mainnet-tornado',
|
||||||
|
pollInterval: 15,
|
||||||
|
constants: {
|
||||||
|
GOVERNANCE_BLOCK: 11474695,
|
||||||
|
NOTE_ACCOUNT_BLOCK: 11842486,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 14248730,
|
||||||
|
REGISTRY_BLOCK: 14173129,
|
||||||
|
MINING_BLOCK_TIME: 15,
|
||||||
|
},
|
||||||
|
'torn.contract.tornadocash.eth': '0x77777FeDdddFfC19Ff86DB637967013e6C6A116C',
|
||||||
|
'governance.contract.tornadocash.eth': '0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce',
|
||||||
|
'tornado-router.contract.tornadocash.eth': '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b',
|
||||||
|
'staking-rewards.contract.tornadocash.eth': '0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29',
|
||||||
|
},
|
||||||
|
netId56: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 5,
|
||||||
|
fast: 5,
|
||||||
|
standard: 5,
|
||||||
|
low: 5,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'bnb',
|
||||||
|
currencyName: 'BNB',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://bscscan.com/tx/',
|
||||||
|
address: 'https://bscscan.com/address/',
|
||||||
|
block: 'https://bscscan.com/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Binance Smart Chain',
|
||||||
|
deployedBlock: 8158799,
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
|
||||||
|
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
tornadoSubgraph: 'tornadocash/bsc-tornado-subgraph',
|
||||||
|
subgraphs: {
|
||||||
|
tornado,
|
||||||
|
theGraph,
|
||||||
|
},
|
||||||
|
rpcUrls: {
|
||||||
|
tornado: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://tornadocash-rpc.com/bsc',
|
||||||
|
},
|
||||||
|
chainnodes: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://bsc-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
|
||||||
|
},
|
||||||
|
stackup: {
|
||||||
|
name: 'Stackup RPC',
|
||||||
|
url: 'https://public.stackup.sh/api/v1/node/bsc-mainnet',
|
||||||
|
},
|
||||||
|
noderealRPC: {
|
||||||
|
name: 'NodeReal RPC',
|
||||||
|
url: 'https://bsc-mainnet.nodereal.io/v1/64a9df0874fb4a93b9d0a3849de012d3',
|
||||||
|
},
|
||||||
|
oneRPC: {
|
||||||
|
name: '1RPC',
|
||||||
|
url: 'https://1rpc.io/bnb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
bnb: {
|
||||||
|
instanceAddress: {
|
||||||
|
'0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F',
|
||||||
|
'1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
|
||||||
|
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
|
||||||
|
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
|
||||||
|
},
|
||||||
|
symbol: 'BNB',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'bsc-tornado',
|
||||||
|
pollInterval: 10,
|
||||||
|
constants: {
|
||||||
|
NOTE_ACCOUNT_BLOCK: 8159269,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 8159269,
|
||||||
|
},
|
||||||
|
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
},
|
||||||
|
netId137: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 100,
|
||||||
|
fast: 75,
|
||||||
|
standard: 50,
|
||||||
|
low: 30,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'matic',
|
||||||
|
currencyName: 'MATIC',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://polygonscan.com/tx/',
|
||||||
|
address: 'https://polygonscan.com/address/',
|
||||||
|
block: 'https://polygonscan.com/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Polygon (Matic) Network',
|
||||||
|
deployedBlock: 16257962,
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
|
||||||
|
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
gasPriceOracleContract: '0xF81A8D8D3581985D3969fe53bFA67074aDFa8F3C',
|
||||||
|
tornadoSubgraph: 'tornadocash/matic-tornado-subgraph',
|
||||||
|
subgraphs: {
|
||||||
|
tornado,
|
||||||
|
theGraph,
|
||||||
|
},
|
||||||
|
rpcUrls: {
|
||||||
|
chainnodes: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
|
||||||
|
},
|
||||||
|
stackup: {
|
||||||
|
name: 'Stackup RPC',
|
||||||
|
url: 'https://public.stackup.sh/api/v1/node/polygon-mainnet',
|
||||||
|
},
|
||||||
|
oneRpc: {
|
||||||
|
name: '1RPC',
|
||||||
|
url: 'https://1rpc.io/matic',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
matic: {
|
||||||
|
instanceAddress: {
|
||||||
|
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
|
||||||
|
'1000': '0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178',
|
||||||
|
'10000': '0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040',
|
||||||
|
'100000': '0xa5C2254e4253490C54cef0a4347fddb8f75A4998',
|
||||||
|
},
|
||||||
|
symbol: 'MATIC',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'polygon-tornado',
|
||||||
|
pollInterval: 10,
|
||||||
|
constants: {
|
||||||
|
NOTE_ACCOUNT_BLOCK: 16257996,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 16257996,
|
||||||
|
},
|
||||||
|
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
},
|
||||||
|
netId10: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 0.001,
|
||||||
|
fast: 0.001,
|
||||||
|
standard: 0.001,
|
||||||
|
low: 0.001,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'eth',
|
||||||
|
currencyName: 'ETH',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://optimistic.etherscan.io/tx/',
|
||||||
|
address: 'https://optimistic.etherscan.io/address/',
|
||||||
|
block: 'https://optimistic.etherscan.io/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Optimism',
|
||||||
|
deployedBlock: 2243689,
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
|
||||||
|
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
ovmGasPriceOracleContract: '0x420000000000000000000000000000000000000F',
|
||||||
|
tornadoSubgraph: 'tornadocash/optimism-tornado-subgraph',
|
||||||
|
subgraphs: {
|
||||||
|
tornado,
|
||||||
|
theGraph,
|
||||||
|
},
|
||||||
|
rpcUrls: {
|
||||||
|
tornado: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://tornadocash-rpc.com/op',
|
||||||
|
},
|
||||||
|
chainnodes: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://optimism-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
|
||||||
|
},
|
||||||
|
optimism: {
|
||||||
|
name: 'Optimism RPC',
|
||||||
|
url: 'https://mainnet.optimism.io',
|
||||||
|
},
|
||||||
|
stackup: {
|
||||||
|
name: 'Stackup RPC',
|
||||||
|
url: 'https://public.stackup.sh/api/v1/node/optimism-mainnet',
|
||||||
|
},
|
||||||
|
oneRpc: {
|
||||||
|
name: '1RPC',
|
||||||
|
url: 'https://1rpc.io/op',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
eth: {
|
||||||
|
instanceAddress: {
|
||||||
|
'0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F',
|
||||||
|
'1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
|
||||||
|
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
|
||||||
|
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
|
||||||
|
},
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'optimism-tornado',
|
||||||
|
pollInterval: 15,
|
||||||
|
constants: {
|
||||||
|
NOTE_ACCOUNT_BLOCK: 2243694,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 2243694,
|
||||||
|
},
|
||||||
|
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
},
|
||||||
|
netId42161: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 4,
|
||||||
|
fast: 3,
|
||||||
|
standard: 2.52,
|
||||||
|
low: 2.29,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'eth',
|
||||||
|
currencyName: 'ETH',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://arbiscan.io/tx/',
|
||||||
|
address: 'https://arbiscan.io/address/',
|
||||||
|
block: 'https://arbiscan.io/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Arbitrum One',
|
||||||
|
deployedBlock: 3430648,
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
|
||||||
|
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
tornadoSubgraph: 'tornadocash/arbitrum-tornado-subgraph',
|
||||||
|
subgraphs: {
|
||||||
|
tornado,
|
||||||
|
theGraph,
|
||||||
|
},
|
||||||
|
rpcUrls: {
|
||||||
|
tornado: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://tornadocash-rpc.com/arbitrum',
|
||||||
|
},
|
||||||
|
chainnodes: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://arbitrum-one.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
|
||||||
|
},
|
||||||
|
arbitrum: {
|
||||||
|
name: 'Arbitrum RPC',
|
||||||
|
url: 'https://arb1.arbitrum.io/rpc',
|
||||||
|
},
|
||||||
|
stackup: {
|
||||||
|
name: 'Stackup RPC',
|
||||||
|
url: 'https://public.stackup.sh/api/v1/node/arbitrum-one',
|
||||||
|
},
|
||||||
|
oneRpc: {
|
||||||
|
name: '1rpc',
|
||||||
|
url: 'https://1rpc.io/arb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
eth: {
|
||||||
|
instanceAddress: {
|
||||||
|
'0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F',
|
||||||
|
'1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
|
||||||
|
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
|
||||||
|
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
|
||||||
|
},
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'arbitrum-tornado',
|
||||||
|
pollInterval: 15,
|
||||||
|
constants: {
|
||||||
|
NOTE_ACCOUNT_BLOCK: 3430605,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 3430605,
|
||||||
|
},
|
||||||
|
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
},
|
||||||
|
netId100: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 6,
|
||||||
|
fast: 5,
|
||||||
|
standard: 4,
|
||||||
|
low: 1,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'xdai',
|
||||||
|
currencyName: 'xDAI',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://blockscout.com/xdai/mainnet/tx/',
|
||||||
|
address: 'https://blockscout.com/xdai/mainnet/address/',
|
||||||
|
block: 'https://blockscout.com/xdai/mainnet/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Gnosis Chain',
|
||||||
|
deployedBlock: 17754561,
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
|
||||||
|
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
tornadoSubgraph: 'tornadocash/xdai-tornado-subgraph',
|
||||||
|
subgraphs: {
|
||||||
|
tornado,
|
||||||
|
theGraph,
|
||||||
|
},
|
||||||
|
rpcUrls: {
|
||||||
|
tornado: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://tornadocash-rpc.com/gnosis',
|
||||||
|
},
|
||||||
|
chainnodes: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://gnosis-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
|
||||||
|
},
|
||||||
|
gnosis: {
|
||||||
|
name: 'Gnosis RPC',
|
||||||
|
url: 'https://rpc.gnosischain.com',
|
||||||
|
},
|
||||||
|
stackup: {
|
||||||
|
name: 'Stackup RPC',
|
||||||
|
url: 'https://public.stackup.sh/api/v1/node/arbitrum-one',
|
||||||
|
},
|
||||||
|
blockPi: {
|
||||||
|
name: 'BlockPi',
|
||||||
|
url: 'https://gnosis.blockpi.network/v1/rpc/public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
xdai: {
|
||||||
|
instanceAddress: {
|
||||||
|
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
|
||||||
|
'1000': '0xdf231d99Ff8b6c6CBF4E9B9a945CBAcEF9339178',
|
||||||
|
'10000': '0xaf4c0B70B2Ea9FB7487C7CbB37aDa259579fe040',
|
||||||
|
'100000': '0xa5C2254e4253490C54cef0a4347fddb8f75A4998',
|
||||||
|
},
|
||||||
|
symbol: 'xDAI',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'gnosis-tornado',
|
||||||
|
pollInterval: 15,
|
||||||
|
constants: {
|
||||||
|
NOTE_ACCOUNT_BLOCK: 17754564,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 17754564,
|
||||||
|
},
|
||||||
|
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
},
|
||||||
|
netId43114: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 225,
|
||||||
|
fast: 35,
|
||||||
|
standard: 25,
|
||||||
|
low: 25,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'avax',
|
||||||
|
currencyName: 'AVAX',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://snowtrace.io/tx/',
|
||||||
|
address: 'https://snowtrace.io/address/',
|
||||||
|
block: 'https://snowtrace.io/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Avalanche Mainnet',
|
||||||
|
deployedBlock: 4429818,
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
echoContract: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
|
||||||
|
routerContract: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
tornadoSubgraph: 'tornadocash/avalanche-tornado-subgraph',
|
||||||
|
subgraphs: {
|
||||||
|
theGraph,
|
||||||
|
},
|
||||||
|
rpcUrls: {
|
||||||
|
oneRPC: {
|
||||||
|
name: 'OneRPC',
|
||||||
|
url: 'https://1rpc.io/avax/c',
|
||||||
|
},
|
||||||
|
avalancheRPC: {
|
||||||
|
name: 'Avalanche RPC',
|
||||||
|
url: 'https://api.avax.network/ext/bc/C/rpc',
|
||||||
|
},
|
||||||
|
meowRPC: {
|
||||||
|
name: 'Meow RPC',
|
||||||
|
url: 'https://avax.meowrpc.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
avax: {
|
||||||
|
instanceAddress: {
|
||||||
|
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
|
||||||
|
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD',
|
||||||
|
'500': '0xaf8d1839c3c67cf571aa74B5c12398d4901147B3',
|
||||||
|
},
|
||||||
|
symbol: 'AVAX',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'avalanche-tornado',
|
||||||
|
pollInterval: 10,
|
||||||
|
constants: {
|
||||||
|
NOTE_ACCOUNT_BLOCK: 4429813,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 4429813,
|
||||||
|
},
|
||||||
|
'tornado-proxy-light.contract.tornadocash.eth': '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||||
|
},
|
||||||
|
netId11155111: {
|
||||||
|
rpcCallRetryAttempt: 15,
|
||||||
|
gasPrices: {
|
||||||
|
instant: 2,
|
||||||
|
fast: 2,
|
||||||
|
standard: 2,
|
||||||
|
low: 2,
|
||||||
|
},
|
||||||
|
nativeCurrency: 'eth',
|
||||||
|
currencyName: 'SepoliaETH',
|
||||||
|
explorerUrl: {
|
||||||
|
tx: 'https://sepolia.etherscan.io/tx/',
|
||||||
|
address: 'https://sepolia.etherscan.io/address/',
|
||||||
|
block: 'https://sepolia.etherscan.io/block/',
|
||||||
|
},
|
||||||
|
merkleTreeHeight: 20,
|
||||||
|
emptyElement: '21663839004416932945382355908790599225266501822907911457504978515578255421292',
|
||||||
|
networkName: 'Ethereum Sepolia',
|
||||||
|
deployedBlock: 5594395,
|
||||||
|
multicall: '0xcA11bde05977b3631167028862bE2a173976CA11',
|
||||||
|
routerContract: '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
|
||||||
|
registryContract: '0x1428e5d2356b13778A13108b10c440C83011dfB8',
|
||||||
|
echoContract: '0xcDD1fc3F5ac2782D83449d3AbE80D6b7B273B0e5',
|
||||||
|
aggregatorContract: '0x4088712AC9fad39ea133cdb9130E465d235e9642',
|
||||||
|
reverseRecordsContract: '0xEc29700C0283e5Be64AcdFe8077d6cC95dE23C23',
|
||||||
|
tornadoSubgraph: 'tornadocash/sepolia-tornado-subgraph',
|
||||||
|
subgraphs: {
|
||||||
|
tornado,
|
||||||
|
},
|
||||||
|
rpcUrls: {
|
||||||
|
tornado: {
|
||||||
|
name: 'Tornado RPC',
|
||||||
|
url: 'https://tornadocash-rpc.com/sepolia',
|
||||||
|
},
|
||||||
|
sepolia: {
|
||||||
|
name: 'Sepolia RPC',
|
||||||
|
url: 'https://rpc.sepolia.org',
|
||||||
|
},
|
||||||
|
chainnodes: {
|
||||||
|
name: 'Chainnodes RPC',
|
||||||
|
url: 'https://sepolia.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
eth: {
|
||||||
|
instanceAddress: {
|
||||||
|
'0.1': '0x8C4A04d872a6C1BE37964A21ba3a138525dFF50b',
|
||||||
|
'1': '0x8cc930096B4Df705A007c4A039BDFA1320Ed2508',
|
||||||
|
'10': '0x8D10d506D29Fc62ABb8A290B99F66dB27Fc43585',
|
||||||
|
'100': '0x44c5C92ed73dB43888210264f0C8b36Fd68D8379',
|
||||||
|
},
|
||||||
|
symbol: 'ETH',
|
||||||
|
decimals: 18,
|
||||||
|
},
|
||||||
|
dai: {
|
||||||
|
instanceAddress: {
|
||||||
|
'100': '0x6921fd1a97441dd603a997ED6DDF388658daf754',
|
||||||
|
'1000': '0x50a637770F5d161999420F7d70d888DE47207145',
|
||||||
|
'10000': '0xecD649870407cD43923A816Cc6334a5bdf113621',
|
||||||
|
'100000': '0x73B4BD04bF83206B6e979BE2507098F92EDf4F90',
|
||||||
|
},
|
||||||
|
tokenAddress: '0xFF34B3d4Aee8ddCd6F9AFFFB6Fe49bD371b8a357',
|
||||||
|
tokenGasLimit: 70_000,
|
||||||
|
symbol: 'DAI',
|
||||||
|
decimals: 18,
|
||||||
|
gasLimit: 700_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ensSubdomainKey: 'sepolia-tornado',
|
||||||
|
pollInterval: 15,
|
||||||
|
constants: {
|
||||||
|
GOVERNANCE_BLOCK: 5594395,
|
||||||
|
NOTE_ACCOUNT_BLOCK: 5594395,
|
||||||
|
ENCRYPTED_NOTES_BLOCK: 5594395,
|
||||||
|
MINING_BLOCK_TIME: 15,
|
||||||
|
},
|
||||||
|
'torn.contract.tornadocash.eth': '0x3AE6667167C0f44394106E197904519D808323cA',
|
||||||
|
'governance.contract.tornadocash.eth': '0xe5324cD7602eeb387418e594B87aCADee08aeCAD',
|
||||||
|
'tornado-router.contract.tornadocash.eth': '0x1572AFE6949fdF51Cb3E0856216670ae9Ee160Ee',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const subdomains = enabledChains.map((chain) => networkConfig[`netId${chain}`].ensSubdomainKey);
|
||||||
|
|
||||||
|
export default networkConfig;
|
62
src/services/parser.ts
Normal file
62
src/services/parser.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { InvalidArgumentError } from 'commander';
|
||||||
|
import { computeAddress, getAddress, Mnemonic } from 'ethers';
|
||||||
|
import { validateUrl } from './utils';
|
||||||
|
|
||||||
|
export function parseNumber(value?: string | number): number {
|
||||||
|
if (!value || isNaN(Number(value))) {
|
||||||
|
throw new InvalidArgumentError('Invalid Number');
|
||||||
|
}
|
||||||
|
return Number(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseUrl(value?: string): string {
|
||||||
|
if (!value || !validateUrl(value, ['http:', 'https:'])) {
|
||||||
|
throw new InvalidArgumentError('Invalid URL');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRelayer(value?: string): string {
|
||||||
|
if (!value || !(value.endsWith('.eth') || validateUrl(value, ['http:', 'https:']))) {
|
||||||
|
throw new InvalidArgumentError('Invalid Relayer ETH address or URL');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAddress(value?: string): string {
|
||||||
|
if (!value) {
|
||||||
|
throw new InvalidArgumentError('Invalid Address');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return getAddress(value);
|
||||||
|
} catch {
|
||||||
|
throw new InvalidArgumentError('Invalid Address');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMnemonic(value?: string): string {
|
||||||
|
if (!value) {
|
||||||
|
throw new InvalidArgumentError('Invalid Mnemonic');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Mnemonic.fromPhrase(value);
|
||||||
|
} catch {
|
||||||
|
throw new InvalidArgumentError('Invalid Mnemonic');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseKey(value?: string): string {
|
||||||
|
if (!value) {
|
||||||
|
throw new InvalidArgumentError('Invalid Private Key');
|
||||||
|
}
|
||||||
|
if (value.length === 64) {
|
||||||
|
value = '0x' + value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
computeAddress(value);
|
||||||
|
} catch {
|
||||||
|
throw new InvalidArgumentError('Invalid Private Key');
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
32
src/services/pedersen.ts
Normal file
32
src/services/pedersen.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { BabyJub, PedersenHash, Point, buildPedersenHash } from 'circomlibjs';
|
||||||
|
|
||||||
|
export class Pedersen {
|
||||||
|
pedersenHash?: PedersenHash;
|
||||||
|
babyJub?: BabyJub;
|
||||||
|
pedersenPromise: Promise<void>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.pedersenPromise = this.initPedersen();
|
||||||
|
}
|
||||||
|
|
||||||
|
async initPedersen() {
|
||||||
|
this.pedersenHash = await buildPedersenHash();
|
||||||
|
this.babyJub = this.pedersenHash.babyJub;
|
||||||
|
}
|
||||||
|
|
||||||
|
async unpackPoint(buffer: Uint8Array) {
|
||||||
|
await this.pedersenPromise;
|
||||||
|
return this.babyJub?.unpackPoint(this.pedersenHash?.hash(buffer) as Uint8Array);
|
||||||
|
}
|
||||||
|
|
||||||
|
toStringBuffer(buffer: Uint8Array): string {
|
||||||
|
return this.babyJub?.F.toString(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pedersen = new Pedersen();
|
||||||
|
|
||||||
|
export async function buffPedersenHash(buffer: Uint8Array): Promise<string> {
|
||||||
|
const [hash] = (await pedersen.unpackPoint(buffer)) as Point;
|
||||||
|
return pedersen.toStringBuffer(hash);
|
||||||
|
}
|
31
src/services/prices.ts
Normal file
31
src/services/prices.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { parseEther, type Provider } from 'ethers';
|
||||||
|
import type { OffchainOracle, Multicall } from '../typechain';
|
||||||
|
import { multicall } from './multicall';
|
||||||
|
|
||||||
|
export class TokenPriceOracle {
|
||||||
|
oracle?: OffchainOracle;
|
||||||
|
multicall: Multicall;
|
||||||
|
provider: Provider;
|
||||||
|
|
||||||
|
constructor(provider: Provider, multicall: Multicall, oracle?: OffchainOracle) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.multicall = multicall;
|
||||||
|
this.oracle = oracle;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchPrices(tokens: string[]): Promise<bigint[]> {
|
||||||
|
// setup mock price for testnets
|
||||||
|
if (!this.oracle) {
|
||||||
|
return new Promise((resolve) => resolve(tokens.map(() => parseEther('0.0001'))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return multicall(
|
||||||
|
this.multicall,
|
||||||
|
tokens.map((token) => ({
|
||||||
|
contract: this.oracle,
|
||||||
|
name: 'getRateToEth',
|
||||||
|
params: [token, true],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
646
src/services/providers.ts
Normal file
646
src/services/providers.ts
Normal file
@ -0,0 +1,646 @@
|
|||||||
|
import type { EventEmitter } from 'stream';
|
||||||
|
import type { RequestOptions } from 'http';
|
||||||
|
import crossFetch from 'cross-fetch';
|
||||||
|
import { HttpProxyAgent } from 'http-proxy-agent';
|
||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
|
import {
|
||||||
|
FetchRequest,
|
||||||
|
JsonRpcApiProvider,
|
||||||
|
JsonRpcProvider,
|
||||||
|
Wallet,
|
||||||
|
HDNodeWallet,
|
||||||
|
FetchGetUrlFunc,
|
||||||
|
Provider,
|
||||||
|
SigningKey,
|
||||||
|
TransactionRequest,
|
||||||
|
JsonRpcSigner,
|
||||||
|
BrowserProvider,
|
||||||
|
Networkish,
|
||||||
|
Eip1193Provider,
|
||||||
|
VoidSigner,
|
||||||
|
Network,
|
||||||
|
parseUnits,
|
||||||
|
FetchUrlFeeDataNetworkPlugin,
|
||||||
|
BigNumberish,
|
||||||
|
FeeData,
|
||||||
|
EnsPlugin,
|
||||||
|
GasCostPlugin,
|
||||||
|
} from 'ethers';
|
||||||
|
import type { RequestInfo, RequestInit, Response, HeadersInit } from 'node-fetch';
|
||||||
|
import { GasPriceOracle, GasPriceOracle__factory, Multicall, Multicall__factory } from '../typechain';
|
||||||
|
import { isNode, sleep } from './utils';
|
||||||
|
import type { Config } from './networkConfig';
|
||||||
|
import { multicall } from './multicall';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
ethereum?: Eip1193Provider & EventEmitter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update this for every Tor Browser release
|
||||||
|
export const defaultUserAgent = 'Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0';
|
||||||
|
|
||||||
|
export const fetch = crossFetch as unknown as nodeFetch;
|
||||||
|
|
||||||
|
export type nodeFetch = (url: RequestInfo, init?: RequestInit) => Promise<Response>;
|
||||||
|
|
||||||
|
export type fetchDataOptions = RequestInit & {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
headers?: HeadersInit | any;
|
||||||
|
maxRetry?: number;
|
||||||
|
retryOn?: number;
|
||||||
|
userAgent?: string;
|
||||||
|
timeout?: number;
|
||||||
|
proxy?: string;
|
||||||
|
torPort?: number;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
debug?: Function;
|
||||||
|
returnResponse?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NodeAgent = RequestOptions['agent'] | ((parsedUrl: URL) => RequestOptions['agent']);
|
||||||
|
|
||||||
|
export function getHttpAgent({
|
||||||
|
fetchUrl,
|
||||||
|
proxyUrl,
|
||||||
|
torPort,
|
||||||
|
retry,
|
||||||
|
}: {
|
||||||
|
fetchUrl: string;
|
||||||
|
proxyUrl?: string;
|
||||||
|
torPort?: number;
|
||||||
|
retry: number;
|
||||||
|
}): NodeAgent | undefined {
|
||||||
|
if (torPort) {
|
||||||
|
return new SocksProxyAgent(`socks5h://tor${retry}@127.0.0.1:${torPort}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!proxyUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHttps = fetchUrl.includes('https://');
|
||||||
|
|
||||||
|
if (proxyUrl.includes('socks://') || proxyUrl.includes('socks4://') || proxyUrl.includes('socks5://')) {
|
||||||
|
return new SocksProxyAgent(proxyUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxyUrl.includes('http://') || proxyUrl.includes('https://')) {
|
||||||
|
if (isHttps) {
|
||||||
|
return new HttpsProxyAgent(proxyUrl);
|
||||||
|
}
|
||||||
|
return new HttpProxyAgent(proxyUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchData(url: string, options: fetchDataOptions = {}) {
|
||||||
|
const MAX_RETRY = options.maxRetry ?? 3;
|
||||||
|
const RETRY_ON = options.retryOn ?? 500;
|
||||||
|
const userAgent = options.userAgent ?? defaultUserAgent;
|
||||||
|
|
||||||
|
let retry = 0;
|
||||||
|
let errorObject;
|
||||||
|
|
||||||
|
if (!options.method) {
|
||||||
|
if (!options.body) {
|
||||||
|
options.method = 'GET';
|
||||||
|
} else {
|
||||||
|
options.method = 'POST';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.headers) {
|
||||||
|
options.headers = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNode && !options.headers['User-Agent']) {
|
||||||
|
options.headers['User-Agent'] = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (retry < MAX_RETRY + 1) {
|
||||||
|
let timeout;
|
||||||
|
|
||||||
|
// Define promise timeout when the options.timeout is available
|
||||||
|
if (!options.signal && options.timeout) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
options.signal = controller.signal;
|
||||||
|
|
||||||
|
// Define timeout in seconds
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
controller.abort();
|
||||||
|
}, options.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.agent && isNode && (options.proxy || options.torPort)) {
|
||||||
|
options.agent = getHttpAgent({
|
||||||
|
fetchUrl: url,
|
||||||
|
proxyUrl: options.proxy,
|
||||||
|
torPort: options.torPort,
|
||||||
|
retry,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.debug && typeof options.debug === 'function') {
|
||||||
|
options.debug('request', {
|
||||||
|
url,
|
||||||
|
retry,
|
||||||
|
errorObject,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: options.method,
|
||||||
|
headers: options.headers,
|
||||||
|
body: options.body,
|
||||||
|
redirect: options.redirect,
|
||||||
|
signal: options.signal,
|
||||||
|
agent: options.agent,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.debug && typeof options.debug === 'function') {
|
||||||
|
options.debug('response', resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const errMsg = `Request to ${url} failed with error code ${resp.status}:\n` + (await resp.text());
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.returnResponse) {
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = resp.headers.get('content-type');
|
||||||
|
|
||||||
|
// If server returns JSON object, parse it and return as an object
|
||||||
|
if (contentType?.includes('application/json')) {
|
||||||
|
return await resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else if the server returns text parse it as a string
|
||||||
|
if (contentType?.includes('text')) {
|
||||||
|
return await resp.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return as a response object https://developer.mozilla.org/en-US/docs/Web/API/Response
|
||||||
|
return resp;
|
||||||
|
} catch (error) {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
errorObject = error;
|
||||||
|
|
||||||
|
retry++;
|
||||||
|
|
||||||
|
await sleep(RETRY_ON);
|
||||||
|
} finally {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.debug && typeof options.debug === 'function') {
|
||||||
|
options.debug('error', errorObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw errorObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable prettier/prettier, @typescript-eslint/no-explicit-any */
|
||||||
|
export const fetchGetUrlFunc =
|
||||||
|
(options: fetchDataOptions = {}): FetchGetUrlFunc =>
|
||||||
|
async (req, _signal) => {
|
||||||
|
let signal;
|
||||||
|
|
||||||
|
if (_signal) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
signal = controller.signal;
|
||||||
|
_signal.addListener(() => {
|
||||||
|
controller.abort();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
...options,
|
||||||
|
method: req.method || 'POST',
|
||||||
|
headers: req.headers,
|
||||||
|
body: req.body || undefined,
|
||||||
|
signal,
|
||||||
|
returnResponse: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resp = await fetchData(req.url, init);
|
||||||
|
|
||||||
|
const headers = {} as { [key in string]: any };
|
||||||
|
resp.headers.forEach((value: any, key: string) => {
|
||||||
|
headers[key.toLowerCase()] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const respBody = await resp.arrayBuffer();
|
||||||
|
const body = respBody == null ? null : new Uint8Array(respBody);
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: resp.status,
|
||||||
|
statusMessage: resp.statusText,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* eslint-enable prettier/prettier, @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
// caching to improve performance
|
||||||
|
const oracleMapper = new Map();
|
||||||
|
const multicallMapper = new Map();
|
||||||
|
|
||||||
|
export type getProviderOptions = fetchDataOptions & {
|
||||||
|
pollingInterval?: number;
|
||||||
|
gasPriceOracle?: string;
|
||||||
|
gasStationApi?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getGasOraclePlugin(networkKey: string, fetchOptions?: getProviderOptions) {
|
||||||
|
const gasStationApi = fetchOptions?.gasStationApi || 'https://gasstation.polygon.technology/v2';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
return new FetchUrlFeeDataNetworkPlugin(gasStationApi, async (fetchFeeData, provider, request) => {
|
||||||
|
if (!oracleMapper.has(networkKey)) {
|
||||||
|
oracleMapper.set(networkKey, GasPriceOracle__factory.connect(fetchOptions?.gasPriceOracle as string, provider));
|
||||||
|
}
|
||||||
|
if (!multicallMapper.has(networkKey)) {
|
||||||
|
multicallMapper.set(
|
||||||
|
networkKey,
|
||||||
|
Multicall__factory.connect('0xcA11bde05977b3631167028862bE2a173976CA11', provider),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const Oracle = oracleMapper.get(networkKey) as GasPriceOracle;
|
||||||
|
const Multicall = multicallMapper.get(networkKey) as Multicall;
|
||||||
|
|
||||||
|
const [timestamp, heartbeat, feePerGas, priorityFeePerGas] = await multicall(Multicall, [
|
||||||
|
{
|
||||||
|
contract: Oracle,
|
||||||
|
name: 'timestamp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contract: Oracle,
|
||||||
|
name: 'heartbeat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contract: Oracle,
|
||||||
|
name: 'maxFeePerGas',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contract: Oracle,
|
||||||
|
name: 'maxPriorityFeePerGas',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const isOutdated = Number(timestamp) <= Date.now() / 1000 - Number(heartbeat);
|
||||||
|
|
||||||
|
if (!isOutdated) {
|
||||||
|
const maxPriorityFeePerGas = (priorityFeePerGas * BigInt(13)) / BigInt(10);
|
||||||
|
const maxFeePerGas = feePerGas * BigInt(2) + maxPriorityFeePerGas;
|
||||||
|
|
||||||
|
return {
|
||||||
|
gasPrice: maxFeePerGas,
|
||||||
|
maxFeePerGas,
|
||||||
|
maxPriorityFeePerGas,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchReq = new FetchRequest(gasStationApi);
|
||||||
|
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
|
||||||
|
if (isNode) {
|
||||||
|
// Prevent Cloudflare from blocking our request in node.js
|
||||||
|
fetchReq.setHeader('User-Agent', 'ethers');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [
|
||||||
|
{
|
||||||
|
bodyJson: { fast },
|
||||||
|
},
|
||||||
|
{ gasPrice },
|
||||||
|
] = await Promise.all([fetchReq.send(), fetchFeeData()]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gasPrice,
|
||||||
|
maxFeePerGas: parseUnits(`${fast.maxFee}`, 9),
|
||||||
|
maxPriorityFeePerGas: parseUnits(`${fast.maxPriorityFee}`, 9),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProvider(rpcUrl: string, fetchOptions?: getProviderOptions): Promise<JsonRpcProvider> {
|
||||||
|
const fetchReq = new FetchRequest(rpcUrl);
|
||||||
|
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
|
||||||
|
// omit network plugins and mimic registerEth function (required for polygon)
|
||||||
|
const _staticNetwork = await new JsonRpcProvider(fetchReq).getNetwork();
|
||||||
|
const ensPlugin = _staticNetwork.getPlugin('org.ethers.plugins.network.Ens');
|
||||||
|
const gasCostPlugin = _staticNetwork.getPlugin('org.ethers.plugins.network.GasCost');
|
||||||
|
const gasStationPlugin = <FetchUrlFeeDataNetworkPlugin>(
|
||||||
|
_staticNetwork.getPlugin('org.ethers.plugins.network.FetchUrlFeeDataPlugin')
|
||||||
|
);
|
||||||
|
const staticNetwork = new Network(_staticNetwork.name, _staticNetwork.chainId);
|
||||||
|
if (ensPlugin) {
|
||||||
|
staticNetwork.attachPlugin(ensPlugin);
|
||||||
|
}
|
||||||
|
if (gasCostPlugin) {
|
||||||
|
staticNetwork.attachPlugin(gasCostPlugin);
|
||||||
|
}
|
||||||
|
if (fetchOptions?.gasPriceOracle) {
|
||||||
|
staticNetwork.attachPlugin(getGasOraclePlugin(`${_staticNetwork.chainId}_${rpcUrl}`, fetchOptions));
|
||||||
|
} else if (gasStationPlugin) {
|
||||||
|
staticNetwork.attachPlugin(gasStationPlugin);
|
||||||
|
}
|
||||||
|
const provider = new JsonRpcProvider(fetchReq, staticNetwork, {
|
||||||
|
staticNetwork,
|
||||||
|
});
|
||||||
|
provider.pollingInterval = fetchOptions?.pollingInterval || 1000;
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProviderWithNetId(
|
||||||
|
netId: BigNumberish,
|
||||||
|
rpcUrl: string,
|
||||||
|
config: Config,
|
||||||
|
fetchOptions?: getProviderOptions,
|
||||||
|
): JsonRpcProvider {
|
||||||
|
const { networkName, reverseRecordsContract, gasPriceOracleContract, gasStationApi, pollInterval } = config;
|
||||||
|
const hasEns = Boolean(reverseRecordsContract);
|
||||||
|
|
||||||
|
const fetchReq = new FetchRequest(rpcUrl);
|
||||||
|
fetchReq.getUrlFunc = fetchGetUrlFunc(fetchOptions);
|
||||||
|
const staticNetwork = new Network(networkName, netId);
|
||||||
|
if (hasEns) {
|
||||||
|
staticNetwork.attachPlugin(new EnsPlugin(null, Number(netId)));
|
||||||
|
}
|
||||||
|
|
||||||
|
staticNetwork.attachPlugin(new GasCostPlugin());
|
||||||
|
|
||||||
|
if (gasPriceOracleContract) {
|
||||||
|
staticNetwork.attachPlugin(
|
||||||
|
getGasOraclePlugin(`${netId}_${rpcUrl}`, {
|
||||||
|
gasPriceOracle: gasPriceOracleContract,
|
||||||
|
gasStationApi,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = new JsonRpcProvider(fetchReq, staticNetwork, {
|
||||||
|
staticNetwork,
|
||||||
|
});
|
||||||
|
|
||||||
|
provider.pollingInterval = fetchOptions?.pollingInterval || pollInterval * 1000;
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const populateTransaction = async (
|
||||||
|
signer: TornadoWallet | TornadoVoidSigner | TornadoRpcSigner,
|
||||||
|
tx: TransactionRequest,
|
||||||
|
) => {
|
||||||
|
const provider = signer.provider as Provider;
|
||||||
|
|
||||||
|
if (!tx.from) {
|
||||||
|
tx.from = signer.address;
|
||||||
|
} else if (tx.from !== signer.address) {
|
||||||
|
const errMsg = `populateTransaction: signer mismatch for tx, wants ${tx.from} have ${signer.address}`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [feeData, nonce] = await Promise.all([
|
||||||
|
(async () => {
|
||||||
|
if (tx.maxFeePerGas && tx.maxPriorityFeePerGas) {
|
||||||
|
return new FeeData(null, BigInt(tx.maxFeePerGas), BigInt(tx.maxPriorityFeePerGas));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tx.gasPrice) {
|
||||||
|
return new FeeData(BigInt(tx.gasPrice), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedFeeData = await provider.getFeeData();
|
||||||
|
|
||||||
|
if (fetchedFeeData.maxFeePerGas && fetchedFeeData.maxPriorityFeePerGas) {
|
||||||
|
return new FeeData(
|
||||||
|
null,
|
||||||
|
(fetchedFeeData.maxFeePerGas * (BigInt(10000) + BigInt(signer.gasPriceBump))) / BigInt(10000),
|
||||||
|
fetchedFeeData.maxPriorityFeePerGas,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new FeeData(
|
||||||
|
((fetchedFeeData.gasPrice as bigint) * (BigInt(10000) + BigInt(signer.gasPriceBump))) / BigInt(10000),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
(async () => {
|
||||||
|
if (tx.nonce) {
|
||||||
|
return tx.nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchedNonce = await provider.getTransactionCount(signer.address, 'pending');
|
||||||
|
|
||||||
|
// Deal with cached nonce results
|
||||||
|
if (signer.bumpNonce && signer.nonce && signer.nonce >= fetchedNonce) {
|
||||||
|
console.log(
|
||||||
|
`populateTransaction: bumping nonce from ${fetchedNonce} to ${fetchedNonce + 1} for ${signer.address}`,
|
||||||
|
);
|
||||||
|
fetchedNonce++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetchedNonce;
|
||||||
|
})(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
tx.nonce = nonce;
|
||||||
|
|
||||||
|
// EIP-1559
|
||||||
|
if (feeData.maxFeePerGas && feeData.maxPriorityFeePerGas) {
|
||||||
|
tx.maxFeePerGas = feeData.maxFeePerGas;
|
||||||
|
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
|
||||||
|
if (!tx.type) {
|
||||||
|
tx.type = 2;
|
||||||
|
}
|
||||||
|
delete tx.gasPrice;
|
||||||
|
} else if (feeData.gasPrice) {
|
||||||
|
tx.gasPrice = feeData.gasPrice;
|
||||||
|
if (!tx.type) {
|
||||||
|
tx.type = 0;
|
||||||
|
}
|
||||||
|
delete tx.maxFeePerGas;
|
||||||
|
delete tx.maxPriorityFeePerGas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// gasLimit
|
||||||
|
tx.gasLimit =
|
||||||
|
tx.gasLimit ||
|
||||||
|
(await (async () => {
|
||||||
|
try {
|
||||||
|
const gasLimit = await provider.estimateGas(tx);
|
||||||
|
return gasLimit === BigInt(21000)
|
||||||
|
? gasLimit
|
||||||
|
: (gasLimit * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000);
|
||||||
|
} catch (err) {
|
||||||
|
if (signer.gasFailover) {
|
||||||
|
console.log('populateTransaction: warning gas estimation failed falling back to 3M gas');
|
||||||
|
// Gas failover
|
||||||
|
return BigInt('3000000');
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
|
||||||
|
return tx;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TornadoWalletOptions = {
|
||||||
|
gasPriceBump?: number;
|
||||||
|
gasLimitBump?: number;
|
||||||
|
gasFailover?: boolean;
|
||||||
|
bumpNonce?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TornadoWallet extends Wallet {
|
||||||
|
nonce?: number | null;
|
||||||
|
gasPriceBump: number;
|
||||||
|
gasLimitBump: number;
|
||||||
|
gasFailover: boolean;
|
||||||
|
bumpNonce: boolean;
|
||||||
|
constructor(
|
||||||
|
key: string | SigningKey,
|
||||||
|
provider?: null | Provider,
|
||||||
|
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {},
|
||||||
|
) {
|
||||||
|
super(key, provider);
|
||||||
|
// 10% bump from the recommended fee
|
||||||
|
this.gasPriceBump = gasPriceBump ?? 1000;
|
||||||
|
// 30% bump from the recommended gaslimit
|
||||||
|
this.gasLimitBump = gasLimitBump ?? 3000;
|
||||||
|
this.gasFailover = gasFailover ?? false;
|
||||||
|
// Disable bump nonce feature unless being used by the server environment
|
||||||
|
this.bumpNonce = bumpNonce ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromMnemonic(mneomnic: string, provider: Provider, index = 0, options?: TornadoWalletOptions) {
|
||||||
|
const defaultPath = `m/44'/60'/0'/0/${index}`;
|
||||||
|
const { privateKey } = HDNodeWallet.fromPhrase(mneomnic, undefined, defaultPath);
|
||||||
|
return new TornadoWallet(privateKey as unknown as SigningKey, provider, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
async populateTransaction(tx: TransactionRequest) {
|
||||||
|
const txObject = await populateTransaction(this, tx);
|
||||||
|
this.nonce = txObject.nonce;
|
||||||
|
|
||||||
|
return super.populateTransaction(txObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TornadoVoidSigner extends VoidSigner {
|
||||||
|
nonce?: number | null;
|
||||||
|
gasPriceBump: number;
|
||||||
|
gasLimitBump: number;
|
||||||
|
gasFailover: boolean;
|
||||||
|
bumpNonce: boolean;
|
||||||
|
constructor(
|
||||||
|
address: string,
|
||||||
|
provider?: null | Provider,
|
||||||
|
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {},
|
||||||
|
) {
|
||||||
|
super(address, provider);
|
||||||
|
// 10% bump from the recommended fee
|
||||||
|
this.gasPriceBump = gasPriceBump ?? 1000;
|
||||||
|
// 30% bump from the recommended gaslimit
|
||||||
|
this.gasLimitBump = gasLimitBump ?? 3000;
|
||||||
|
this.gasFailover = gasFailover ?? false;
|
||||||
|
// turn off bumpNonce feature for view only wallet
|
||||||
|
this.bumpNonce = bumpNonce ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async populateTransaction(tx: TransactionRequest) {
|
||||||
|
const txObject = await populateTransaction(this, tx);
|
||||||
|
this.nonce = txObject.nonce;
|
||||||
|
|
||||||
|
return super.populateTransaction(txObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TornadoRpcSigner extends JsonRpcSigner {
|
||||||
|
nonce?: number | null;
|
||||||
|
gasPriceBump: number;
|
||||||
|
gasLimitBump: number;
|
||||||
|
gasFailover: boolean;
|
||||||
|
bumpNonce: boolean;
|
||||||
|
constructor(
|
||||||
|
provider: JsonRpcApiProvider,
|
||||||
|
address: string,
|
||||||
|
{ gasPriceBump, gasLimitBump, gasFailover, bumpNonce }: TornadoWalletOptions = {},
|
||||||
|
) {
|
||||||
|
super(provider, address);
|
||||||
|
// 10% bump from the recommended fee
|
||||||
|
this.gasPriceBump = gasPriceBump ?? 1000;
|
||||||
|
// 30% bump from the recommended gaslimit
|
||||||
|
this.gasLimitBump = gasLimitBump ?? 3000;
|
||||||
|
this.gasFailover = gasFailover ?? false;
|
||||||
|
// turn off bumpNonce feature for browser wallet
|
||||||
|
this.bumpNonce = bumpNonce ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendUncheckedTransaction(tx: TransactionRequest) {
|
||||||
|
return super.sendUncheckedTransaction(await populateTransaction(this, tx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
export type connectWalletFunc = (...args: any[]) => Promise<void>;
|
||||||
|
|
||||||
|
export type handleWalletFunc = (...args: any[]) => void;
|
||||||
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
export type TornadoBrowserProviderOptions = TornadoWalletOptions & {
|
||||||
|
webChainId?: BigNumberish;
|
||||||
|
connectWallet?: connectWalletFunc;
|
||||||
|
handleNetworkChanges?: handleWalletFunc;
|
||||||
|
handleAccountChanges?: handleWalletFunc;
|
||||||
|
handleAccountDisconnect?: handleWalletFunc;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TornadoBrowserProvider extends BrowserProvider {
|
||||||
|
options?: TornadoBrowserProviderOptions;
|
||||||
|
constructor(ethereum: Eip1193Provider, network?: Networkish, options?: TornadoBrowserProviderOptions) {
|
||||||
|
super(ethereum, network);
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSigner(address: string): Promise<TornadoRpcSigner> {
|
||||||
|
const signerAddress = (await super.getSigner(address)).address;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.options?.webChainId &&
|
||||||
|
this.options?.connectWallet &&
|
||||||
|
Number(await super.send('eth_chainId', [])) !== Number(this.options?.webChainId)
|
||||||
|
) {
|
||||||
|
await this.options.connectWallet();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options?.handleNetworkChanges) {
|
||||||
|
window?.ethereum?.on('chainChanged', this.options.handleNetworkChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options?.handleAccountChanges) {
|
||||||
|
window?.ethereum?.on('accountsChanged', this.options.handleAccountChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options?.handleAccountDisconnect) {
|
||||||
|
window?.ethereum?.on('disconnect', this.options.handleAccountDisconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TornadoRpcSigner(this, signerAddress, this.options);
|
||||||
|
}
|
||||||
|
}
|
412
src/services/relayerClient.ts
Normal file
412
src/services/relayerClient.ts
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
import { namehash, parseEther } from 'ethers';
|
||||||
|
import type { Aggregator } from '@tornado/contracts';
|
||||||
|
import type { RelayerStructOutput } from '@tornado/contracts/dist/contracts/Governance/Aggregator/Aggregator';
|
||||||
|
import { sleep } from './utils';
|
||||||
|
import type { Config } from './networkConfig';
|
||||||
|
import { fetchData, fetchDataOptions } from './providers';
|
||||||
|
import { ajv, jobsSchema, getStatusSchema } from './schemas';
|
||||||
|
import type { snarkProofs } from './websnark';
|
||||||
|
|
||||||
|
export const MIN_STAKE_BALANCE = parseEther('500');
|
||||||
|
|
||||||
|
export interface RelayerParams {
|
||||||
|
ensName: string;
|
||||||
|
relayerAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Relayer {
|
||||||
|
netId: number;
|
||||||
|
url: string;
|
||||||
|
rewardAccount: string;
|
||||||
|
currentQueue: number;
|
||||||
|
tornadoServiceFee: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RelayerInfo = Relayer & {
|
||||||
|
hostname: string;
|
||||||
|
ensName: string;
|
||||||
|
stakeBalance: bigint;
|
||||||
|
relayerAddress: string;
|
||||||
|
ethPrices?: {
|
||||||
|
[key in string]: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RelayerError = {
|
||||||
|
hostname: string;
|
||||||
|
relayerAddress?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RelayerStatus {
|
||||||
|
url: string;
|
||||||
|
rewardAccount: string;
|
||||||
|
instances: {
|
||||||
|
[key in string]: {
|
||||||
|
instanceAddress: {
|
||||||
|
[key in string]: string;
|
||||||
|
};
|
||||||
|
tokenAddress?: string;
|
||||||
|
symbol: string;
|
||||||
|
decimals: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
gasPrices?: {
|
||||||
|
fast: number;
|
||||||
|
additionalProperties?: number;
|
||||||
|
};
|
||||||
|
netId: number;
|
||||||
|
ethPrices?: {
|
||||||
|
[key in string]: string;
|
||||||
|
};
|
||||||
|
tornadoServiceFee: number;
|
||||||
|
latestBlock?: number;
|
||||||
|
version: string;
|
||||||
|
health: {
|
||||||
|
status: string;
|
||||||
|
error: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
errorsLog: any[];
|
||||||
|
};
|
||||||
|
currentQueue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayerTornadoWithdraw {
|
||||||
|
id?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayerTornadoJobs {
|
||||||
|
error?: string;
|
||||||
|
id: string;
|
||||||
|
type?: string;
|
||||||
|
status: string;
|
||||||
|
contract?: string;
|
||||||
|
proof?: string;
|
||||||
|
args?: string[];
|
||||||
|
txHash?: string;
|
||||||
|
confirmations?: number;
|
||||||
|
failedReason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const semVerRegex =
|
||||||
|
/^(?<major>0|[1-9]\d*)\.(?<minor>0|[1-9]\d*)\.(?<patch>0|[1-9]\d*)(?:-(?<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
|
||||||
|
|
||||||
|
export interface semanticVersion {
|
||||||
|
major: string;
|
||||||
|
minor: string;
|
||||||
|
patch: string;
|
||||||
|
prerelease?: string;
|
||||||
|
buildmetadata?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSemanticVersion(version: string) {
|
||||||
|
const { groups } = semVerRegex.exec(version) as RegExpExecArray;
|
||||||
|
return groups as unknown as semanticVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRelayerUpdated(relayerVersion: string, netId: number | string) {
|
||||||
|
const { major, patch, prerelease } = parseSemanticVersion(relayerVersion);
|
||||||
|
// Save backwards compatibility with V4 relayers for Ethereum Mainnet
|
||||||
|
const requiredMajor = netId === 1 ? '4' : '5';
|
||||||
|
const isUpdatedMajor = major === requiredMajor;
|
||||||
|
|
||||||
|
if (prerelease) return false;
|
||||||
|
return isUpdatedMajor && (Number(patch) >= 5 || Number(netId) !== 1); // Patch checking - also backwards compatibility for Mainnet
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateScore({ stakeBalance, tornadoServiceFee }: RelayerInfo, minFee = 0.33, maxFee = 0.53) {
|
||||||
|
if (tornadoServiceFee < minFee) {
|
||||||
|
tornadoServiceFee = minFee;
|
||||||
|
} else if (tornadoServiceFee >= maxFee) {
|
||||||
|
return BigInt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceFeeCoefficient = (tornadoServiceFee - minFee) ** 2;
|
||||||
|
const feeDiffCoefficient = 1 / (maxFee - minFee) ** 2;
|
||||||
|
const coefficientsMultiplier = 1 - feeDiffCoefficient * serviceFeeCoefficient;
|
||||||
|
|
||||||
|
return BigInt(Math.floor(Number(stakeBalance) * coefficientsMultiplier));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeightRandom(weightsScores: bigint[], random: bigint) {
|
||||||
|
for (let i = 0; i < weightsScores.length; i++) {
|
||||||
|
if (random < weightsScores[i]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
random = random - weightsScores[i];
|
||||||
|
}
|
||||||
|
return Math.floor(Math.random() * weightsScores.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickWeightedRandomRelayer(relayers: RelayerInfo[], netId: string | number) {
|
||||||
|
let minFee: number, maxFee: number;
|
||||||
|
|
||||||
|
if (Number(netId) !== 1) {
|
||||||
|
minFee = 0.01;
|
||||||
|
maxFee = 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
const weightsScores = relayers.map((el) => calculateScore(el, minFee, maxFee));
|
||||||
|
const totalWeight = weightsScores.reduce((acc, curr) => {
|
||||||
|
return (acc = acc + curr);
|
||||||
|
}, BigInt('0'));
|
||||||
|
|
||||||
|
const random = BigInt(Number(totalWeight) * Math.random());
|
||||||
|
const weightRandomIndex = getWeightRandom(weightsScores, random);
|
||||||
|
|
||||||
|
return relayers[weightRandomIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelayerClientConstructor {
|
||||||
|
netId: number | string;
|
||||||
|
config: Config;
|
||||||
|
Aggregator: Aggregator;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RelayerClientWithdraw = snarkProofs & {
|
||||||
|
contract: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class RelayerClient {
|
||||||
|
netId: number;
|
||||||
|
config: Config;
|
||||||
|
Aggregator: Aggregator;
|
||||||
|
selectedRelayer?: Relayer;
|
||||||
|
fetchDataOptions?: fetchDataOptions;
|
||||||
|
|
||||||
|
constructor({ netId, config, Aggregator, fetchDataOptions }: RelayerClientConstructor) {
|
||||||
|
this.netId = Number(netId);
|
||||||
|
this.config = config;
|
||||||
|
this.Aggregator = Aggregator;
|
||||||
|
this.fetchDataOptions = fetchDataOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
async askRelayerStatus({
|
||||||
|
hostname,
|
||||||
|
relayerAddress,
|
||||||
|
}: {
|
||||||
|
hostname: string;
|
||||||
|
relayerAddress?: string;
|
||||||
|
}): Promise<RelayerStatus> {
|
||||||
|
const url = `https://${!hostname.endsWith('/') ? hostname + '/' : hostname}`;
|
||||||
|
|
||||||
|
const rawStatus = (await fetchData(`${url}status`, {
|
||||||
|
...this.fetchDataOptions,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json, application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
timeout: this.fetchDataOptions?.torPort ? 10000 : 3000,
|
||||||
|
maxRetry: this.fetchDataOptions?.torPort ? 2 : 0,
|
||||||
|
})) as object;
|
||||||
|
|
||||||
|
const statusValidator = ajv.compile(getStatusSchema(this.netId, this.config));
|
||||||
|
|
||||||
|
if (!statusValidator(rawStatus)) {
|
||||||
|
throw new Error('Invalid status schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
...rawStatus,
|
||||||
|
url,
|
||||||
|
} as RelayerStatus;
|
||||||
|
|
||||||
|
if (status.currentQueue > 5) {
|
||||||
|
throw new Error('Withdrawal queue is overloaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.netId !== this.netId) {
|
||||||
|
throw new Error('This relayer serves a different network');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relayerAddress && this.netId === 1 && status.rewardAccount !== relayerAddress) {
|
||||||
|
throw new Error('The Relayer reward address must match registered address');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRelayerUpdated(status.version, this.netId)) {
|
||||||
|
throw new Error('Outdated version.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async filterRelayer(
|
||||||
|
curr: RelayerStructOutput,
|
||||||
|
relayer: RelayerParams,
|
||||||
|
subdomains: string[],
|
||||||
|
debugRelayer: boolean = false,
|
||||||
|
): Promise<RelayerInfo | RelayerError> {
|
||||||
|
const { ensSubdomainKey } = this.config;
|
||||||
|
const subdomainIndex = subdomains.indexOf(ensSubdomainKey);
|
||||||
|
const mainnetSubdomain = curr.records[0];
|
||||||
|
const hostname = curr.records[subdomainIndex];
|
||||||
|
const isHostWithProtocol = hostname.includes('http');
|
||||||
|
|
||||||
|
const { owner, balance: stakeBalance, isRegistered } = curr;
|
||||||
|
const { ensName, relayerAddress } = relayer;
|
||||||
|
|
||||||
|
const isOwner = !relayerAddress || relayerAddress === owner;
|
||||||
|
const hasMinBalance = stakeBalance >= MIN_STAKE_BALANCE;
|
||||||
|
|
||||||
|
const preCondition =
|
||||||
|
hostname && isOwner && mainnetSubdomain && isRegistered && hasMinBalance && !isHostWithProtocol;
|
||||||
|
|
||||||
|
if (preCondition || debugRelayer) {
|
||||||
|
try {
|
||||||
|
const status = await this.askRelayerStatus({ hostname, relayerAddress });
|
||||||
|
|
||||||
|
return {
|
||||||
|
netId: status.netId,
|
||||||
|
url: status.url,
|
||||||
|
hostname,
|
||||||
|
ensName,
|
||||||
|
stakeBalance,
|
||||||
|
relayerAddress,
|
||||||
|
rewardAccount: status.rewardAccount,
|
||||||
|
ethPrices: status.ethPrices,
|
||||||
|
currentQueue: status.currentQueue,
|
||||||
|
tornadoServiceFee: status.tornadoServiceFee,
|
||||||
|
} as RelayerInfo;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} catch (err: any) {
|
||||||
|
if (debugRelayer) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
hostname,
|
||||||
|
relayerAddress,
|
||||||
|
errorMessage: err.message,
|
||||||
|
} as RelayerError;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (debugRelayer) {
|
||||||
|
const errMsg = `Relayer ${hostname} condition not met`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
hostname,
|
||||||
|
relayerAddress,
|
||||||
|
errorMessage: `Relayer ${hostname} condition not met`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValidRelayers(
|
||||||
|
// this should be ascending order of events
|
||||||
|
relayers: RelayerParams[],
|
||||||
|
subdomains: string[],
|
||||||
|
debugRelayer: boolean = false,
|
||||||
|
): Promise<{
|
||||||
|
validRelayers: RelayerInfo[];
|
||||||
|
invalidRelayers: RelayerError[];
|
||||||
|
}> {
|
||||||
|
const relayersSet = new Set();
|
||||||
|
|
||||||
|
const uniqueRelayers = relayers.reverse().filter(({ ensName }) => {
|
||||||
|
if (!relayersSet.has(ensName)) {
|
||||||
|
relayersSet.add(ensName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const relayerNameHashes = uniqueRelayers.map((r) => namehash(r.ensName));
|
||||||
|
|
||||||
|
const relayersData = await this.Aggregator.relayersData.staticCall(relayerNameHashes, subdomains);
|
||||||
|
|
||||||
|
const invalidRelayers: RelayerError[] = [];
|
||||||
|
|
||||||
|
const validRelayers = (
|
||||||
|
await Promise.all(
|
||||||
|
relayersData.map((curr, index) => this.filterRelayer(curr, uniqueRelayers[index], subdomains, debugRelayer)),
|
||||||
|
)
|
||||||
|
).filter((r) => {
|
||||||
|
if ((r as RelayerError).errorMessage) {
|
||||||
|
invalidRelayers.push(r);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}) as RelayerInfo[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
validRelayers,
|
||||||
|
invalidRelayers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pickWeightedRandomRelayer(relayers: RelayerInfo[]) {
|
||||||
|
return pickWeightedRandomRelayer(relayers, this.netId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async tornadoWithdraw({ contract, proof, args }: RelayerClientWithdraw) {
|
||||||
|
const { url } = this.selectedRelayer as Relayer;
|
||||||
|
|
||||||
|
const withdrawResponse = (await fetchData(`${url}v1/tornadoWithdraw`, {
|
||||||
|
...this.fetchDataOptions,
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
contract,
|
||||||
|
proof,
|
||||||
|
args,
|
||||||
|
}),
|
||||||
|
})) as RelayerTornadoWithdraw;
|
||||||
|
|
||||||
|
const { id, error } = withdrawResponse;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
let relayerStatus: string | undefined;
|
||||||
|
|
||||||
|
const jobUrl = `${url}v1/jobs/${id}`;
|
||||||
|
|
||||||
|
console.log(`Job submitted: ${jobUrl}\n`);
|
||||||
|
|
||||||
|
while (!relayerStatus || !['FAILED', 'CONFIRMED'].includes(relayerStatus)) {
|
||||||
|
const jobResponse = await fetchData(jobUrl, {
|
||||||
|
...this.fetchDataOptions,
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (jobResponse.error) {
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jobValidator = ajv.compile(jobsSchema);
|
||||||
|
|
||||||
|
if (!jobValidator(jobResponse)) {
|
||||||
|
const errMsg = `${jobUrl} has an invalid job response`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { status, txHash, confirmations, failedReason } = jobResponse as unknown as RelayerTornadoJobs;
|
||||||
|
|
||||||
|
if (relayerStatus !== status) {
|
||||||
|
if (status === 'FAILED') {
|
||||||
|
const errMsg = `Job ${status}: ${jobUrl} failed reason: ${failedReason}`;
|
||||||
|
throw new Error(errMsg);
|
||||||
|
} else if (status === 'SENT') {
|
||||||
|
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}\n`);
|
||||||
|
} else if (status === 'MINED') {
|
||||||
|
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`);
|
||||||
|
} else if (status === 'CONFIRMED') {
|
||||||
|
console.log(`Job ${status}: ${jobUrl}, txhash: ${txHash}, confirmations: ${confirmations}\n`);
|
||||||
|
} else {
|
||||||
|
console.log(`Job ${status}: ${jobUrl}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
relayerStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
src/services/schemas/index.ts
Normal file
21
src/services/schemas/index.ts
Normal file
@ -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';
|
59
src/services/schemas/jobs.ts
Normal file
59
src/services/schemas/jobs.ts
Normal file
@ -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'],
|
||||||
|
};
|
181
src/services/schemas/status.ts
Normal file
181
src/services/schemas/status.ts
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import type { Config } from '../networkConfig';
|
||||||
|
|
||||||
|
export type statusInstanceType = {
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
instanceAddress: {
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
[key in string]: typeof addressType;
|
||||||
|
};
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
tokenAddress?: typeof addressType;
|
||||||
|
symbol?: { enum: string[] };
|
||||||
|
decimals: { enum: number[] };
|
||||||
|
};
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type statusInstancesType = {
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
[key in string]: statusInstanceType;
|
||||||
|
};
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type statusEthPricesType = {
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
[key in string]: typeof bnType;
|
||||||
|
};
|
||||||
|
required?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type statusSchema = {
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
rewardAccount: typeof addressType;
|
||||||
|
instances?: statusInstancesType;
|
||||||
|
gasPrices: {
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
[key in string]: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
netId: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
ethPrices?: statusEthPricesType;
|
||||||
|
tornadoServiceFee?: {
|
||||||
|
type: string;
|
||||||
|
maximum: number;
|
||||||
|
minimum: number;
|
||||||
|
};
|
||||||
|
latestBlock?: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
version: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
health: {
|
||||||
|
type: string;
|
||||||
|
properties: {
|
||||||
|
status: { const: string };
|
||||||
|
error: { type: string };
|
||||||
|
};
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
currentQueue: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const addressType = { type: 'string', pattern: '^0x[a-fA-F0-9]{40}$' };
|
||||||
|
|
||||||
|
const bnType = { type: 'string', BN: true };
|
||||||
|
|
||||||
|
const statusSchema: statusSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
rewardAccount: addressType,
|
||||||
|
gasPrices: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fast: { type: 'number' },
|
||||||
|
additionalProperties: { type: 'number' },
|
||||||
|
},
|
||||||
|
required: ['fast'],
|
||||||
|
},
|
||||||
|
netId: { type: 'integer' },
|
||||||
|
tornadoServiceFee: { type: 'number', maximum: 20, minimum: 0 },
|
||||||
|
latestBlock: { type: 'number' },
|
||||||
|
version: { type: 'string' },
|
||||||
|
health: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
status: { const: 'true' },
|
||||||
|
error: { type: 'string' },
|
||||||
|
},
|
||||||
|
required: ['status'],
|
||||||
|
},
|
||||||
|
currentQueue: { type: 'number' },
|
||||||
|
},
|
||||||
|
required: ['rewardAccount', 'instances', 'netId', 'tornadoServiceFee', 'version', 'health'],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getStatusSchema(netId: number | string, config: Config) {
|
||||||
|
const { tokens, optionalTokens = [], nativeCurrency } = config;
|
||||||
|
|
||||||
|
// deep copy schema
|
||||||
|
const schema = JSON.parse(JSON.stringify(statusSchema)) as statusSchema;
|
||||||
|
|
||||||
|
const instances = Object.keys(tokens).reduce(
|
||||||
|
(acc: statusInstancesType, token) => {
|
||||||
|
const { instanceAddress, tokenAddress, symbol, decimals, optionalInstances = [] } = tokens[token];
|
||||||
|
const amounts = Object.keys(instanceAddress);
|
||||||
|
|
||||||
|
const instanceProperties: statusInstanceType = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
instanceAddress: {
|
||||||
|
type: 'object',
|
||||||
|
properties: amounts.reduce((acc: { [key in string]: typeof addressType }, cur) => {
|
||||||
|
acc[cur] = addressType;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
required: amounts.filter((amount) => !optionalInstances.includes(amount)),
|
||||||
|
},
|
||||||
|
decimals: { enum: [decimals] },
|
||||||
|
},
|
||||||
|
required: ['instanceAddress', 'decimals'].concat(
|
||||||
|
tokenAddress ? ['tokenAddress'] : [],
|
||||||
|
symbol ? ['symbol'] : [],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tokenAddress) {
|
||||||
|
instanceProperties.properties.tokenAddress = addressType;
|
||||||
|
}
|
||||||
|
if (symbol) {
|
||||||
|
instanceProperties.properties.symbol = { enum: [symbol] };
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.properties[token] = instanceProperties;
|
||||||
|
if (!optionalTokens.includes(token)) {
|
||||||
|
acc.required.push(token);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
schema.properties.instances = instances;
|
||||||
|
|
||||||
|
if (Number(netId) === 1) {
|
||||||
|
const _tokens = Object.keys(tokens).filter((t) => t !== nativeCurrency);
|
||||||
|
|
||||||
|
const ethPrices: statusEthPricesType = {
|
||||||
|
type: 'object',
|
||||||
|
properties: _tokens.reduce((acc: { [key in string]: typeof bnType }, token: string) => {
|
||||||
|
acc[token] = bnType;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
// required: _tokens
|
||||||
|
};
|
||||||
|
schema.properties.ethPrices = ethPrices;
|
||||||
|
// schema.required.push('ethPrices')
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema;
|
||||||
|
}
|
90
src/services/tokens.ts
Normal file
90
src/services/tokens.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { Provider, ZeroAddress } from 'ethers';
|
||||||
|
import { ERC20__factory, Multicall } from '../typechain';
|
||||||
|
import { chunk } from './utils';
|
||||||
|
import { Call3, multicall } from './multicall';
|
||||||
|
|
||||||
|
export interface tokenBalances {
|
||||||
|
address: string;
|
||||||
|
name: string;
|
||||||
|
symbol: string;
|
||||||
|
decimals: number;
|
||||||
|
balance: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTokenBalances({
|
||||||
|
provider,
|
||||||
|
Multicall,
|
||||||
|
currencyName,
|
||||||
|
userAddress,
|
||||||
|
tokenAddresses = [],
|
||||||
|
}: {
|
||||||
|
provider: Provider;
|
||||||
|
Multicall: Multicall;
|
||||||
|
currencyName: string;
|
||||||
|
userAddress: string;
|
||||||
|
tokenAddresses: string[];
|
||||||
|
}): Promise<tokenBalances[]> {
|
||||||
|
const tokenCalls = tokenAddresses
|
||||||
|
.map((tokenAddress) => {
|
||||||
|
const Token = ERC20__factory.connect(tokenAddress, provider);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
contract: Token,
|
||||||
|
name: 'balanceOf',
|
||||||
|
params: [userAddress],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contract: Token,
|
||||||
|
name: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contract: Token,
|
||||||
|
name: 'symbol',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
contract: Token,
|
||||||
|
name: 'decimals',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
})
|
||||||
|
.flat() as Call3[];
|
||||||
|
|
||||||
|
const multicallResults = await multicall(Multicall, [
|
||||||
|
{
|
||||||
|
contract: Multicall,
|
||||||
|
name: 'getEthBalance',
|
||||||
|
params: [userAddress],
|
||||||
|
},
|
||||||
|
...(tokenCalls.length ? tokenCalls : []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ethResults = multicallResults[0];
|
||||||
|
const tokenResults = multicallResults.slice(1).length
|
||||||
|
? chunk(multicallResults.slice(1), tokenCalls.length / tokenAddresses.length)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const tokenBalances = tokenResults.map((tokenResult, index) => {
|
||||||
|
const [tokenBalance, tokenName, tokenSymbol, tokenDecimals] = tokenResult;
|
||||||
|
const tokenAddress = tokenAddresses[index];
|
||||||
|
|
||||||
|
return {
|
||||||
|
address: tokenAddress,
|
||||||
|
name: tokenName,
|
||||||
|
symbol: tokenSymbol,
|
||||||
|
decimals: Number(tokenDecimals),
|
||||||
|
balance: tokenBalance,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
address: ZeroAddress,
|
||||||
|
name: currencyName,
|
||||||
|
symbol: currencyName,
|
||||||
|
decimals: 18,
|
||||||
|
balance: ethResults,
|
||||||
|
},
|
||||||
|
...tokenBalances,
|
||||||
|
];
|
||||||
|
}
|
131
src/services/utils.ts
Normal file
131
src/services/utils.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { URL } from 'url';
|
||||||
|
import BN from 'bn.js';
|
||||||
|
import type { BigNumberish } from 'ethers';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(BigInt.prototype as any).toJSON = function () {
|
||||||
|
return this.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
type bnInput = number | string | number[] | Uint8Array | Buffer | BN;
|
||||||
|
|
||||||
|
export const isNode =
|
||||||
|
!(
|
||||||
|
process as typeof process & {
|
||||||
|
browser?: boolean;
|
||||||
|
}
|
||||||
|
).browser && typeof globalThis.window === 'undefined';
|
||||||
|
|
||||||
|
export const chunk = <T>(arr: T[], size: number): T[][] =>
|
||||||
|
[...Array(Math.ceil(arr.length / size))].map((_, i) => arr.slice(size * i, size + size * i));
|
||||||
|
|
||||||
|
export function sleep(ms: number) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateUrl(url: string, protocols?: string[]) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
if (protocols && protocols.length) {
|
||||||
|
return protocols.map((p) => p.toLowerCase()).includes(parsedUrl.protocol);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bufferToBytes(b: Buffer) {
|
||||||
|
return new Uint8Array(b.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToBase64(bytes: Uint8Array) {
|
||||||
|
let binary = '';
|
||||||
|
const len = bytes.byteLength;
|
||||||
|
for (let i = 0; i < len; ++i) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64ToBytes(base64: string) {
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToHex(bytes: Uint8Array) {
|
||||||
|
return (
|
||||||
|
'0x' +
|
||||||
|
Array.from(bytes)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert BE encoded bytes (Buffer | Uint8Array) array to BigInt
|
||||||
|
export function bytesToBN(bytes: Uint8Array) {
|
||||||
|
return BigInt(bytesToHex(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert BigInt to BE encoded Uint8Array type
|
||||||
|
export function bnToBytes(bigint: bigint | string) {
|
||||||
|
// Parse bigint to hex string
|
||||||
|
let hexString: string = typeof bigint === 'bigint' ? bigint.toString(16) : bigint;
|
||||||
|
// Remove hex string prefix if exists
|
||||||
|
if (hexString.startsWith('0x')) {
|
||||||
|
hexString = hexString.replace('0x', '');
|
||||||
|
}
|
||||||
|
// Hex string length should be a multiplier of two (To make correct bytes)
|
||||||
|
if (hexString.length % 2 !== 0) {
|
||||||
|
hexString = '0' + hexString;
|
||||||
|
}
|
||||||
|
return Uint8Array.from((hexString.match(/.{1,2}/g) as string[]).map((byte) => parseInt(byte, 16)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert LE encoded bytes (Buffer | Uint8Array) array to BigInt
|
||||||
|
export function leBuff2Int(bytes: Uint8Array) {
|
||||||
|
return new BN(bytes, 16, 'le');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert BigInt to LE encoded Uint8Array type
|
||||||
|
export function leInt2Buff(bigint: bnInput | bigint) {
|
||||||
|
return Uint8Array.from(new BN(bigint as bnInput).toArray('le', 31));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inherited from tornado-core and tornado-cli
|
||||||
|
export function toFixedHex(numberish: BigNumberish, length = 32) {
|
||||||
|
return (
|
||||||
|
'0x' +
|
||||||
|
BigInt(numberish)
|
||||||
|
.toString(16)
|
||||||
|
.padStart(length * 2, '0')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFixedLength(string: string, length: number = 32) {
|
||||||
|
string = string.replace('0x', '');
|
||||||
|
return '0x' + string.padStart(length * 2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random BigInt in a range of bytes
|
||||||
|
export function rBigInt(nbytes: number = 31) {
|
||||||
|
return bytesToBN(crypto.getRandomValues(new Uint8Array(nbytes)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for JSON.stringify(value, bigIntReplacer, space)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function bigIntReplacer(key: any, value: any) {
|
||||||
|
return typeof value === 'bigint' ? value.toString() : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function substring(str: string, length: number = 10) {
|
||||||
|
if (str.length < length * 2) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str.substring(0, length)}...${str.substring(str.length - length)}`;
|
||||||
|
}
|
85
src/services/websnark.ts
Normal file
85
src/services/websnark.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// @ts-expect-error no-websnark-types
|
||||||
|
import * as websnarkUtils from '@tornado/websnark/src/utils';
|
||||||
|
// @ts-expect-error no-websnark-types
|
||||||
|
import websnarkGroth from '@tornado/websnark/src/groth16';
|
||||||
|
import type { Element } from '@tornado/fixed-merkle-tree';
|
||||||
|
import type { AddressLike, BytesLike, BigNumberish } from 'ethers';
|
||||||
|
import { toFixedHex } from './utils';
|
||||||
|
|
||||||
|
export type snarkInputs = {
|
||||||
|
// Public snark inputs
|
||||||
|
root: Element;
|
||||||
|
nullifierHex: string;
|
||||||
|
recipient: AddressLike;
|
||||||
|
relayer: AddressLike;
|
||||||
|
fee: bigint;
|
||||||
|
refund: bigint;
|
||||||
|
|
||||||
|
// Private snark inputs
|
||||||
|
nullifier: bigint;
|
||||||
|
secret: bigint;
|
||||||
|
pathElements: Element[];
|
||||||
|
pathIndices: Element[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type snarkArgs = [
|
||||||
|
_root: BytesLike,
|
||||||
|
_nullifierHash: BytesLike,
|
||||||
|
_recipient: AddressLike,
|
||||||
|
_relayer: AddressLike,
|
||||||
|
_fee: BigNumberish,
|
||||||
|
_refund: BigNumberish,
|
||||||
|
];
|
||||||
|
|
||||||
|
export type snarkProofs = {
|
||||||
|
proof: BytesLike;
|
||||||
|
args: snarkArgs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
let groth16: any;
|
||||||
|
|
||||||
|
const groth16Promise = (async () => {
|
||||||
|
if (!groth16) {
|
||||||
|
groth16 = await websnarkGroth({ wasmInitialMemory: 2000 });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
export async function calculateSnarkProof(
|
||||||
|
input: snarkInputs,
|
||||||
|
circuit: object,
|
||||||
|
provingKey: ArrayBuffer,
|
||||||
|
): Promise<snarkProofs> {
|
||||||
|
await groth16Promise;
|
||||||
|
|
||||||
|
const snarkInput = {
|
||||||
|
root: input.root,
|
||||||
|
nullifierHash: BigInt(input.nullifierHex).toString(),
|
||||||
|
recipient: BigInt(input.recipient as string),
|
||||||
|
relayer: BigInt(input.relayer as string),
|
||||||
|
fee: input.fee,
|
||||||
|
refund: input.refund,
|
||||||
|
|
||||||
|
nullifier: input.nullifier,
|
||||||
|
secret: input.secret,
|
||||||
|
pathElements: input.pathElements,
|
||||||
|
pathIndices: input.pathIndices,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Start generating SNARK proof', snarkInput);
|
||||||
|
console.time('SNARK proof time');
|
||||||
|
const proofData = await websnarkUtils.genWitnessAndProve(groth16, snarkInput, circuit, provingKey);
|
||||||
|
const proof = websnarkUtils.toSolidityInput(proofData).proof as BytesLike;
|
||||||
|
console.timeEnd('SNARK proof time');
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
toFixedHex(input.root, 32) as BytesLike,
|
||||||
|
toFixedHex(input.nullifierHex, 32) as BytesLike,
|
||||||
|
input.recipient,
|
||||||
|
input.relayer,
|
||||||
|
toFixedHex(input.fee, 32) as BigNumberish,
|
||||||
|
toFixedHex(input.refund, 32) as BigNumberish,
|
||||||
|
] as snarkArgs;
|
||||||
|
|
||||||
|
return { proof, args };
|
||||||
|
}
|
@ -42,7 +42,7 @@
|
|||||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving 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. */
|
// "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. */
|
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||||
|
|
||||||
@ -108,5 +108,6 @@
|
|||||||
/* Completeness */
|
/* Completeness */
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||||
}
|
},
|
||||||
|
"include": ["./src/**/*"]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user