master #1

Closed
tornadosto wants to merge 0 commits from (deleted):master into master
16 changed files with 4923 additions and 1819 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ node_modules
backup-tornado-*
backup-tornadoInvoice-*
test
parseTool.js

View File

@ -5,8 +5,8 @@ RUN apt update && apt install --yes --no-install-recommends wget git apt-transpo
WORKDIR /home/root/tornado-cli
ENV GIT_REPOSITORY=https://git.tornado.ws/tornadocash/tornado-cli
ENV GIT_COMMIT_HASH=7e0d8d9183f1f29409e1ec0b3c94a44354ae30fa
ENV GIT_REPOSITORY=https://git.tornado.ws/tornadosto/tornado-cli.git
ENV GIT_COMMIT_HASH=5efae744be6c1d3269d697a032fae389f8ddf0cb
RUN git init && \
git remote add origin $GIT_REPOSITORY && \
@ -18,3 +18,7 @@ RUN npm ci
RUN npm install -g pkg@5.8.1
RUN node scripts/createDeterministicExecutable.js
RUN printf '#!/bin/sh\ncp /home/root/tornado-cli/tornado-cli.exe /output/' > /copy_out.sh && chmod +x /copy_out.sh
CMD ["/bin/bash"]

View File

@ -53,12 +53,7 @@ Also, you can use VPN for CLI without Tor or even combine it.
All code logic located in `cli.js`, other files is just testing scripts or static files. You can check or change any part of the code and at next `node cli.js` run changes will apply.
Any user can check that the precompiled `tornado-cli.exe` matches the source code. To verify **tornado-cli.exe**:
1. Build docker image from latest commit: `docker build -t tornado-cli .`
2. Create container`docker create --name tornado-cli tornado-cli:latest`
3. Copy executable from container `docker cp tornado-cli:/home/root/tornado-cli/tornado-cli.exe tornado-cli-verify.exe`
4. Compare hashes of original tornado-cli.exe and freshly generated executable with two commands: on Windows use `CertUtil -hashfile tornado-cli.exe SHA256` and `CertUtil -hashfile tornado-cli-verify.exe SHA256`, on Linux - `sha256sum tornado-cli.exe` and `sha256sum tornado-cli-verify.exe`. Hashes should be identical.
Any user can check that the precompiled `tornado-cli.exe` matches the source code. To build `tornado-cli.exe`, [pkg](https://www.npmjs.com/package/pkg) package is used, which allows creating deterministic and reproducible executable from source. Creating script is located at `./scripts/createDeterministicExecutable.js`, and you can rebuild executable with command `npm run createExe`, or you can verify that sha1 hash of existing executable matches new executable, verification script located at `./scripts/verifyExecutable` and you can call it with command `npm run verifyExe` and compare hashes.
### Commands and usage
@ -168,3 +163,21 @@ View transaction on block explorer https://goerli.etherscan.io/tx/0x6ded443caed8
Tornado contract balance is xxx.x ETH
Sender account balance is x.xxxxxxx ETH
```
#### To verify:
```bash
$ docker build -t tornado-cli:latest .
```
wait for docker to build
```bash
$ docker run --rm -v %cd%/output:/output tornado-cli:latest /copy_out.sh
```
copy exe to current folder in windows
```bash
CertUtil -hashfile output/tornado-cli.exe SHA256
CertUtil -hashfile tornado-cli.exe SHA256
```
compare with the exe in git

File diff suppressed because it is too large Load Diff

View File

@ -1,290 +0,0 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "governanceAddress",
"type": "address"
},
{
"internalType": "address",
"name": "tornAddress",
"type": "address"
},
{
"internalType": "address",
"name": "_relayerRegistry",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "rewardsClaimed",
"type": "uint256"
}
],
"name": "RewardsClaimed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "rewards",
"type": "uint256"
}
],
"name": "RewardsUpdated",
"type": "event"
},
{
"inputs": [],
"name": "Governance",
"outputs": [
{
"internalType": "contract ITornadoGovernance",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "accumulatedRewardPerTorn",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "accumulatedRewardRateOnLastUpdate",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "accumulatedRewards",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "addBurnRewards",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32[]",
"name": "domains",
"type": "bytes32[]"
}
],
"name": "bulkResolve",
"outputs": [
{
"internalType": "address[]",
"name": "result",
"type": "address[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "checkReward",
"outputs": [
{
"internalType": "uint256",
"name": "rewards",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getReward",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "ratioConstant",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "relayerRegistry",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "node",
"type": "bytes32"
}
],
"name": "resolve",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "setReward",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "torn",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "amountLockedBeforehand",
"type": "uint256"
}
],
"name": "updateRewardsOnLockedBalanceChange",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "withdrawTorn",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

View File

@ -0,0 +1,110 @@
[
{
"blockNumber": 22385840,
"transactionHash": "0x63fa83cf4127ef4147d507b23501179cfc6c037731e8dfb4fd2cc49c09b9eae8",
"commitment": "0x03f8ca1f7d166d11531313ef8b15acb22b19ae5dd98ceea6886b16a320a517cd",
"leafIndex": 0
},
{
"blockNumber": 22385872,
"transactionHash": "0xf55784219dc9d71c142b42f4099e6b791ef5fb2035ee9d14c8e82879a37eae2c",
"commitment": "0x1be521b0899780c2dbc5ce84dfcf90b5635692b23f6ea1042c9312d742e990d8",
"leafIndex": 1
},
{
"blockNumber": 22392807,
"transactionHash": "0x9c7c0202da743a351aafc60c5e62f46c920b7ef8ab0fbd9a437ee5df2f273a52",
"commitment": "0x2c6e62f51b8251c9ebdeccbcdd58efe15b35326cb3f1e0ddd4b99eed3c9fccb0",
"leafIndex": 2
},
{
"blockNumber": 22392987,
"transactionHash": "0x81505022469948ef6d700738463cbfa30aab567897a7bcd7c5b9ac34c25f9b14",
"commitment": "0x1fc417281a28562a41f921786f147ac95e77323304c5cdb9a9359d2dd971056f",
"leafIndex": 3
},
{
"blockNumber": 22393172,
"transactionHash": "0x764550f81da7ba82bb609bd45bba1f0e61deb14fa1a30a320ac8fa7042f6f383",
"commitment": "0x29b11c7da6fcaeb3c6248472677a65fb55daec2a110b90ff0d5c7ad95d4bd05e",
"leafIndex": 4
},
{
"blockNumber": 22393172,
"transactionHash": "0x207a4a95a0bd67e8d7bfb35f81a02d80465b467467fd3097bcf9877a5d1dd712",
"commitment": "0x0a29115a2af3acbb0820056604fd123ed365a3b8e856901430b0c5609ed03897",
"leafIndex": 5
},
{
"blockNumber": 22393172,
"transactionHash": "0x6afd92cf8f9f0afec38179f6c223e49b69fd414354f382c87a0f3e23b596dab3",
"commitment": "0x167ece4de25842f445e5005a525f20f86f7edc8b2a65a923184b052f43a894d5",
"leafIndex": 6
},
{
"blockNumber": 22393172,
"transactionHash": "0xd4c4bae2a66fb7aa192d81144459f1e9030a5211f68a644919fbad722a644b0c",
"commitment": "0x15902671ae98cbd40887e395eeef6e9d66972c671fdd688cdecbc6b7c89b3bc0",
"leafIndex": 7
},
{
"blockNumber": 22393172,
"transactionHash": "0x84fb0853a93fb412f3570a115e56f97677df99762f741c6c281df411703ea14b",
"commitment": "0x146518004a7639391f39833de66bce38ccb23f7d24ae072f2ea7e3ad349fbcb0",
"leafIndex": 8
},
{
"blockNumber": 22393287,
"transactionHash": "0x9c9d005fb758fdc3810d26923573e27c6149cec3d3f0e219ea7182010f8279a0",
"commitment": "0x1c7e098f281b291f47ffdbe734bd7762638692765aa10ae79d00296c683b4f64",
"leafIndex": 9
},
{
"blockNumber": 22393298,
"transactionHash": "0xa145a95807fb52c42dcaa478f5784755feedbc06948871066f2c4b63a674e583",
"commitment": "0x0e874573d767d81c8e04427b38ef7ee88732c9c92c9534f3b42965091d244d95",
"leafIndex": 10
},
{
"blockNumber": 22393384,
"transactionHash": "0xaefad2d9200f27c1ff2dc6731563753fd87ffd5abb16f2610f9d8408e5173643",
"commitment": "0x109040cd8b4f5073949d62c7923cc8030fcc92e3675cc35932a4cbbacb8ce6be",
"leafIndex": 11
},
{
"blockNumber": 22396258,
"transactionHash": "0x6135e90e4c74bf5b3271a44850a10f5556bb3ca57d8f2850c0eddfb1375a1280",
"commitment": "0x034e30e5e1c21deaa71cfedea0fa153c25d3ff380bed56f83a78ed5a307a0c36",
"leafIndex": 12
},
{
"blockNumber": 22396797,
"transactionHash": "0x82dd8478501366f3d7bc82694af2bdbeb5fd3878baf4ddb9d02f1eaf7d31ce63",
"commitment": "0x1469fb2b9bed134e8cff2ada483a8f416b2a44629c7a388dd713b9dc05b87d02",
"leafIndex": 13
},
{
"blockNumber": 22396930,
"transactionHash": "0x56d1744e17e907f0a33ac774eed8dcf1ddb003e14d1e52bf37fdbcf41b0a8cb2",
"commitment": "0x10199e8b32f29fdc6410cac40d9db40302e0902afd71e7a86f94a33af9a6c6d0",
"leafIndex": 14
},
{
"blockNumber": 22396986,
"transactionHash": "0xd3f697cbe6384d33e3751e222e75ed868adc2175bacd1f3bf91cdd9fa2332a5e",
"commitment": "0x0e13899823c0e21680f21de2e1ffd74d899ebfebbd767c8dd3a9b665fb9a2ed4",
"leafIndex": 15
},
{
"blockNumber": 22399955,
"transactionHash": "0xbe98adfe86b0492cd85da8c684e53a4adc2b0b62eae2495c7a5962da635c80e9",
"commitment": "0x227deb325a01d3d83b36b03f2c31b761cf8c20e6f722c2f7c2ea8be02284fce1",
"leafIndex": 16
},
{
"blockNumber": 22410370,
"transactionHash": "0x99194cba56f82b27b6928cb37a72a441d83a990e9f8072b12d0c31cce79eb6af",
"commitment": "0x143e37303e307f5367ba3fc59a066fca1be31cdb714d622ab8ffa4246faafc05",
"leafIndex": 17
}
]

View File

@ -0,0 +1,79 @@
[
{
"blockNumber": 22385858,
"transactionHash": "0x1a8cb44a9d03d2e1248deed7c65d86b30e2ae0f6ccd1e26dfcdd026e6e7ba7e2",
"nullifierHash": "0x01de4b0b2bee6944f771806e61dcb8f223f77ebcb86140040e4b18532dcfa642",
"to": "0xc2f667db796e82913160c1e01030aabbd5ead47e",
"fee": "0"
},
{
"blockNumber": 22385878,
"transactionHash": "0xef83a1582639c13b2ccf90d10305d2c873eedc04346563988d0175cb2e9cf876",
"nullifierHash": "0x0466c43a2ba160630a9929a1e9ef956610ab9fd8038fadd0d80dd6dab498ee37",
"to": "0xc2f667db796e82913160c1e01030aabbd5ead47e",
"fee": "0"
},
{
"blockNumber": 22393007,
"transactionHash": "0x397d7d92ca3aaa571bb10f8f8cb1dc272ecb7be35bc4e8131ce086d36c2b5f5f",
"nullifierHash": "0x11e974a721874186ce971f36873c013bc93c464ed26a6a82ed96319573f9c673",
"to": "0xb4ef209ccee95de23a8e1f7627ac7e676ff0739d",
"fee": "7182616000000000"
},
{
"blockNumber": 22393369,
"transactionHash": "0x8d5d7de4fcd014b050b9feff398fafd88112c04b0fccd420944ecabfe339dd44",
"nullifierHash": "0x22af5c5cd73087a65567b25a3da887d3dc192806691a3694fa868f9ea63ef7d2",
"to": "0xc0f12799b8d3fa8810dfe1616095170c72117f8f",
"fee": "1986919520000000"
},
{
"blockNumber": 22393391,
"transactionHash": "0xfdcb3c4521394b1871243e1a6ad3ee1c5c0a18e118c571f4206e6a7b48e96a34",
"nullifierHash": "0x0add34f677a5e2faccda12a32698418222f07075983b205bd990e68c0b96255e",
"to": "0xc0f12799b8d3fa8810dfe1616095170c72117f8f",
"fee": "4974722720000000"
},
{
"blockNumber": 22393496,
"transactionHash": "0x840dcdc3982c47ca46b088bd9d63977682a6eb2461df832ee1a85d41d5e8475b",
"nullifierHash": "0x1fca9aec5973b7cefbd632df53224c32a5bdb7e0c5516b1bfbcf7819f3f1c1b3",
"to": "0xc2f667db796e82913160c1e01030aabbd5ead47e",
"fee": "0"
},
{
"blockNumber": 22396800,
"transactionHash": "0x306a40c939450320a433399435e93b99f6de9c835196cb5aef94e97829e7a165",
"nullifierHash": "0x1664ce55a6e2520b562b8d2a9e4a965b4e72478bb423bdcd671e89ca9083b515",
"to": "0xb4ef209ccee95de23a8e1f7627ac7e676ff0739d",
"fee": "1574701600000000"
},
{
"blockNumber": 22396811,
"transactionHash": "0x9728fab8b35a5a4fd0e6a67bb0e90963ade1262f5bb49a80a736253c27c3017e",
"nullifierHash": "0x19aa705bf1b01bf5a304b46f5f4f960766f8191068750373b4585ebb0d5f0dee",
"to": "0x9b5fbedaa0c2474b43e4f96f58e62f702614c8b8",
"fee": "1575916000000000"
},
{
"blockNumber": 22397057,
"transactionHash": "0x45a369f482fc98f39eb55c4bd2e113ebf8436f93a01211609881acdab5a87d06",
"nullifierHash": "0x1f9d9874d23cbd163d31b8f2b936ba18d5b3a42991519970bdea29712e1f0ff0",
"to": "0xc0f12799b8d3fa8810dfe1616095170c72117f8f",
"fee": "4986919520000000"
},
{
"blockNumber": 22397185,
"transactionHash": "0x55ece3a9554848a6042ce8cbd4ff22ddd64d51d4b63347d15c661ba8345a68ff",
"nullifierHash": "0x1934703fb8829f2e2764b8621e208a9c0a51396a338ea1d098591c7b9a30c991",
"to": "0x9b5fbedaa0c2474b43e4f96f58e62f702614c8b8",
"fee": "4975916000000000"
},
{
"blockNumber": 22399965,
"transactionHash": "0xd881c6f30fc6486559dc7bfd870b753895a7bab0d101bec8ecb3c1d60a4bd68e",
"nullifierHash": "0x21edacf1834d5506c88be35371f7b328f89f3ed3947b58df5ce34a741adc96f2",
"to": "0x754e3d0f4473d2bd7b6bcb62a4c61706d1be9545",
"fee": "0"
}
]

2894
cache/sepolia/deposits_eth_0.1.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -550,23 +550,5 @@
"transactionHash": "0xf810858c8d70168efa0c4db912ed7caf191b1379612c6e4ec0adc7272d48e599",
"commitment": "0x05cef42e5d479a19a6d6dfc3e2a59efee92ab350875aa77c84bec4b5d7f04546",
"leafIndex": 91
},
{
"blockNumber": 8548903,
"transactionHash": "0xd00051d1b3d58e8abdf2e333b84a80e48722342e2e60371d4ca5fc74b4812968",
"commitment": "0x2820eab529e832cb5550001ffbb336b11000cbaf7d7e444283858df83ec752e7",
"leafIndex": 92
},
{
"blockNumber": 8554220,
"transactionHash": "0xe9d72cb832e7e45fb43d3c3f5f4485b759dd298e1488e37bcf7f0f2e14340cf1",
"commitment": "0x1bc58d9aa06b54578c59cf5503bf5f9824a46463976cdcc750bcc789d1f17827",
"leafIndex": 93
},
{
"blockNumber": 8582499,
"transactionHash": "0xb5b3bf519294917588501dedeaff90f75274a34e38c02dd93640847cee3a2a61",
"commitment": "0x305c2a56eb349ca8910cdce13ac0ff86a607521423481a315a40c9b80e30c484",
"leafIndex": 94
}
]

View File

@ -1,86 +0,0 @@
[
{
"blockNumber": 6808552,
"transactionHash": "0x7d7f9d83dc671ddac7f68fbd1643f35ee388f74c049339a40c92bca9120f303b",
"commitment": "0x1fabbaf0879a9691cf5d7d0cfab8411ac18b59cde8aa6336647c55b5b0364756",
"leafIndex": 0
},
{
"blockNumber": 6841965,
"transactionHash": "0x894a12cdc48d3c2f1c0e41403014c750eee232fc0942aa6a1b7b45e8ffe01c25",
"commitment": "0x28554b45d48c8b4defc13874c0bed9a3a2508947de91664be901b747985b8c66",
"leafIndex": 1
},
{
"blockNumber": 6841972,
"transactionHash": "0x13f1e212b49d814066afa79a7ec9c0ebfdb19cd49ab1a4f54753d2bea0707eba",
"commitment": "0x1c3378f8e955fe66f5d6f2621ddb812bbbfd53d25f2a1efc3aaaf7539639109b",
"leafIndex": 2
},
{
"blockNumber": 6841977,
"transactionHash": "0x128193940812eee01b874336378215c02446b9022671c19b516a5a47c4943094",
"commitment": "0x11dd59ccfcf8b8f95a8d488b6b92dfef7271426b2d8a80724aa458a949d2d616",
"leafIndex": 3
},
{
"blockNumber": 6841996,
"transactionHash": "0x91abeea9495392b3e86fad1de78a02ab98c7ec7309f764de513cc5da27c6fb7b",
"commitment": "0x1ebe8c66bb44d730e96a0c3030deaf099eadeb7c49f3fc558e66484d5a59e479",
"leafIndex": 4
},
{
"blockNumber": 6842009,
"transactionHash": "0x643c36ca6a6770c0838bd03da4424f41eba4ae0010bbfe6c2db3fc82e8ee3f77",
"commitment": "0x09b3231821f0cc8b957051feafa73796a43567c16802a59b8d355abad013d302",
"leafIndex": 5
},
{
"blockNumber": 6842010,
"transactionHash": "0x7e3bd713be872432cf0fd25aad30f5607b7346754a4d55040811b3ed24623a97",
"commitment": "0x1a63b64f8687b587c159aede8bcf1baf34a4bf9aa4d5d10d6bc5959a62a61f8e",
"leafIndex": 6
},
{
"blockNumber": 6859777,
"transactionHash": "0xa7e5cd6440fb27401c2065562ad003e27f56bf5e3deb1113c11f91e9a5fe5a14",
"commitment": "0x2ec46d07c14093a5b59061e647b2780d7b7d43495ae175c414a40e3bf3434aef",
"leafIndex": 7
},
{
"blockNumber": 6859779,
"transactionHash": "0xcc0f337ac271d595cfac1e5f2ad3b5e12983dbbff945929a32e1f964c12ba8d4",
"commitment": "0x2e6f47a8e1ccd23af268fb5d2ef8739b74cd486b480963e76493f73d3eca8ae7",
"leafIndex": 8
},
{
"blockNumber": 6859781,
"transactionHash": "0x9384d9be1508d30b76d97fe1f83b29f91948ad6aae886c107bd8cc620b3c28d7",
"commitment": "0x2827191f0a60a6867114fe38db2adbf96543e23d821979f5825a07de76f06edd",
"leafIndex": 9
},
{
"blockNumber": 8142964,
"transactionHash": "0x3637a59d43ded88e11a1e33a47e9ac25d588f1a645d73ce463d32c8f5ee20c41",
"commitment": "0x25e1e0db5f9fee5f34a6bf177bb102ae3bb55ddbb850a1394bb788a0f83402e7",
"leafIndex": 10
},
{
"blockNumber": 8414361,
"transactionHash": "0x9db998ec88fc6569d8707a09d9b556bd3332b84037ed05b0ce73e97e37dc637d",
"commitment": "0x030d556c506fac8d8420e8583c8683af885761e08c68647c51300836c5d061c3",
"leafIndex": 11
},
{
"blockNumber": 8415769,
"transactionHash": "0xae54c08901cc31f66e5f2aa559b65b4ad41d335e2247f4fdd71bc99b3588e7e4",
"commitment": "0x1cdc8de34b0a461e6486770f97bc623b1f34ea10f79b6e98f8f47ef33dc02a20",
"leafIndex": 12
},
{
"blockNumber": 8589745,
"transactionHash": "0xdabfe58e8c2beb13e636b751c36efde634f62f38ac65ad05e52ab2cc5009b8bf",
"commitment": "0x0cbe58b8e9002532253174ff8f0e4574d968760f4fb35c948a9a947850739639",
"leafIndex": 13
}
]

1626
cache/sepolia/withdrawals_eth_0.1.json vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -446,26 +446,5 @@
"nullifierHash": "0x00fac88aeaaf4cdfe15e4f0e246ccf7e5b64adca4a0699989f9eaeb47d0a32bd",
"to": "0xc53c51af207c224a865d8bb5e1948ac5c2cb8442",
"fee": "0"
},
{
"blockNumber": 8554223,
"transactionHash": "0x2a134c9ecacc558184d74b65b60fad388985de493e8542a43c24288f160c04c5",
"nullifierHash": "0x2d5e889a9d3b27af4578e4df48e620de2673e551a444bd005c61692d93fd890c",
"to": "0xb66596d2eab0204494dedaeffe0f86fc74714a0f",
"fee": "0"
},
{
"blockNumber": 8583310,
"transactionHash": "0xf30b1b4064f561b103d2d54f4dfd8fd6c6b27e11d049b57ef128f02a0a77e67a",
"nullifierHash": "0x27b4bc29a518df33ad035be4d580528bbf5447da01c1cd3619fafc0ffa8497bd",
"to": "0xb67af20093837ab2d4fc826a729f30612c47ef2e",
"fee": "0"
},
{
"blockNumber": 8589727,
"transactionHash": "0xaa1da6d30393434b4e7500e946129343676c954526864a890d0a6760c70cd67a",
"nullifierHash": "0x2bccfd39f219e8acade8908e35e48711eef76edf24c221aa850bd1f8b07812f7",
"to": "0x9ff3c1bea9ffb56a78824fe29f457f066257dd58",
"fee": "0"
}
]

View File

@ -1,44 +0,0 @@
[
{
"blockNumber": 6852128,
"transactionHash": "0x1207bb7e71881f0585069c3d5460481bdd0b06b89769ecee6a4ea2721a458c66",
"nullifierHash": "0x1f048c38e166e09fec79454a2b6d4169051ab640e26a28571036bffad9afd0f8",
"to": "0x40c3d1656a26c9266f4a10fed0d87eff79f54e64",
"fee": "88446881087520000"
},
{
"blockNumber": 6895895,
"transactionHash": "0x73bab36402644d065a6ff5bb805416538c302c2460f186302b27bb23f6e0cde1",
"nullifierHash": "0x1e3df17b5fb58f51aa17b247c4ef4e201b8017e4633908f97cb8e885ba447657",
"to": "0x7d98a740517586b3f2b369015585c21d02db117e",
"fee": "0"
},
{
"blockNumber": 6895899,
"transactionHash": "0x6cbec76fc1d86a3d9a5ff9d72dcd09a117fc814d66867d35751c5f4a3bc6136e",
"nullifierHash": "0x01044fd0ffb372aa0ce7b91c0b8d41a68142621f76653d0d466c69fa49f95658",
"to": "0x7d98a740517586b3f2b369015585c21d02db117e",
"fee": "0"
},
{
"blockNumber": 6895903,
"transactionHash": "0xd8693cb67c6f668008b4887736b638903b7fe3debed3f38b900424d1456230d4",
"nullifierHash": "0x1998975018dfb4c3b946685aaf9557faea15152d4cade2255d4127c1aa0a614c",
"to": "0x7d98a740517586b3f2b369015585c21d02db117e",
"fee": "0"
},
{
"blockNumber": 8414382,
"transactionHash": "0x06fc1138e6bc20ec3d41fb35785810fdfd5160ab05b04b38a752158938431c62",
"nullifierHash": "0x22b9b8581092ee88e8cf1d8c214403ba3008383e86f6425f780b562e11f5b236",
"to": "0xa382e9af881ea84010847922cad4307a83fbfcc7",
"fee": "0"
},
{
"blockNumber": 8415797,
"transactionHash": "0x10e1e7d474ec670dc05e040ab1a9349116d3faf2425c68a98eff6ccf8386fb21",
"nullifierHash": "0x00763202337be87dc9e606012b3aa9a2dac9f210f54db613a91c75d89183426d",
"to": "0x68356fd2f92a96040c312fed8ffd4569d1fcde59",
"fee": "0"
}
]

485
cli.js
View File

@ -29,16 +29,11 @@ const tornadoProxyAbi = require('./abis/TornadoProxy.abi.json');
const tornadoInstanceAbi = require('./abis/Instance.abi.json');
const relayerRegistryAbi = require("./abis/RelayerRegistry.abi.json");
const relayerAggregatorAbi = require('./abis/Aggregator.abi');
const tornadoGovernanceAbi = require("./abis/Governance.abi.json");
const stakingRewardsAbi = require("./abis/StakingRewards.abi.json");
const relayerAggregatorAddress = config.deployments[`netId1`].relayerAggregator;
const relayerRegistryAddress = config.deployments[`netId1`].relayerRegistry;
const relayerRegistryDeployedBlockNumber = config.deployments["netId1"].relayerRegistryDeployedBlockNumber;
const relayerSubdomains = Object.values(config.deployments).map(({ ensSubdomainKey }) => ensSubdomainKey);
const tornTokenAddress = "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C";
const governanceAddress = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce";
const stakingRewardsAddress = "0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29";
/** @typedef {import ("web3-eth-contract").Contract} Web3Contract */
/** @typedef {import ("web3-eth").Eth} Web3Eth */
@ -73,9 +68,6 @@ const stakingRewardsAddress = "0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29";
* @property {Web3Contract} [tornadoInstanceContract] Tornado cash instance contract for selected pool (chain/currency/value)
* @property {Web3Contract} [tornadoProxyContract] Tornado cash proxy contract for selected chain
* @property {Web3Contract} [tornadoTokenInstanceContract] Tornado Cash instance contract for selected token pool (for ERC20 token mixing pools, e.g. DAI)
* @property {Web3Contract} [governanceContract] Tornado Cash Governance contract instance to access staking/voting functionality
* @property {Web3Contract} [tornTokenContract] TORN token contract instance to approve/send tokens
* @property {Web3Contract} [stakingRewardsContract] Staking rewards contract to distribute TORN tokens earned by Tornado protocol between TORN stakers
* @property {string} netName Network (chain) human-readable name
* @property {string} netSymbol Network main token symbol (ETH for Ethereum mainnet and so on)
* @property {string} netId Network (chain) ID
@ -101,9 +93,6 @@ const globals =
feeOracle: undefined,
tornadoInstanceContract: undefined,
tornadoProxyContract: undefined,
governanceContract: undefined,
tornTokenContract: undefined,
stakingRewardsContract: undefined,
netName: "Ethereum",
netSymbol: "ETH",
netId: 1
@ -300,7 +289,8 @@ async function generateTransaction(to, encodedData, value = 0, txType = 'other')
/**
* Create deposit object from secret and nullifier
*/
function createDeposit({ nullifier, secret }) {
function createDeposit({ nullifier, secret })
{
let deposit = { nullifier, secret };
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]);
deposit.commitment = pedersenHash(deposit.preimage);
@ -340,7 +330,8 @@ async function backupInvoice({ currency, amount, netId, commitmentNote, invoiceS
* @param currency Сurrency
* @param amount Deposit amount
*/
async function createInvoice({ currency, amount, chainId }) {
async function createInvoice({ currency, amount, chainId })
{
const deposit = createDeposit({
nullifier: rbigint(31),
secret: rbigint(31)
@ -367,7 +358,7 @@ async function createInvoice({ currency, amount, chainId }) {
async function deposit({ currency, amount, commitmentNote }) {
currency = currency.toLowerCase();
const { signerAddress, tornadoProxyAddress, tornadoInstanceAddress, tornadoProxyContract, instanceTokenAddress, tornadoTokenInstanceContract, netSymbol, netId } = globals;
const { signerAddress, tornadoProxyAddress, tornadoInstanceAddress, tornadoProxyContract, instanceTokenAddress, tornadoTokenInstanceContract, netSymbol, netId } = globals;
assert(signerAddress != null, 'Error! Private key not found. Please provide PRIVATE_KEY in .env file or as command argument, if you deposit');
let commitment, noteString;
if (!commitmentNote) {
@ -397,14 +388,14 @@ async function deposit({ currency, amount, commitmentNote }) {
// a token
await printERC20Balance({ address: tornadoInstanceAddress, name: 'Tornado contract' });
await printERC20Balance({ address: signerAddress, name: 'Sender account' });
const decimals = config.deployments[`netId${netId}`]['tokens'][currency].decimals;
const decimals = config.deployments[`netId${netId}`]['tokens'][currency].decimals;
const tokenAmount = fromDecimals({ amount, decimals });
const allowance = await tornadoTokenInstanceContract.methods.allowance(signerAddress, tornadoProxyAddress).call({ from: signerAddress });
console.log('Current allowance is', fromWei(allowance));
if (toBN(allowance).lt(toBN(tokenAmount))) {
console.log('Approving tokens for deposit');
await generateTransaction(tornTokenAddress, tornadoTokenInstanceContract.methods.approve(tornadoProxyAddress, tokenAmount).call(), 0, 'send')
await generateTransaction(instanceTokenAddress, tornadoTokenInstanceContract.methods.approve(tornadoProxyAddress, tokenAmount).encodeABI());
}
console.log('Submitting deposit transaction');
@ -434,7 +425,8 @@ async function deposit({ currency, amount, commitmentNote }) {
* @param {number} amount Tornado instance amount, like 0.1 (ETH or BNB) or 10
* @return {Promise<MerkleProof>} Calculated valid merkle tree (proof)
*/
async function generateMerkleProof(deposit, currency, amount) {
async function generateMerkleProof(deposit, currency, amount)
{
const { web3Instance, multiCallAddress, tornadoInstanceContract } = globals;
// Get all deposit events from smart contract and assemble merkle tree from them
@ -481,7 +473,8 @@ async function generateMerkleProof(deposit, currency, amount) {
* @param {MerkleProof} [args.merkleProof] Valid merkle tree proof
* @returns {Promise<ProofData>} Proof data
*/
async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0, merkleProof }) {
async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0, merkleProof })
{
// Compute merkle proof of our commitment
if (merkleProof === undefined)
merkleProof = await generateMerkleProof(deposit, currency, amount);
@ -532,9 +525,11 @@ async function generateProof({ deposit, currency, amount, recipient, relayerAddr
* @param noteString Note to withdraw
* @param recipient Recipient address
*/
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund, privateKey }) {
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund, privateKey })
{
const { web3Instance, signerAddress, tornadoProxyAddress, requestOptions, feeOracle, tornadoInstanceAddress, tornadoProxyContract, netSymbol, netId, shouldPromptConfirmation } = globals;
if (currency === netSymbol.toLowerCase() && refund && refund !== '0') {
if (currency === netSymbol.toLowerCase() && refund && refund !== '0')
{
throw new Error('The ETH purchase is supposed to be 0 for ETH withdrawals');
}
@ -543,66 +538,75 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
else
refund = toBN(await feeOracle.fetchRefundInETH(currency.toLowerCase()));
if (!web3Utils.isAddress(recipient)) {
if (!web3Utils.isAddress(recipient))
{
throw new Error('Recipient address is not valid');
}
const depositInfo = await loadDepositData({ amount, currency, deposit });
const allDeposits = loadCachedEvents({ type: "deposit", currency, amount });
if ((depositInfo.leafIndex > allDeposits[allDeposits.length - 1].leafIndex - 10)
&& allDeposits.length > 10) {
&& allDeposits.length > 10)
{
console.log("\nWARNING: you're trying to withdraw your deposit too early, there are not enough subsequent deposits to ensure good anonymity level. Read: https://docs.tornado.ws/general/guides/opsec.html");
if (shouldPromptConfirmation)
await promptConfirmation("Continue withdrawal with risks to anonymity? [Y/n]: ")
}
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit });
if (withdrawInfo) {
if(withdrawInfo)
{
console.error("\nError: note has already been withdrawn. Use `compliance` command to check deposit and withdrawal info.\n");
process.exit(1);
}
if (privateKey || globals.privateKey) {
if (privateKey || globals.privateKey)
{
// using private key
// check if the address of recepient matches with the account of provided private key from environment to prevent accidental use of deposit address for withdrawal transaction.
assert
(
recipient.toLowerCase() == signerAddress.toLowerCase(),
'Withdrawal recepient mismatches with the account of provided private key from environment file'
);
(
recipient.toLowerCase() == signerAddress.toLowerCase(),
'Withdrawal recepient mismatches with the account of provided private key from environment file'
);
const checkBalance = await web3Instance.getBalance(signerAddress);
assert
(
checkBalance !== 0,
'You have 0 balance, make sure to fund account by withdrawing from tornado using relayer first'
);
(
checkBalance !== 0,
'You have 0 balance, make sure to fund account by withdrawing from tornado using relayer first'
);
const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund });
console.log('Submitting withdraw transaction');
await generateTransaction
(
tornadoProxyAddress,
tornadoProxyContract.methods.withdraw(tornadoInstanceAddress, proof, ...args).encodeABI(),
toBN(args[5]),
'user_withdrawal'
);
(
tornadoProxyAddress,
tornadoProxyContract.methods.withdraw(tornadoInstanceAddress, proof, ...args).encodeABI(),
toBN(args[5]),
'user_withdrawal'
);
}
else {
else
{
let relayerInfo;
if (relayerURL) {
try {
if (relayerURL)
{
try
{
relayerURL = new URL(relayerURL).origin;
res = await axios.get(relayerURL + '/status', requestOptions);
relayerInfo = res.data;
} catch (err) {
} catch (err)
{
console.error(err);
throw new Error('Cannot get relayer status');
}
}
else {
else
{
const availableRelayers = await getRelayers(netId);
if (availableRelayers.length === 0) throw new Error("Cannot automatically pick a relayer to withdraw your note. Provide relayer manually with `--relayer` cmd option or use private key withdrawal")
if(availableRelayers.length === 0) throw new Error("Cannot automatically pick a relayer to withdraw your note. Provide relayer manually with `--relayer` cmd option or use private key withdrawal")
relayerInfo = pickWeightedRandomRelayer(availableRelayers);
relayerURL = "https://" + relayerInfo.hostname
console.log(`Selected relayer: ${relayerURL}`)
@ -691,10 +695,12 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
}
}
if (currency === netSymbol.toLowerCase()) {
if (currency === netSymbol.toLowerCase())
{
await printETHBalance({ address: recipient, name: 'Recipient' });
}
else {
else
{
await printERC20Balance({ address: recipient, name: 'Recipient' });
}
console.log('Done withdrawal from Tornado Cash');
@ -976,9 +982,9 @@ function getCurrentNetworkSymbol(chainId) {
*/
async function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
let retryAttempt = 0;
while (retryAttempt < attempts) {
while(retryAttempt < attempts){
const result = await globals.web3Instance.getTransactionReceipt(txHash);
if (!result?.blockNumber) {
if(!result?.blockNumber){
retryAttempt++;
await sleep(delay);
continue;
@ -997,31 +1003,31 @@ async function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
* if we can fetch all deposit events from subgraph, we shouldn't require archive node)
* @returns {Promise<string>} Full RPC link
*/
async function selectDefaultRpc(chainId, eventType, isSubgraphAvailable = false) {
async function selectDefaultRpc(chainId, eventType, isSubgraphAvailable = false){
const candidates = config.deployments[`netId${chainId}`].defaultRpcs;
for (const candidate of candidates) {
for(const candidate of candidates){
const localWeb3 = await createWeb3Instance(candidate);
try {
try{
if (!(await localWeb3.net.isListening())) throw new Error('Cannot connect to websocket provider');
if (eventType === "relayer") {
if (eventType === "relayer"){
const relayerRegistryContract = new localWeb3.Contract(relayerRegistryAbi, relayerRegistryAddress);
const registeredRelayers = loadCachedEvents({ type: "relayer" });
const registeredRelayers = loadCachedEvents({type: "relayer"});
if (registeredRelayers.length > 0) {
if(registeredRelayers.length > 0){
const relayerAggregatorContract = new localWeb3.Contract(relayerAggregatorAbi, relayerAggregatorAddress);
const relayerNameHashes = registeredRelayers.map(r => r.ensHash);
// Here it checks RPC returndata size limit: when getRelayer function aggregates onchain data for all relayers, returndata size will be big
await relayerAggregatorContract.methods.relayersData(relayerNameHashes, relayerSubdomains).call();
}
const lastBlock = await localWeb3.getBlockNumber();
const lastCachedBlock = registeredRelayers.length > 0 ? registeredRelayers[registeredRelayers.length - 1].blockNumber : relayerRegistryDeployedBlockNumber;
const fromBlock = isSubgraphAvailable ? lastBlock - 1000 : lastCachedBlock;
const toBlock = isSubgraphAvailable ? lastBlock : lastCachedBlock + 1000;
await relayerRegistryContract.getPastEvents("RelayerRegistered", { fromBlock, toBlock });
const lastBlock = await localWeb3.getBlockNumber();
const lastCachedBlock = registeredRelayers.length > 0 ? registeredRelayers[registeredRelayers.length - 1].blockNumber : relayerRegistryDeployedBlockNumber;
const fromBlock = isSubgraphAvailable ? lastBlock - 1000 : lastCachedBlock;
const toBlock = isSubgraphAvailable ? lastBlock : lastCachedBlock + 1000;
await relayerRegistryContract.getPastEvents("RelayerRegistered", { fromBlock, toBlock });
}
else if (eventType === "withdrawal") {
const oldTransactionHash = config.deployments[`netId${chainId}`].firstDeploymentTransaction;
@ -1042,7 +1048,7 @@ async function selectDefaultRpc(chainId, eventType, isSubgraphAvailable = false)
console.log("Selected RPC: " + candidate);
return candidate;
} catch (e) {
} catch(e){
console.log(e)
}
}
@ -1056,24 +1062,24 @@ async function selectDefaultRpc(chainId, eventType, isSubgraphAvailable = false)
* @param {EventType} eventType Possible type of events in Tornado: deposit, withdrawal or fetch relayers
* @returns {Promise<string>} Full subgraph link
*/
async function selectDefaultGraph(chainId, eventType) {
async function selectDefaultGraph(chainId, eventType){
let candidates = config.deployments[`netId${chainId}`].subgraphs;
if (eventType === "relayer") {
if (eventType === "relayer"){
if (chainId != 1) throw new Error("Relayer subgraph is available only for mainnet");
candidates = config.deployments[`netId${chainId}`].relayerSubgraphs;
query = '{ relayers(first: 10) { address, ensName, ensHash, blockRegistration } }'
}
else if (eventType === "deposit") query = `{ deposits(first: 1, orderBy: timestamp) { blockNumber, index } }`;
else if (eventType === "deposit") query = `{ deposits(first: 1, orderBy: timestamp) { blockNumber, index } }`;
else query = `{ withdrawals(first: 1, orderBy: timestamp) { timestamp } }`
for (const candidate of candidates) {
try {
const response = await axios.post(candidate, { query }, globals.requestOptions);
for(const candidate of candidates) {
try{
const response = await axios.post(candidate, {query}, globals.requestOptions);
const result = response.data.data[`${eventType}s`];
if (!result) throw new Error("Invalid response from subgraph");
console.log(`Selected subgraph for ${eventType}s - ${candidate}`);
return candidate;
} catch (e) {
} catch(e){
console.log(e)
}
}
@ -1087,7 +1093,7 @@ async function selectDefaultGraph(chainId, eventType) {
* @param {string | number} chainId
* @returns {Promise<Array<Object>>} List of available relayers
*/
async function getRelayers(chainId) {
async function getRelayers(chainId){
console.log("Fetching relayers...");
const MIN_STAKE_LISTED_BALANCE = '0X1B1AE4D6E2EF500000'; // 500 TORN
@ -1131,7 +1137,7 @@ async function getRelayers(chainId) {
return validRelayers;
}
async function getAvailableRelayersData(relayers) {
async function getAvailableRelayersData(relayers){
let statuses = [];
for (const relayer of relayers) {
try {
@ -1144,14 +1150,14 @@ async function getRelayers(chainId) {
});
}
} catch (e) {
// console.error(`Failed to fetch status for ${relayer.hostname}:`);
// console.error(`Failed to fetch status for ${relayer.hostname}:`);
}
}
return statuses;
}
const registeredRelayers = await fetchEvents({ type: "relayer" });
const registeredRelayers = await fetchEvents({type: "relayer"});
// Some relayers can be unregistered and then registrered again
const deduplicatedRelayers = registeredRelayers.filter((relayer, index, relayers) => index === relayers.findIndex(r => relayer.ensName === r.ensName));
const validRelayers = await getValidRelayers(deduplicatedRelayers, ensSubdomainKey);
@ -1223,12 +1229,12 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
*/
async function retryPostRequest(url, data, requestOptions, retryAttempts, waitingTimeIncrease = 2000) {
let retryAttempt = 0;
while (1) {
while (1){
await sleep(waitingTimeIncrease * retryAttempt);
try {
try{
return await axios.post(url, data, requestOptions);
} catch (e) {
if (retryAttempt === retryAttempts) throw e;
} catch(e){
if(retryAttempt === retryAttempts) throw e;
retryAttempt++;
}
}
@ -1241,7 +1247,7 @@ async function retryPostRequest(url, data, requestOptions, retryAttempts, waitin
* @param {number | string} amount Tornado instance currency amount
* @returns {string} Path to cache file
*/
function getEventsFilePath(eventType, currency, amount) {
function getEventsFilePath(eventType, currency, amount){
return eventType === "relayer" ? "./cache/relayer/register.json" : `./cache/${globals.netName.toLowerCase()}/${eventType}s_${currency.toLowerCase()}_${amount}.json`;
}
@ -1274,7 +1280,7 @@ function loadCachedEvents({ type, currency, amount }) {
* @returns {Promise<Array<Object>>} All events (cached and newly fetched)
*/
async function fetchEvents({ type, currency, amount }) {
if (currency) currency = currency.toLowerCase();
if(currency) currency = currency.toLowerCase();
if (type === 'withdraw') {
type = 'withdrawal';
}
@ -1286,7 +1292,7 @@ async function fetchEvents({ type, currency, amount }) {
const cachedEvents = loadCachedEvents({ type, currency, amount });
const startBlock = cachedEvents.length ? cachedEvents[cachedEvents.length - 1].blockNumber + 1 : (type === "relayer" ? relayerRegistryDeployedBlockNumber : instanceDeployedBlockNumber);
if (type !== "relayer") {
if(type !== "relayer"){
console.log('Loaded cached', amount, currency.toUpperCase(), type, 'events for', startBlock, 'block');
console.log('Fetching', amount, currency.toUpperCase(), type, 'events for', netName, 'network');
}
@ -1300,9 +1306,9 @@ async function fetchEvents({ type, currency, amount }) {
if (type === 'relayer') fetchedEvents.sort((first, second) => first.blockRegistration - second.blockRegistration);
try {
const cachedEvents = loadCachedEvents({ type, currency, amount });
const cachedEvents = loadCachedEvents({type, currency, amount});
const events = cachedEvents.concat(fetchedEvents);
fs.writeFileSync(getEventsFilePath(type, currency, amount), JSON.stringify(events, null, 2), { flag: 'w+', encoding: 'utf-8' });
fs.writeFileSync(getEventsFilePath(type, currency, amount), JSON.stringify(events, null, 2), 'utf8');
} catch (error) {
console.log(error)
throw new Error('Writing cache file failed:', error);
@ -1320,19 +1326,19 @@ async function fetchEvents({ type, currency, amount }) {
for (let i = startBlock; i < targetBlock; i += chunks) {
let mapFunction;
if (type === "relayer")
mapFunction = ({ blockNumber, returnValues: { relayer, relayerAddress, ensName } }) => ({ blockNumber, ensHash: relayer, ensName, address: relayerAddress });
mapFunction = ({ blockNumber, returnValues: {relayer, relayerAddress, ensName} }) => ({blockNumber, ensHash: relayer, ensName, address: relayerAddress});
else if (type === "deposit")
mapFunction = ({ blockNumber, transactionHash, returnValues: { commitment, leafIndex, timestamp } }) =>
({ blockNumber, transactionHash, commitment, leafIndex: Number(leafIndex), timestamp: Number(timestamp) });
else mapFunction = ({ blockNumber, transactionHash, returnValues: { nullifierHash, to, fee } }) => ({ blockNumber, transactionHash, nullifierHash, to, fee });
({blockNumber, transactionHash, commitment, leafIndex: Number(leafIndex), timestamp: Number(timestamp)});
else mapFunction = ({ blockNumber, transactionHash, returnValues: {nullifierHash, to, fee} }) => ({blockNumber, transactionHash, nullifierHash, to, fee});
const finalBlock = Math.min(i + chunks - 1, targetBlock);
try {
const fetchedEvents = await contract.getPastEvents(eventNameInContract, { fromBlock: i, toBlock: finalBlock });
try{
const fetchedEvents = await contract.getPastEvents(eventNameInContract, {fromBlock: i, toBlock: finalBlock});
console.log('Fetched', type === "relayer" ? type : `${amount} ${currency.toUpperCase()} ${type}`, 'events to block:', finalBlock);
if (fetchedEvents.length === 0) continue;
if(fetchedEvents.length === 0) continue;
await updateCache(fetchedEvents.map(mapFunction));
} catch (err) {
} catch(err){
console.error(`Failed fetching ${type} events from node on block ${i}: `, err)
process.exit(1);
}
@ -1367,7 +1373,7 @@ async function fetchEvents({ type, currency, amount }) {
address, ensName, ensHash, blockRegistration
}
}`;
mapFunction = ({ blockRegistration, address, ensName, ensHash }) => ({ address, ensHash, ensName, blockNumber: Number(blockRegistration) });
mapFunction = ({blockRegistration, address, ensName, ensHash}) => ({address, ensHash, ensName, blockNumber: Number(blockRegistration)});
}
else if (type === 'deposit') {
query = `
@ -1376,19 +1382,19 @@ async function fetchEvents({ type, currency, amount }) {
blockNumber, transactionHash, commitment, index
}
}`;
mapFunction = ({ blockNumber, transactionHash, commitment, index }) => ({ blockNumber: Number(blockNumber), transactionHash, commitment, leafIndex: Number(index) });
mapFunction = ({ blockNumber, transactionHash, commitment, index }) => ({blockNumber: Number(blockNumber), transactionHash, commitment, leafIndex: Number(index)});
}
else if (type === "withdrawal") {
query = `
query = `
query($currency: String, $amount: String, $blockNumber: Int){
withdrawals(orderBy: blockNumber, first: 1000, where: {currency: $currency, amount: $amount, blockNumber${filter}: $blockNumber}) {
blockNumber, transactionHash, nullifier, to, fee
}
}`;
mapFunction = ({ blockNumber, transactionHash, nullifier, to, fee }) => ({ blockNumber: Number(blockNumber), transactionHash, nullifierHash: nullifier, to, fee });
mapFunction = ({ blockNumber, transactionHash, nullifier, to, fee }) => ({blockNumber: Number(blockNumber), transactionHash, nullifierHash: nullifier, to, fee});
}
const querySubgraph = await retryPostRequest(subgraph, { query, variables }, globals.requestOptions, 3);
const querySubgraph = await retryPostRequest(subgraph, {query, variables}, globals.requestOptions, 3);
const queryResult = querySubgraph.data.data[`${type}s`];
return queryResult.map(mapFunction);
} catch (error) {
@ -1400,15 +1406,15 @@ async function fetchEvents({ type, currency, amount }) {
console.log('Querying latest events from subgraph');
const latestBlock = await web3Instance.getBlockNumber();
try {
for (let i = startBlock; i < latestBlock;) {
for (let i = startBlock; i < latestBlock; ) {
let result = await queryFromGraph(i);
if (Object.keys(result).length === 0) break;
const resultBlockNumber = result[result.length - 1].blockNumber;
while (result.length > 0 && result[result.length - 1].blockNumber === resultBlockNumber) result.pop();
while(result.length > 0 && result[result.length - 1].blockNumber === resultBlockNumber) result.pop();
result = result.concat(await queryFromGraph(resultBlockNumber, ""));
await updateCache(result);
i = resultBlockNumber;
console.log('Fetched', type === 'relayer' ? type : `${amount} ${currency.toUpperCase()} ${type}`, 'events to block:', Number(resultBlockNumber));
console.log('Fetched', type === 'relayer' ? type : `${amount} ${currency.toUpperCase()} ${type}`, 'events to block:', Number(resultBlockNumber));
}
} catch {
console.log('Fallback to web3 events');
@ -1423,7 +1429,7 @@ async function fetchEvents({ type, currency, amount }) {
await syncEvents();
}
const updatedEvents = loadCachedEvents({ type, currency, amount })
const updatedEvents = loadCachedEvents({type, currency, amount})
const updatedBlock = updatedEvents[updatedEvents.length - 1].blockNumber;
console.log('Cache updated for Tornado', type === 'relayer' ? type : `${amount} ${currency.toUpperCase()} instance to block`, updatedBlock, 'successfully');
console.log(`Total ${type}s:`, updatedEvents.length - 1);
@ -1434,10 +1440,12 @@ async function fetchEvents({ type, currency, amount }) {
* Parses Tornado Cash note
* @param {string} noteString the note
*/
function parseNote(noteString) {
function parseNote(noteString)
{
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g;
const match = noteRegex.exec(noteString);
if (!match) {
if (!match)
{
throw new Error('The note has invalid format');
}
@ -1480,7 +1488,7 @@ function parseInvoice(invoiceString) {
async function loadDepositData({ amount, currency, deposit }) {
const { web3Instance, tornadoInstanceContract } = globals;
const cachedEvents = await fetchEvents({ type: 'deposit', currency, amount });
const cachedEvents = await fetchEvents({ type: 'deposit', currency, amount});
const depositEvent = cachedEvents.find(event => event.commitment === deposit.commitmentHex);
if (!depositEvent) throw new Error('There is no related deposit, the note is invalid');
@ -1537,119 +1545,13 @@ async function promptConfirmation(query) {
}
}
/**
* Stake TORN tokens in Governance contract to earn rewards from withdrawals and perticate in Governance with voting
* @param {number | string} amount Amount of tokens. Can be fractional, for example stake 100.8432 TORN
*/
async function stakeTorn(amount) {
const { tornTokenContract, governanceContract, signerAddress } = globals;
const tokenAmount = fromDecimals({ amount, decimals: 18 });
const allowance = await tornTokenContract.methods.allowance(signerAddress, governanceAddress).call();
console.log('Current TORN allowance is', fromWei(allowance));
if (toBN(allowance).lt(toBN(tokenAmount))) {
console.log('Approving tokens for stake');
await generateTransaction(tornTokenAddress, tornTokenContract.methods.approve(governanceAddress, tokenAmount).encodeABI(), 0, 'send');
}
console.log("Sending stake transaction...");
await generateTransaction(governanceAddress, governanceContract.methods.lockWithApproval(tokenAmount).encodeABI(), 0, 'send');
const stakedAmount = await governanceContract.methods.lockedBalance(signerAddress).call();
console.log("Staked successfull: your current stake balance is", fromWei(stakedAmount), "TORN");
}
/**
* Withdraw TORN tokens from Governance staking (without rewards)
* @param {number | string} amount Amount of TORn tokens to withdraw, can be fractional
*/
async function unstakeTorn(amount) {
const { governanceContract, signerAddress } = globals;
const tokenAmount = fromDecimals({ amount, decimals: 18 });
const stakedAmount = await governanceContract.methods.lockedBalance(signerAddress).call();
if (toBN(stakedAmount).lt(toBN(tokenAmount))) throw new Error(`Not enough tokens in stake. You have ${fromWei(stakedAmount)} tokens, but you're trying to withdraw ${amount}.`);
console.log("Sending unstake transaction...");
await generateTransaction(governanceAddress, governanceContract.methods.unlock(tokenAmount).encodeABI(), 0, 'send');
}
/**
* Delegate voting power in Tornado Cash governance to another address: delegatee can vote and create proposals on your behalf
* @param {string} address Delegatee address
*/
async function delegate(address) {
if (!web3Utils.isAddress(address)) throw new Error("Cannot delegate: invalid delegatee address provided");
await generateTransaction(governanceAddress, globals.governanceContract.methods.delegate(address).encodeABI(), 0, 'send');
}
/**
* Remove Tornado Cash governance delegation. After doing it, nobody can vote or create proposals on your behalf
*/
async function undelegate() {
const { governanceContract, signerAddress } = globals;
const currentDelegatee = await governanceContract.methods.delegatedTo(signerAddress).call();
if (currentDelegatee.toLowerCase() === "0x0000000000000000000000000000000000000000") {
console.log("No actual delegatee: already undelegated");
return;
}
await generateTransaction(governanceAddress, governanceContract.methods.undelegate().encodeABI(), 0, 'send');
}
/**
* Loads actual data from contract and prints information about Tornado Cash staking for specified address: stake amount, unclaimed staking rewards and voting power delegation
* @param {string} address
*/
async function printStakeInfo(address) {
if (!web3Utils.isAddress(address)) throw new Error("Cannot check stake info: invalid address provided");
const { governanceContract, stakingRewardsContract } = globals;
const stakedAmount = await governanceContract.methods.lockedBalance(address).call();
const rewardsAmount = await stakingRewardsContract.methods.checkReward(address).call();
const delegatee = await governanceContract.methods.delegatedTo(address).call();
console.log('\n====================Staking info====================');
console.log('Account :', address);
console.log('Staked balance :', `${fromWei(stakedAmount)} TORN`);
console.log('Unclaimed rewards :', `${fromWei(rewardsAmount)} TORN`);
console.log('Delegation status :', delegatee.toLowerCase() === "0x0000000000000000000000000000000000000000" ? "not delegated" : `voting power delegated to ${delegatee}`);
console.log('====================================================', '\n');
}
/**
* Vote in governance from signer address with all staked tokens for or against specified proposal by ID
* @param {string | number} proposalId Proposal ID
* @param {string} decision For or against proposal ("yes" is for, "no" is against, true is for, false is against)
*/
async function vote(proposalId, decision){
const { governanceContract, signerAddress } = globals;
const signerStakedBalance = await governanceContract.methods.lockedBalance(signerAddress).call();
if (signerStakedBalance.lte(0)) throw new Error("You have no staked balance, therefore you cannot vote.");
let support;
if (decision === "yes" || decision === "for") support = true;
else if (decision === "no" || decision === "against") support = false;
else throw new Error("Invalid user decision: cannot vote for or against proposal");
await generateTransaction(governanceAddress, governanceContract.methods.castVote(Number(proposalId), support).encodeABI(), 0, 'send');
}
/**
* Initiate transaction to claim staking rewards from Tornado Cash staking
*/
async function claimStakingRewards(){
await generateTransaction(stakingRewardsAddress, globals.stakingRewardsContract.methods.getReward().encodeABI(), 0, 'send');
}
/**
* Create web3 eth instance with provider using RPC link
* @param {string} rpc Full RPC link
* @returns {Promise<Web3Eth>} Initialized Web3 instance object
*/
async function createWeb3Instance(rpc) {
const { torPort } = globals;
async function createWeb3Instance(rpc){
const { torPort }= globals;
let web3;
if (torPort && rpc.startsWith('https')) {
@ -1670,26 +1572,12 @@ async function createWeb3Instance(rpc) {
web3 = new Web3(new Web3.providers.WebsocketProvider(rpc, web3Options), null, { transactionConfirmationBlocks: 1 });
} else {
console.log(`Connecting to remote node ${rpc}`);
web3 = new Web3(new Web3.providers.HttpProvider(rpc, { timeout: 10000, keepAlive: true }), null, { transactionConfirmationBlocks: 1 });
web3 = new Web3(new Web3.providers.HttpProvider(rpc, {timeout: 10000, keepAlive: true}), null, { transactionConfirmationBlocks: 1 });
}
return web3.eth;
}
/**
* Initialize TORN contracts and then call initNetwork
* @param {Object} args Arguments to pass to initNetwork function
*/
async function initTorn(args) {
initPreferences(args);
await initNetwork({ ...args, chainId: 1, onlyRpc: true });
const { web3Instance } = globals;
globals.governanceContract = new web3Instance.Contract(tornadoGovernanceAbi, governanceAddress);
globals.tornTokenContract = new web3Instance.Contract(erc20Abi, tornTokenAddress);
globals.stakingRewardsContract = new web3Instance.Contract(stakingRewardsAbi, stakingRewardsAddress);
}
/**
* Init web3 network from user parameters for all program
* @param {Object} args Arguments
@ -1701,7 +1589,7 @@ async function initTorn(args) {
* @param {EventType} [args.eventType] Applicable event type for user actions
* @param {string} [relayer] User-provided relayer link
*/
async function initNetwork({ rpc, chainId, privateKey, torPort, onlyRpc, eventType, relayer }) {
async function initNetwork({rpc, chainId, privateKey, torPort, onlyRpc, eventType, relayer}) {
if (torPort) {
globals.torPort = torPort;
@ -1714,7 +1602,7 @@ async function initNetwork({ rpc, chainId, privateKey, torPort, onlyRpc, eventTy
if (chainId && !rpc) {
const subgraphUrl = onlyRpc ? null : await selectDefaultGraph(chainId, eventType)
rpc = await selectDefaultRpc(chainId, eventType, !!subgraphUrl)
rpc = await selectDefaultRpc(chainId, eventType, subgraphUrl)
}
globals.web3Instance = await createWeb3Instance(rpc)
@ -1725,7 +1613,7 @@ async function initNetwork({ rpc, chainId, privateKey, torPort, onlyRpc, eventTy
globals.feeOracle = Number(globals.netId) === 1 ? new TornadoFeeOracleV4(globals.netId, rpc) : new TornadoFeeOracleV5(globals.netId, rpc);
// Create web3 instance to fetch relayer on mainnet, if user want to get relayers list or withdraw, but he didn't provide relayer link
if (!relayer && !privateKey && (eventType === 'relayer' || eventType === 'withdrawal')) {
if (!relayer && !privateKey && (eventType === 'relayer' || eventType === 'withdrawal')){
if (globals.netId === 1) globals.relayerWeb3Instance = globals.web3Instance;
else {
const subgraphUrl = onlyRpc ? null : await selectDefaultGraph(1, 'relayer');
@ -1769,7 +1657,7 @@ async function initNetwork({ rpc, chainId, privateKey, torPort, onlyRpc, eventTy
* @param {boolean} [userPreferences.nonconfirmation] Don't ask for confirmation for crucial actions
* @param {boolean} [userPreferences.localMode] Don't submit signed transactions to blockchain (remote nodes)
*/
function initPreferences({ nonconfirmation, localMode }) {
function initPreferences({nonconfirmation, localMode}){
if (nonconfirmation) {
console.log("Non-confirmation mode detected: program won't ask confirmation for crucial actions")
globals.shouldPromptConfirmation = false;
@ -1783,11 +1671,11 @@ function initPreferences({ nonconfirmation, localMode }) {
/**
* Init web3, all Tornado contracts, and snark
*/
async function init({ rpc, chainId, currency = 'dai', amount = '100', privateKey, torPort, onlyRpc, nonconfirmation, localMode, eventType, relayer }) {
async function init({ rpc, chainId, currency = 'dai', amount = '100', privateKey, torPort, onlyRpc, nonconfirmation, localMode, eventType, relayer}) {
currency = currency.toLowerCase()
initPreferences({ nonconfirmation, localMode });
await initNetwork({ rpc, chainId, privateKey, torPort, onlyRpc, eventType, relayer });
initPreferences({nonconfirmation, localMode});
await initNetwork({rpc, chainId, privateKey, torPort, onlyRpc, eventType, relayer});
const { netId, web3Instance } = globals;
// console.log(netId, chainId);
@ -1841,7 +1729,7 @@ async function main() {
.action(async (noteString, recipient, refund) => {
const { currency, amount, netId, deposit } = parseNote(noteString);
await init({ ...program, chainId: netId, currency, amount, eventType: 'withdrawal' });
await init({...program, chainId: netId, currency, amount, eventType: 'withdrawal'});
await withdraw({
deposit,
@ -1853,94 +1741,6 @@ async function main() {
relayerURL: program.relayer
});
});
program
.command('compliance <note>')
.description(
'Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.'
)
.action(async (noteString) => {
const { currency, amount, netId, deposit } = parseNote(noteString);
await init({ ...program, chainId: netId, currency, amount, eventType: 'withdrawal', relayer: "dummy" });
const depositInfo = await loadDepositData({ amount, currency, deposit });
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit });
const depositDate = new Date(depositInfo.timestamp * 1000);
console.log('\n=============Deposit=================');
console.log('Deposit :', amount, currency.toUpperCase());
console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString());
console.log('From :', `https://${getExplorerLink()}/address/${depositInfo.from}`);
console.log('Transaction :', `https://${getExplorerLink()}/tx/${depositInfo.txHash}`);
console.log('Commitment :', depositInfo.commitment);
console.log('Spent :', depositInfo.isSpent);
console.log('=====================================', '\n');
if (!depositInfo.isSpent) {
console.log('The note was not spent!');
return;
}
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000);
console.log('\n=============Withdrawal==============');
console.log('Withdrawal :', withdrawInfo.amount, currency);
console.log('Relayer Fee :', withdrawInfo.fee, currency);
console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString());
console.log('To :', `https://${getExplorerLink()}/address/${withdrawInfo.to}`);
console.log('Transaction :', `https://${getExplorerLink()}/tx/${withdrawInfo.txHash}`);
console.log('Nullifier :', withdrawInfo.nullifier);
console.log('=====================================', '\n');
});
program
.command("stake <amount>")
.description("Stake TORN tokens in Governance contract to earn rewards and vote. Requires private key")
.action(async (amount) => {
await initTorn(program);
await stakeTorn(Number(amount));
})
program
.command("unstake <amount>")
.description("Unstake TORN tokens (withdraw from Governance staking). Requires private key")
.action(async (amount) => {
await initTorn(program);
await unstakeTorn(amount);
})
program
.command("checkStake <address>")
.description("Check Tornado Cash staking information about provided address: stake amount, unclaimed staking rewards and voting power delegation")
.action(async (address) => {
await initTorn(program);
await printStakeInfo(address);
});
program
.command("claim")
.description("Claim staking rewards. Requires private key")
.action(async () => {
await initTorn(program);
await claimStakingRewards();
})
program
.command("vote <decision> <proposal_id>")
.description("Vote for or against Tornado Cash governance proposal with all staked tokens. Decision can be `yes/for` or `no/against`. To change your vote, just use this function again with different decision")
.action(async (decision, proposalId) => {
await initTorn(program);
await vote(proposalId, decision.toLowerCase())
})
program
.command("delegate <address>")
.description("Delegate voting power to another address. Requires private key")
.action(async (address) => {
await initTorn(program);
await delegate(address);
});
program
.command("undelegate")
.description("Remove current delegatee (nobody can vote or create proposals on your behalf after it). Requires private key")
.action(async () => {
await initTorn(program);
await undelegate();
});
program
.command('createNote <currency> <amount> <chainId>')
.description(
@ -1970,15 +1770,15 @@ async function main() {
.command("listRelayers <chain_id>")
.description("Check available relayers on selected chain. If you wantue non-default RPC, you should provide ONLY mainnet RPC urls")
.action(async (chainId) => {
await initNetwork({ ...program, chainId: 1, eventType: 'relayer' })
await initNetwork({...program, chainId: 1, eventType: 'relayer'})
const availableRelayers = await getRelayers(chainId);
console.log("There are " + availableRelayers.length + " available relayers")
for (const relayer of availableRelayers) {
for(const relayer of availableRelayers){
console.log({
'hostname': 'https://' + relayer.hostname + '/',
'ensName': relayer.ensName,
'stakeBalance': Number(web3Utils.fromWei(relayer.stakeBalance, 'ether')).toFixed(2) + " TORN",
'stakeBalance': Number(web3Utils.fromWei(relayer.stakeBalance, 'ether')).toFixed(2)+" TORN",
'tornadoServiceFee': relayer.tornadoServiceFee + "%"
});
}
@ -2014,13 +1814,52 @@ async function main() {
await initNetwork(program);
await submitTransaction(signedTX);
});
program
.command('compliance <note>')
.description(
'Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.'
)
.action(async (noteString) => {
const { currency, amount, netId, deposit } = parseNote(noteString);
await init({ ...program, chainId: netId, currency, amount, eventType: 'withdrawal', relayer: "dummy"});
const depositInfo = await loadDepositData({ amount, currency, deposit });
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit });
const depositDate = new Date(depositInfo.timestamp * 1000);
console.log('\n=============Deposit=================');
console.log('Deposit :', amount, currency.toUpperCase());
console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString());
console.log('From :', `https://${getExplorerLink()}/address/${depositInfo.from}`);
console.log('Transaction :', `https://${getExplorerLink()}/tx/${depositInfo.txHash}`);
console.log('Commitment :', depositInfo.commitment);
console.log('Spent :', depositInfo.isSpent);
console.log('=====================================', '\n');
if (!depositInfo.isSpent) {
console.log('The note was not spent!');
return;
}
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000);
console.log('\n=============Withdrawal==============');
console.log('Withdrawal :', withdrawInfo.amount, currency);
console.log('Relayer Fee :', withdrawInfo.fee, currency);
console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString());
console.log('To :', `https://${getExplorerLink()}/address/${withdrawInfo.to}`);
console.log('Transaction :', `https://${getExplorerLink()}/tx/${withdrawInfo.txHash}`);
console.log('Nullifier :', withdrawInfo.nullifier);
console.log('=====================================', '\n');
});
program
.command('syncEvents <type> <currency> <amount> [chain_id]')
.description('Sync the local cache file of deposit / withdrawal events for specific currency.')
.action(async (type, currency, amount, chainId) => {
console.log('Starting event sync command');
await init({ ...program, currency, amount, chainId });
await init({ ...program, currency, amount, chainId});
if (type === "withdraw") type === "withdrawal";
const cachedEvents = await fetchEvents({ type, currency, amount });
@ -2060,7 +1899,7 @@ async function main() {
netId = parse.netId;
console.log('\n=============Note=================');
console.log('Network:', getCurrentNetworkName());
console.log('Network:', getCurrentNetworkName());
console.log('Denomination:', parse.amount, parse.currency.toUpperCase());
console.log('Commitment: ', parse.deposit.commitmentHex);
console.log('Nullifier Hash: ', parse.deposit.nullifierHex);

BIN
output/tornado-cli.exe Normal file

Binary file not shown.

Binary file not shown.