cli.js 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533
  1. #!/usr/bin/env node
  2. // Works both in browser and node.js
  3. require('dotenv').config();
  4. const fs = require('fs');
  5. const axios = require('axios');
  6. const assert = require('assert');
  7. const snarkjs = require('snarkjs');
  8. const crypto = require('crypto');
  9. const circomlib = require('circomlib');
  10. const bigInt = snarkjs.bigInt;
  11. const merkleTree = require('fixed-merkle-tree');
  12. const Web3 = require('web3');
  13. const Web3HttpProvider = require('web3-providers-http');
  14. const buildGroth16 = require('websnark/src/groth16');
  15. const websnarkUtils = require('websnark/src/utils');
  16. const { toWei, fromWei, toBN, BN } = require('web3-utils');
  17. const BigNumber = require('bignumber.js');
  18. const config = require('./config');
  19. const program = require('commander');
  20. const { GasPriceOracle } = require('gas-price-oracle');
  21. const SocksProxyAgent = require('socks-proxy-agent');
  22. const is_ip_private = require('private-ip');
  23. let web3, torPort, tornado, tornadoContract, tornadoInstance, circuit, proving_key, groth16, erc20, senderAccount, netId, netName, netSymbol, doNotSubmitTx, multiCall, privateRpc, subgraph;
  24. let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY;
  25. /** Whether we are in a browser or node.js */
  26. const inBrowser = typeof window !== 'undefined';
  27. let isTestRPC = false;
  28. /** Generate random number of specified byte length */
  29. const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes));
  30. /** Compute pedersen hash */
  31. const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0];
  32. /** BigNumber to hex string of specified length */
  33. function toHex(number, length = 32) {
  34. const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16);
  35. return '0x' + str.padStart(length * 2, '0');
  36. }
  37. /** Remove Decimal without rounding with BigNumber */
  38. function rmDecimalBN(bigNum, decimals = 6) {
  39. return new BigNumber(bigNum).times(BigNumber(10).pow(decimals)).integerValue(BigNumber.ROUND_DOWN).div(BigNumber(10).pow(decimals)).toNumber();
  40. }
  41. /** Use MultiCall Contract */
  42. async function useMultiCall(queryArray) {
  43. const multiCallABI = require('./build/contracts/Multicall.abi.json');
  44. const multiCallContract = new web3.eth.Contract(multiCallABI, multiCall);
  45. const { returnData } = await multiCallContract.methods.aggregate(queryArray).call();
  46. return returnData;
  47. }
  48. /** Display ETH account balance */
  49. async function printETHBalance({ address, name }) {
  50. const checkBalance = new BigNumber(await web3.eth.getBalance(address)).div(BigNumber(10).pow(18));
  51. console.log(`${name} balance is`, rmDecimalBN(checkBalance), `${netSymbol}`);
  52. }
  53. /** Display ERC20 account balance */
  54. async function printERC20Balance({ address, name, tokenAddress }) {
  55. let tokenDecimals, tokenBalance, tokenName, tokenSymbol;
  56. const erc20ContractJson = require('./build/contracts/ERC20Mock.json');
  57. erc20 = tokenAddress ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : erc20;
  58. if (!isTestRPC && !multiCall) {
  59. const tokenCall = await useMultiCall([[tokenAddress, erc20.methods.balanceOf(address).encodeABI()], [tokenAddress, erc20.methods.decimals().encodeABI()], [tokenAddress, erc20.methods.name().encodeABI()], [tokenAddress, erc20.methods.symbol().encodeABI()]]);
  60. tokenDecimals = parseInt(tokenCall[1]);
  61. tokenBalance = new BigNumber(tokenCall[0]).div(BigNumber(10).pow(tokenDecimals));
  62. tokenName = web3.eth.abi.decodeParameter('string', tokenCall[2]);
  63. tokenSymbol = web3.eth.abi.decodeParameter('string', tokenCall[3]);
  64. } else {
  65. tokenDecimals = await erc20.methods.decimals().call();
  66. tokenBalance = new BigNumber(await erc20.methods.balanceOf(address).call()).div(BigNumber(10).pow(tokenDecimals));
  67. tokenName = await erc20.methods.name().call();
  68. tokenSymbol = await erc20.methods.symbol().call();
  69. }
  70. console.log(`${name}`, tokenName, `Balance is`, rmDecimalBN(tokenBalance), tokenSymbol);
  71. }
  72. async function submitTransaction(signedTX) {
  73. console.log("Submitting transaction to the remote node");
  74. await web3.eth.sendSignedTransaction(signedTX)
  75. .on('transactionHash', function (txHash) {
  76. console.log(`View transaction on block explorer https://${getExplorerLink()}/tx/${txHash}`);
  77. })
  78. .on('error', function (e) {
  79. console.error('on transactionHash error', e.message);
  80. });
  81. }
  82. async function generateTransaction(to, encodedData, value = 0) {
  83. const nonce = await web3.eth.getTransactionCount(senderAccount);
  84. let gasPrice = await fetchGasPrice();
  85. let gasLimit;
  86. async function estimateGas() {
  87. const fetchedGas = await web3.eth.estimateGas({
  88. from : senderAccount,
  89. to : to,
  90. value : value,
  91. nonce : nonce,
  92. data : encodedData
  93. });
  94. const bumped = Math.floor(fetchedGas * 1.3);
  95. return web3.utils.toHex(bumped);
  96. }
  97. if (encodedData) {
  98. gasLimit = await estimateGas();
  99. } else {
  100. gasLimit = web3.utils.toHex(21000);
  101. }
  102. function txoptions() {
  103. // Generate EIP-1559 transaction
  104. if (netId == 1) {
  105. return {
  106. to : to,
  107. value : value,
  108. nonce : nonce,
  109. maxFeePerGas : gasPrice,
  110. maxPriorityFeePerGas : web3.utils.toHex(web3.utils.toWei('3', 'gwei')),
  111. gas : gasLimit,
  112. data : encodedData
  113. }
  114. } else if (netId == 5 || netId == 137 || netId == 43114) {
  115. return {
  116. to : to,
  117. value : value,
  118. nonce : nonce,
  119. maxFeePerGas : gasPrice,
  120. maxPriorityFeePerGas : gasPrice,
  121. gas : gasLimit,
  122. data : encodedData
  123. }
  124. } else {
  125. return {
  126. to : to,
  127. value : value,
  128. nonce : nonce,
  129. gasPrice : gasPrice,
  130. gas : gasLimit,
  131. data : encodedData
  132. }
  133. }
  134. }
  135. const tx = txoptions();
  136. const signed = await web3.eth.accounts.signTransaction(tx, PRIVATE_KEY);
  137. if (!doNotSubmitTx) {
  138. await submitTransaction(signed.rawTransaction);
  139. } else {
  140. console.log('\n=============Raw TX=================', '\n');
  141. console.log(`Please submit this raw tx to https://${getExplorerLink()}/pushTx, or otherwise broadcast with node cli.js broadcast command.`, `\n`);
  142. console.log(signed.rawTransaction, `\n`);
  143. console.log('=====================================', '\n');
  144. }
  145. }
  146. /**
  147. * Create deposit object from secret and nullifier
  148. */
  149. function createDeposit({ nullifier, secret }) {
  150. const deposit = { nullifier, secret };
  151. deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]);
  152. deposit.commitment = pedersenHash(deposit.preimage);
  153. deposit.commitmentHex = toHex(deposit.commitment);
  154. deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31));
  155. deposit.nullifierHex = toHex(deposit.nullifierHash);
  156. return deposit;
  157. }
  158. async function backupNote({ currency, amount, netId, note, noteString }) {
  159. try {
  160. await fs.writeFileSync(`./backup-tornado-${currency}-${amount}-${netId}-${note.slice(0, 10)}.txt`, noteString, 'utf8');
  161. console.log("Backed up deposit note as", `./backup-tornado-${currency}-${amount}-${netId}-${note.slice(0, 10)}.txt`);
  162. } catch (e) {
  163. throw new Error('Writing backup note failed:', e);
  164. }
  165. }
  166. async function backupInvoice({ currency, amount, netId, commitmentNote, invoiceString }) {
  167. try {
  168. await fs.writeFileSync(`./backup-tornadoInvoice-${currency}-${amount}-${netId}-${commitmentNote.slice(0, 10)}.txt`, invoiceString, 'utf8');
  169. console.log("Backed up invoice as", `./backup-tornadoInvoice-${currency}-${amount}-${netId}-${commitmentNote.slice(0, 10)}.txt`)
  170. } catch (e) {
  171. throw new Error('Writing backup invoice failed:', e)
  172. }
  173. }
  174. /**
  175. * create a deposit invoice.
  176. * @param currency Сurrency
  177. * @param amount Deposit amount
  178. */
  179. async function createInvoice({ currency, amount, chainId }) {
  180. const deposit = createDeposit({
  181. nullifier: rbigint(31),
  182. secret: rbigint(31)
  183. });
  184. const note = toHex(deposit.preimage, 62);
  185. const noteString = `tornado-${currency}-${amount}-${chainId}-${note}`;
  186. console.log(`Your note: ${noteString}`);
  187. const commitmentNote = toHex(deposit.commitment);
  188. const invoiceString = `tornadoInvoice-${currency}-${amount}-${chainId}-${commitmentNote}`;
  189. console.log(`Your invoice for deposit: ${invoiceString}`);
  190. await backupNote({ currency, amount, netId: chainId, note, noteString });
  191. await backupInvoice({ currency, amount, netId: chainId, commitmentNote, invoiceString });
  192. return (noteString, invoiceString);
  193. }
  194. /**
  195. * Make a deposit
  196. * @param currency Сurrency
  197. * @param amount Deposit amount
  198. */
  199. async function deposit({ currency, amount, commitmentNote }) {
  200. assert(senderAccount != null, 'Error! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you deposit');
  201. let commitment, noteString;
  202. if (!commitmentNote) {
  203. console.log("Creating new random deposit note");
  204. const deposit = createDeposit({
  205. nullifier: rbigint(31),
  206. secret: rbigint(31)
  207. });
  208. const note = toHex(deposit.preimage, 62);
  209. noteString = `tornado-${currency}-${amount}-${netId}-${note}`;
  210. console.log(`Your note: ${noteString}`);
  211. await backupNote({ currency, amount, netId, note, noteString });
  212. commitment = toHex(deposit.commitment);
  213. } else {
  214. console.log("Using supplied invoice for deposit");
  215. commitment = toHex(commitmentNote);
  216. }
  217. if (currency === netSymbol.toLowerCase()) {
  218. await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract' });
  219. await printETHBalance({ address: senderAccount, name: 'Sender account' });
  220. const value = isTestRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 });
  221. console.log('Submitting deposit transaction');
  222. await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI(), value);
  223. await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract' });
  224. await printETHBalance({ address: senderAccount, name: 'Sender account' });
  225. } else {
  226. // a token
  227. await printERC20Balance({ address: tornadoContract._address, name: 'Tornado contract' });
  228. await printERC20Balance({ address: senderAccount, name: 'Sender account' });
  229. const decimals = isTestRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals;
  230. const tokenAmount = isTestRPC ? TOKEN_AMOUNT : fromDecimals({ amount, decimals });
  231. if (isTestRPC) {
  232. console.log('Minting some test tokens to deposit');
  233. await generateTransaction(erc20Address, erc20.methods.mint(senderAccount, tokenAmount).encodeABI());
  234. }
  235. const allowance = await erc20.methods.allowance(senderAccount, tornado._address).call({ from: senderAccount });
  236. console.log('Current allowance is', fromWei(allowance));
  237. if (toBN(allowance).lt(toBN(tokenAmount))) {
  238. console.log('Approving tokens for deposit');
  239. await generateTransaction(erc20Address, erc20.methods.approve(tornado._address, tokenAmount).encodeABI());
  240. }
  241. console.log('Submitting deposit transaction');
  242. await generateTransaction(contractAddress, tornado.methods.deposit(tornadoInstance, commitment, []).encodeABI());
  243. await printERC20Balance({ address: tornadoContract._address, name: 'Tornado contract' });
  244. await printERC20Balance({ address: senderAccount, name: 'Sender account' });
  245. }
  246. if(!commitmentNote) {
  247. return noteString;
  248. }
  249. }
  250. /**
  251. * Generate merkle tree for a deposit.
  252. * Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
  253. * in it and generates merkle proof
  254. * @param deposit Deposit object
  255. */
  256. async function generateMerkleProof(deposit, currency, amount) {
  257. let leafIndex = -1;
  258. // Get all deposit events from smart contract and assemble merkle tree from them
  259. const cachedEvents = await fetchEvents({ type: 'deposit', currency, amount });
  260. const leaves = cachedEvents
  261. .sort((a, b) => a.leafIndex - b.leafIndex) // Sort events in chronological order
  262. .map((e) => {
  263. const index = toBN(e.leafIndex).toNumber();
  264. if (toBN(e.commitment).eq(toBN(deposit.commitmentHex))) {
  265. leafIndex = index;
  266. }
  267. return toBN(e.commitment).toString(10);
  268. });
  269. const tree = new merkleTree(MERKLE_TREE_HEIGHT, leaves);
  270. // Validate that our data is correct
  271. const root = tree.root();
  272. let isValidRoot, isSpent;
  273. if (!isTestRPC && !multiCall) {
  274. const callContract = await useMultiCall([[tornadoContract._address, tornadoContract.methods.isKnownRoot(toHex(root)).encodeABI()], [tornadoContract._address, tornadoContract.methods.isSpent(toHex(deposit.nullifierHash)).encodeABI()]])
  275. isValidRoot = web3.eth.abi.decodeParameter('bool', callContract[0]);
  276. isSpent = web3.eth.abi.decodeParameter('bool', callContract[1]);
  277. } else {
  278. isValidRoot = await tornadoContract.methods.isKnownRoot(toHex(root)).call();
  279. isSpent = await tornadoContract.methods.isSpent(toHex(deposit.nullifierHash)).call();
  280. }
  281. assert(isValidRoot === true, 'Merkle tree is corrupted');
  282. assert(isSpent === false, 'The note is already spent');
  283. assert(leafIndex >= 0, 'The deposit is not found in the tree');
  284. // Compute merkle proof of our commitment
  285. const { pathElements, pathIndices } = tree.path(leafIndex);
  286. return { root, pathElements, pathIndices };
  287. }
  288. /**
  289. * Generate SNARK proof for withdrawal
  290. * @param deposit Deposit object
  291. * @param recipient Funds recipient
  292. * @param relayer Relayer address
  293. * @param fee Relayer fee
  294. * @param refund Receive ether for exchanged tokens
  295. */
  296. async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0 }) {
  297. // Compute merkle proof of our commitment
  298. const { root, pathElements, pathIndices } = await generateMerkleProof(deposit, currency, amount);
  299. // Prepare circuit input
  300. const input = {
  301. // Public snark inputs
  302. root: root,
  303. nullifierHash: deposit.nullifierHash,
  304. recipient: bigInt(recipient),
  305. relayer: bigInt(relayerAddress),
  306. fee: bigInt(fee),
  307. refund: bigInt(refund),
  308. // Private snark inputs
  309. nullifier: deposit.nullifier,
  310. secret: deposit.secret,
  311. pathElements: pathElements,
  312. pathIndices: pathIndices
  313. }
  314. console.log('Generating SNARK proof');
  315. console.time('Proof time');
  316. const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, proving_key);
  317. const { proof } = websnarkUtils.toSolidityInput(proofData);
  318. console.timeEnd('Proof time');
  319. const args = [
  320. toHex(input.root),
  321. toHex(input.nullifierHash),
  322. toHex(input.recipient, 20),
  323. toHex(input.relayer, 20),
  324. toHex(input.fee),
  325. toHex(input.refund)
  326. ];
  327. return { proof, args };
  328. }
  329. /**
  330. * Do an ETH withdrawal
  331. * @param noteString Note to withdraw
  332. * @param recipient Recipient address
  333. */
  334. async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund = '0' }) {
  335. let options = {};
  336. if (currency === netSymbol.toLowerCase() && refund !== '0') {
  337. throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals');
  338. }
  339. refund = toWei(refund);
  340. if (relayerURL) {
  341. if (relayerURL.endsWith('.eth')) {
  342. throw new Error('ENS name resolving is not supported. Please provide DNS name of the relayer. See instuctions in README.md');
  343. }
  344. if (torPort) {
  345. options = { httpsAgent: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0' } }
  346. }
  347. const relayerStatus = await axios.get(relayerURL + '/status', options);
  348. const { rewardAccount, netId, ethPrices, tornadoServiceFee } = relayerStatus.data
  349. assert(netId === (await web3.eth.net.getId()) || netId === '*', 'This relay is for different network');
  350. console.log('Relay address:', rewardAccount);
  351. const gasPrice = await fetchGasPrice();
  352. const decimals = isTestRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
  353. const fee = calculateFee({
  354. currency,
  355. gasPrice,
  356. amount,
  357. refund,
  358. ethPrices,
  359. relayerServiceFee: tornadoServiceFee,
  360. decimals
  361. });
  362. if (fee.gt(fromDecimals({ amount, decimals }))) {
  363. throw new Error('Too high refund');
  364. };
  365. const { proof, args } = await generateProof({ deposit, currency, amount, recipient, relayerAddress: rewardAccount, fee, refund });
  366. console.log('Sending withdraw transaction through relay');
  367. try {
  368. const response = await axios.post(relayerURL + '/v1/tornadoWithdraw', {
  369. contract: tornadoInstance,
  370. proof,
  371. args
  372. }, options)
  373. const { id } = response.data;
  374. const result = await getStatus(id, relayerURL, options);
  375. console.log('STATUS', result);
  376. } catch (e) {
  377. if (e.response) {
  378. console.error(e.response.data.error);
  379. } else {
  380. console.error(e.message);
  381. }
  382. }
  383. } else {
  384. // using private key
  385. // 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.
  386. assert(recipient.toLowerCase() == senderAccount.toLowerCase(), 'Withdrawal recepient mismatches with the account of provided private key from environment file');
  387. const checkBalance = await web3.eth.getBalance(senderAccount);
  388. assert(checkBalance !== 0, 'You have 0 balance, make sure to fund account by withdrawing from tornado using relayer first');
  389. const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund });
  390. console.log('Submitting withdraw transaction');
  391. await generateTransaction(contractAddress, tornado.methods.withdraw(tornadoInstance, proof, ...args).encodeABI());
  392. }
  393. if (currency === netSymbol.toLowerCase()) {
  394. await printETHBalance({ address: recipient, name: 'Recipient' });
  395. } else {
  396. await printERC20Balance({ address: recipient, name: 'Recipient' });
  397. }
  398. console.log('Done withdrawal from Tornado Cash');
  399. }
  400. /**
  401. * Do an ETH / ERC20 send
  402. * @param address Recepient address
  403. * @param amount Amount to send
  404. * @param tokenAddress ERC20 token address
  405. */
  406. async function send({ address, amount, tokenAddress }) {
  407. // using private key
  408. assert(senderAccount != null, 'Error! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you send');
  409. if (tokenAddress) {
  410. const erc20ContractJson = require('./build/contracts/ERC20Mock.json');
  411. erc20 = new web3.eth.Contract(erc20ContractJson.abi, tokenAddress);
  412. let tokenBalance, tokenDecimals, tokenSymbol;
  413. if (!isTestRPC && !multiCall) {
  414. const callToken = await useMultiCall([[tokenAddress, erc20.methods.balanceOf(senderAccount).encodeABI()], [tokenAddress, erc20.methods.decimals().encodeABI()], [tokenAddress, erc20.methods.symbol().encodeABI()]]);
  415. tokenBalance = new BigNumber(callToken[0]);
  416. tokenDecimals = parseInt(callToken[1]);
  417. tokenSymbol = web3.eth.abi.decodeParameter('string', callToken[2]);
  418. } else {
  419. tokenBalance = new BigNumber(await erc20.methods.balanceOf(senderAccount).call());
  420. tokenDecimals = await erc20.methods.decimals().call();
  421. tokenSymbol = await erc20.methods.symbol().call();
  422. }
  423. const toSend = new BigNumber(amount).times(BigNumber(10).pow(tokenDecimals));
  424. if (tokenBalance.lt(toSend)) {
  425. console.error("You have", rmDecimalBN(tokenBalance.div(BigNumber(10).pow(tokenDecimals))), tokenSymbol, ", you can't send more than you have");
  426. process.exit(1);
  427. }
  428. const encodeTransfer = erc20.methods.transfer(address, toSend).encodeABI();
  429. await generateTransaction(tokenAddress, encodeTransfer);
  430. console.log('Sent', amount, tokenSymbol, 'to', address);
  431. } else {
  432. const balance = new BigNumber(await web3.eth.getBalance(senderAccount));
  433. assert(balance.toNumber() !== 0, "You have 0 balance, can't send transaction");
  434. if (amount) {
  435. toSend = new BigNumber(amount).times(BigNumber(10).pow(18));
  436. if (balance.lt(toSend)) {
  437. console.error("You have", rmDecimalBN(balance.div(BigNumber(10).pow(18))), netSymbol + ", you can't send more than you have.");
  438. process.exit(1);
  439. }
  440. } else {
  441. console.log('Amount not defined, sending all available amounts');
  442. const gasPrice = new BigNumber(await fetchGasPrice());
  443. const gasLimit = new BigNumber(21000);
  444. if (netId == 1) {
  445. const priorityFee = new BigNumber(await gasPrices(3));
  446. toSend = balance.minus(gasLimit.times(gasPrice.plus(priorityFee)));
  447. } else {
  448. toSend = balance.minus(gasLimit.times(gasPrice));
  449. }
  450. }
  451. await generateTransaction(address, null, toSend);
  452. console.log('Sent', rmDecimalBN(toSend.div(BigNumber(10).pow(18))), netSymbol, 'to', address);
  453. }
  454. }
  455. function getStatus(id, relayerURL, options) {
  456. return new Promise((resolve) => {
  457. async function getRelayerStatus() {
  458. const responseStatus = await axios.get(relayerURL + '/v1/jobs/' + id, options);
  459. if (responseStatus.status === 200) {
  460. const { txHash, status, confirmations, failedReason } = responseStatus.data
  461. console.log(`Current job status ${status}, confirmations: ${confirmations}`);
  462. if (status === 'FAILED') {
  463. throw new Error(status + ' failed reason:' + failedReason);
  464. }
  465. if (status === 'CONFIRMED') {
  466. const receipt = await waitForTxReceipt({ txHash });
  467. console.log(
  468. `Transaction submitted through the relay. View transaction on block explorer https://${getExplorerLink()}/tx/${txHash}`
  469. );
  470. console.log('Transaction mined in block', receipt.blockNumber);
  471. resolve(status);
  472. }
  473. }
  474. setTimeout(() => {
  475. getRelayerStatus(id, relayerURL);
  476. }, 3000)
  477. }
  478. getRelayerStatus();
  479. })
  480. }
  481. function capitalizeFirstLetter(string) {
  482. return string.charAt(0).toUpperCase() + string.slice(1);
  483. }
  484. function fromDecimals({ amount, decimals }) {
  485. amount = amount.toString();
  486. let ether = amount.toString();
  487. const base = new BN('10').pow(new BN(decimals));
  488. const baseLength = base.toString(10).length - 1 || 1;
  489. const negative = ether.substring(0, 1) === '-';
  490. if (negative) {
  491. ether = ether.substring(1);
  492. }
  493. if (ether === '.') {
  494. throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, invalid value');
  495. }
  496. // Split it into a whole and fractional part
  497. const comps = ether.split('.');
  498. if (comps.length > 2) {
  499. throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points');
  500. }
  501. let whole = comps[0];
  502. let fraction = comps[1];
  503. if (!whole) {
  504. whole = '0';
  505. }
  506. if (!fraction) {
  507. fraction = '0';
  508. }
  509. if (fraction.length > baseLength) {
  510. throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places');
  511. }
  512. while (fraction.length < baseLength) {
  513. fraction += '0';
  514. }
  515. whole = new BN(whole);
  516. fraction = new BN(fraction);
  517. let wei = whole.mul(base).add(fraction);
  518. if (negative) {
  519. wei = wei.mul(negative);
  520. }
  521. return new BN(wei.toString(10), 10);
  522. }
  523. function toDecimals(value, decimals, fixed) {
  524. const zero = new BN(0);
  525. const negative1 = new BN(-1);
  526. decimals = decimals || 18;
  527. fixed = fixed || 7;
  528. value = new BN(value);
  529. const negative = value.lt(zero);
  530. const base = new BN('10').pow(new BN(decimals));
  531. const baseLength = base.toString(10).length - 1 || 1;
  532. if (negative) {
  533. value = value.mul(negative1);
  534. }
  535. let fraction = value.mod(base).toString(10);
  536. while (fraction.length < baseLength) {
  537. fraction = `0${fraction}`;
  538. }
  539. fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
  540. const whole = value.div(base).toString(10);
  541. value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`;
  542. if (negative) {
  543. value = `-${value}`;
  544. }
  545. if (fixed) {
  546. value = value.slice(0, fixed);
  547. }
  548. return value;
  549. }
  550. // List fetched from https://github.com/ethereum-lists/chains/blob/master/_data/chains
  551. function getExplorerLink() {
  552. switch (netId) {
  553. case 56:
  554. return 'bscscan.com';
  555. case 100:
  556. return 'blockscout.com/poa/xdai';
  557. case 137:
  558. return 'polygonscan.com';
  559. case 42161:
  560. return 'arbiscan.io';
  561. case 43114:
  562. return 'snowtrace.io';
  563. case 5:
  564. return 'goerli.etherscan.io';
  565. case 42:
  566. return 'kovan.etherscan.io';
  567. case 10:
  568. return 'optimistic.etherscan.io';
  569. default:
  570. return 'etherscan.io';
  571. }
  572. }
  573. // List fetched from https://github.com/trustwallet/assets/tree/master/blockchains
  574. function getCurrentNetworkName() {
  575. switch (netId) {
  576. case 1:
  577. return 'Ethereum';
  578. case 56:
  579. return 'BinanceSmartChain';
  580. case 100:
  581. return 'GnosisChain';
  582. case 137:
  583. return 'Polygon';
  584. case 42161:
  585. return 'Arbitrum';
  586. case 43114:
  587. return 'Avalanche';
  588. case 5:
  589. return 'Goerli';
  590. case 42:
  591. return 'Kovan';
  592. case 10:
  593. return 'Optimism';
  594. default:
  595. return 'testRPC';
  596. }
  597. }
  598. function getCurrentNetworkSymbol() {
  599. switch (netId) {
  600. case 56:
  601. return 'BNB';
  602. case 100:
  603. return 'xDAI';
  604. case 137:
  605. return 'MATIC';
  606. case 43114:
  607. return 'AVAX';
  608. default:
  609. return 'ETH';
  610. }
  611. }
  612. function gasPricesETH(value = 80) {
  613. const tenPercent = (Number(value) * 5) / 100;
  614. const max = Math.max(tenPercent, 3);
  615. const bumped = Math.floor(Number(value) + max);
  616. return toHex(toWei(bumped.toString(), 'gwei'));
  617. }
  618. function gasPrices(value = 5) {
  619. return toHex(toWei(value.toString(), 'gwei'));
  620. }
  621. async function fetchGasPrice() {
  622. try {
  623. const options = {
  624. chainId: netId
  625. }
  626. // Bump fees for Ethereum network
  627. if (netId == 1) {
  628. const oracle = new GasPriceOracle(options);
  629. const gas = await oracle.gasPrices();
  630. return gasPricesETH(gas.instant);
  631. } else if (netId == 5 || isTestRPC) {
  632. const web3GasPrice = await web3.eth.getGasPrice();
  633. return web3GasPrice;
  634. } else {
  635. const oracle = new GasPriceOracle(options);
  636. const gas = await oracle.gasPrices();
  637. return gasPrices(gas.instant);
  638. }
  639. } catch (err) {
  640. throw new Error(`Method fetchGasPrice has error ${err.message}`);
  641. }
  642. }
  643. function calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerServiceFee, decimals }) {
  644. const decimalsPoint =
  645. Math.floor(relayerServiceFee) === Number(relayerServiceFee) ? 0 : relayerServiceFee.toString().split('.')[1].length;
  646. const roundDecimal = 10 ** decimalsPoint;
  647. const total = toBN(fromDecimals({ amount, decimals }));
  648. const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100));
  649. const expense = toBN(gasPrice).mul(toBN(5e5));
  650. let desiredFee;
  651. switch (currency) {
  652. case netSymbol.toLowerCase(): {
  653. desiredFee = expense.add(feePercent);
  654. break;
  655. }
  656. default: {
  657. desiredFee = expense
  658. .add(toBN(refund))
  659. .mul(toBN(10 ** decimals))
  660. .div(toBN(ethPrices[currency]));
  661. desiredFee = desiredFee.add(feePercent);
  662. break;
  663. }
  664. }
  665. return desiredFee;
  666. }
  667. /**
  668. * Waits for transaction to be mined
  669. * @param txHash Hash of transaction
  670. * @param attempts
  671. * @param delay
  672. */
  673. function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
  674. return new Promise((resolve, reject) => {
  675. const checkForTx = async (txHash, retryAttempt = 0) => {
  676. const result = await web3.eth.getTransactionReceipt(txHash);
  677. if (!result || !result.blockNumber) {
  678. if (retryAttempt <= attempts) {
  679. setTimeout(() => checkForTx(txHash, retryAttempt + 1), delay);
  680. } else {
  681. reject(new Error('tx was not mined'));
  682. }
  683. } else {
  684. resolve(result);
  685. }
  686. }
  687. checkForTx(txHash);
  688. })
  689. }
  690. function initJson(file) {
  691. return new Promise((resolve, reject) => {
  692. fs.readFile(file, 'utf8', (error, data) => {
  693. if (error) {
  694. resolve([]);
  695. }
  696. try {
  697. resolve(JSON.parse(data));
  698. } catch (error) {
  699. resolve([]);
  700. }
  701. });
  702. });
  703. };
  704. function loadCachedEvents({ type, currency, amount }) {
  705. try {
  706. const module = require(`./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`);
  707. if (module) {
  708. const events = module;
  709. return {
  710. events,
  711. lastBlock: events[events.length - 1].blockNumber
  712. }
  713. }
  714. } catch (err) {
  715. console.log("Error fetching cached files, syncing from block", deployedBlockNumber);
  716. return {
  717. events: [],
  718. lastBlock: deployedBlockNumber,
  719. }
  720. }
  721. }
  722. async function fetchEvents({ type, currency, amount }) {
  723. if (type === "withdraw") {
  724. type = "withdrawal";
  725. }
  726. const cachedEvents = loadCachedEvents({ type, currency, amount });
  727. const startBlock = cachedEvents.lastBlock + 1;
  728. console.log("Loaded cached",amount,currency.toUpperCase(),type,"events for",startBlock,"block");
  729. console.log("Fetching",amount,currency.toUpperCase(),type,"events for",netName,"network");
  730. async function syncEvents() {
  731. try {
  732. let targetBlock = await web3.eth.getBlockNumber();
  733. let chunks = 1000;
  734. console.log("Querying latest events from RPC");
  735. for (let i = startBlock; i < targetBlock; i += chunks) {
  736. let fetchedEvents = [];
  737. function mapDepositEvents() {
  738. fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => {
  739. const { commitment, leafIndex, timestamp } = returnValues;
  740. return {
  741. blockNumber,
  742. transactionHash,
  743. commitment,
  744. leafIndex: Number(leafIndex),
  745. timestamp
  746. }
  747. });
  748. }
  749. function mapWithdrawEvents() {
  750. fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => {
  751. const { nullifierHash, to, fee } = returnValues;
  752. return {
  753. blockNumber,
  754. transactionHash,
  755. nullifierHash,
  756. to,
  757. fee
  758. }
  759. });
  760. }
  761. function mapLatestEvents() {
  762. if (type === "deposit"){
  763. mapDepositEvents();
  764. } else {
  765. mapWithdrawEvents();
  766. }
  767. }
  768. async function fetchWeb3Events(i) {
  769. let j;
  770. if (i + chunks - 1 > targetBlock) {
  771. j = targetBlock;
  772. } else {
  773. j = i + chunks - 1;
  774. }
  775. await tornadoContract.getPastEvents(capitalizeFirstLetter(type), {
  776. fromBlock: i,
  777. toBlock: j,
  778. }).then(r => { fetchedEvents = fetchedEvents.concat(r); console.log("Fetched", amount, currency.toUpperCase(), type, "events to block:", j) }, err => { console.error(i + " failed fetching", type, "events from node", err); process.exit(1); }).catch(console.log);
  779. if (type === "deposit"){
  780. mapDepositEvents();
  781. } else {
  782. mapWithdrawEvents();
  783. }
  784. }
  785. async function updateCache() {
  786. try {
  787. const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`;
  788. const localEvents = await initJson(fileName);
  789. const events = localEvents.concat(fetchedEvents);
  790. await fs.writeFileSync(fileName, JSON.stringify(events, null, 2), 'utf8');
  791. } catch (error) {
  792. throw new Error('Writing cache file failed:',error);
  793. }
  794. }
  795. await fetchWeb3Events(i);
  796. await updateCache();
  797. }
  798. } catch (error) {
  799. throw new Error("Error while updating cache");
  800. process.exit(1);
  801. }
  802. }
  803. async function syncGraphEvents() {
  804. let options = {};
  805. if (torPort) {
  806. options = { httpsAgent: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0' } };
  807. }
  808. async function queryLatestTimestamp() {
  809. try {
  810. const variables = {
  811. currency: currency.toString(),
  812. amount: amount.toString()
  813. }
  814. if (type === "deposit") {
  815. const query = {
  816. query: `
  817. query($currency: String, $amount: String){
  818. deposits(first: 1, orderBy: timestamp, orderDirection: desc, where: {currency: $currency, amount: $amount}) {
  819. timestamp
  820. }
  821. }
  822. `,
  823. variables
  824. }
  825. const querySubgraph = await axios.post(subgraph, query, options);
  826. const queryResult = querySubgraph.data.data.deposits;
  827. const result = queryResult[0].timestamp;
  828. return Number(result);
  829. } else {
  830. const query = {
  831. query: `
  832. query($currency: String, $amount: String){
  833. withdrawals(first: 1, orderBy: timestamp, orderDirection: desc, where: {currency: $currency, amount: $amount}) {
  834. timestamp
  835. }
  836. }
  837. `,
  838. variables
  839. }
  840. const querySubgraph = await axios.post(subgraph, query, options);
  841. const queryResult = querySubgraph.data.data.withdrawals;
  842. const result = queryResult[0].timestamp;
  843. return Number(result);
  844. }
  845. } catch (error) {
  846. console.error("Failed to fetch latest event from thegraph");
  847. }
  848. }
  849. async function queryFromGraph(timestamp) {
  850. try {
  851. const variables = {
  852. currency: currency.toString(),
  853. amount: amount.toString(),
  854. timestamp: timestamp
  855. }
  856. if (type === "deposit") {
  857. const query = {
  858. query: `
  859. query($currency: String, $amount: String, $timestamp: Int){
  860. deposits(orderBy: timestamp, first: 1000, where: {currency: $currency, amount: $amount, timestamp_gt: $timestamp}) {
  861. blockNumber
  862. transactionHash
  863. commitment
  864. index
  865. timestamp
  866. }
  867. }
  868. `,
  869. variables
  870. }
  871. const querySubgraph = await axios.post(subgraph, query, options);
  872. const queryResult = querySubgraph.data.data.deposits;
  873. const mapResult = queryResult.map(({ blockNumber, transactionHash, commitment, index, timestamp }) => {
  874. return {
  875. blockNumber: Number(blockNumber),
  876. transactionHash,
  877. commitment,
  878. leafIndex: Number(index),
  879. timestamp
  880. }
  881. });
  882. return mapResult;
  883. } else {
  884. const query = {
  885. query: `
  886. query($currency: String, $amount: String, $timestamp: Int){
  887. withdrawals(orderBy: timestamp, first: 1000, where: {currency: $currency, amount: $amount, timestamp_gt: $timestamp}) {
  888. blockNumber
  889. transactionHash
  890. nullifier
  891. to
  892. fee
  893. }
  894. }
  895. `,
  896. variables
  897. }
  898. const querySubgraph = await axios.post(subgraph, query, options);
  899. const queryResult = querySubgraph.data.data.withdrawals;
  900. const mapResult = queryResult.map(({ blockNumber, transactionHash, nullifier, to, fee }) => {
  901. return {
  902. blockNumber: Number(blockNumber),
  903. transactionHash,
  904. nullifierHash: nullifier,
  905. to,
  906. fee
  907. }
  908. });
  909. return mapResult;
  910. }
  911. } catch (error) {
  912. console.error(error);
  913. }
  914. }
  915. async function updateCache(fetchedEvents) {
  916. try {
  917. const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`;
  918. const localEvents = await initJson(fileName);
  919. const events = localEvents.concat(fetchedEvents);
  920. await fs.writeFileSync(fileName, JSON.stringify(events, null, 2), 'utf8');
  921. } catch (error) {
  922. throw new Error('Writing cache file failed:',error);
  923. }
  924. }
  925. async function fetchGraphEvents() {
  926. console.log("Querying latest events from TheGraph");
  927. const latestTimestamp = await queryLatestTimestamp();
  928. if (latestTimestamp) {
  929. const getCachedBlock = await web3.eth.getBlock(startBlock);
  930. const cachedTimestamp = getCachedBlock.timestamp;
  931. for (let i = cachedTimestamp; i < latestTimestamp;) {
  932. const result = await queryFromGraph(i);
  933. if (Object.keys(result).length === 0) {
  934. i = latestTimestamp;
  935. } else {
  936. if (type === "deposit") {
  937. const resultBlock = result[result.length - 1].blockNumber;
  938. const resultTimestamp = result[result.length - 1].timestamp;
  939. await updateCache(result);
  940. i = resultTimestamp;
  941. console.log("Fetched", amount, currency.toUpperCase(), type, "events to block:", Number(resultBlock));
  942. } else {
  943. const resultBlock = result[result.length - 1].blockNumber;
  944. const getResultBlock = await web3.eth.getBlock(resultBlock);
  945. const resultTimestamp = getResultBlock.timestamp;
  946. await updateCache(result);
  947. i = resultTimestamp;
  948. console.log("Fetched", amount, currency.toUpperCase(), type, "events to block:", Number(resultBlock));
  949. }
  950. }
  951. }
  952. } else {
  953. console.log("Fallback to web3 events");
  954. await syncEvents();
  955. }
  956. }
  957. await fetchGraphEvents();
  958. }
  959. if (!privateRpc && !subgraph && !isTestRPC) {
  960. await syncGraphEvents();
  961. } else {
  962. await syncEvents();
  963. }
  964. async function loadUpdatedEvents() {
  965. const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`;
  966. const updatedEvents = await initJson(fileName);
  967. const updatedBlock = updatedEvents[updatedEvents.length - 1].blockNumber;
  968. console.log("Cache updated for Tornado",type,amount,currency,"instance to block",updatedBlock,"successfully");
  969. console.log(`Total ${type}s:`, updatedEvents.length);
  970. return updatedEvents;
  971. }
  972. const events = await loadUpdatedEvents();
  973. return events;
  974. }
  975. /**
  976. * Parses Tornado.cash note
  977. * @param noteString the note
  978. */
  979. function parseNote(noteString) {
  980. const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g
  981. const match = noteRegex.exec(noteString);
  982. if (!match) {
  983. throw new Error('The note has invalid format');
  984. }
  985. const buf = Buffer.from(match.groups.note, 'hex');
  986. const nullifier = bigInt.leBuff2int(buf.slice(0, 31));
  987. const secret = bigInt.leBuff2int(buf.slice(31, 62));
  988. const deposit = createDeposit({ nullifier, secret });
  989. const netId = Number(match.groups.netId);
  990. return {
  991. currency: match.groups.currency,
  992. amount: match.groups.amount,
  993. netId,
  994. deposit
  995. }
  996. }
  997. /**
  998. * Parses Tornado.cash deposit invoice
  999. * @param invoiceString the note
  1000. */
  1001. function parseInvoice(invoiceString) {
  1002. const noteRegex = /tornadoInvoice-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<commitmentNote>[0-9a-fA-F]{64})/g
  1003. const match = noteRegex.exec(invoiceString)
  1004. if (!match) {
  1005. throw new Error('The note has invalid format')
  1006. }
  1007. const netId = Number(match.groups.netId)
  1008. const buf = Buffer.from(match.groups.commitmentNote, 'hex')
  1009. const commitmentNote = toHex(buf.slice(0, 32))
  1010. return {
  1011. currency: match.groups.currency,
  1012. amount: match.groups.amount,
  1013. netId,
  1014. commitmentNote
  1015. }
  1016. }
  1017. async function loadDepositData({ amount, currency, deposit }) {
  1018. try {
  1019. const cachedEvents = await fetchEvents({ type: 'deposit', currency, amount });
  1020. const eventWhenHappened = await cachedEvents.filter(function (event) {
  1021. return event.commitment === deposit.commitmentHex;
  1022. })[0];
  1023. if (eventWhenHappened.length === 0) {
  1024. throw new Error('There is no related deposit, the note is invalid');
  1025. }
  1026. const timestamp = eventWhenHappened.timestamp;
  1027. const txHash = eventWhenHappened.transactionHash;
  1028. const isSpent = await tornadoContract.methods.isSpent(deposit.nullifierHex).call();
  1029. const receipt = await web3.eth.getTransactionReceipt(txHash);
  1030. return {
  1031. timestamp,
  1032. txHash,
  1033. isSpent,
  1034. from: receipt.from,
  1035. commitment: deposit.commitmentHex
  1036. }
  1037. } catch (e) {
  1038. console.error('loadDepositData', e);
  1039. }
  1040. return {}
  1041. }
  1042. async function loadWithdrawalData({ amount, currency, deposit }) {
  1043. try {
  1044. const cachedEvents = await fetchEvents({ type: 'withdrawal', currency, amount });
  1045. const withdrawEvent = cachedEvents.filter((event) => {
  1046. return event.nullifierHash === deposit.nullifierHex
  1047. })[0];
  1048. const fee = withdrawEvent.fee;
  1049. const decimals = config.deployments[`netId${netId}`][currency].decimals;
  1050. const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(toBN(fee));
  1051. const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockNumber);
  1052. return {
  1053. amount: toDecimals(withdrawalAmount, decimals, 9),
  1054. txHash: withdrawEvent.transactionHash,
  1055. to: withdrawEvent.to,
  1056. timestamp,
  1057. nullifier: deposit.nullifierHex,
  1058. fee: toDecimals(fee, decimals, 9)
  1059. }
  1060. } catch (e) {
  1061. console.error('loadWithdrawalData', e);
  1062. }
  1063. }
  1064. /**
  1065. * Init web3, contracts, and snark
  1066. */
  1067. async function init({ rpc, noteNetId, currency = 'dai', amount = '100', balanceCheck, localMode }) {
  1068. let contractJson, instanceJson, erc20ContractJson, erc20tornadoJson, tornadoAddress, tokenAddress;
  1069. // TODO do we need this? should it work in browser really?
  1070. if (inBrowser) {
  1071. // Initialize using injected web3 (Metamask)
  1072. // To assemble web version run `npm run browserify`
  1073. web3 = new Web3(window.web3.currentProvider, null, {
  1074. transactionConfirmationBlocks: 1
  1075. });
  1076. contractJson = await (await fetch('build/contracts/TornadoProxy.abi.json')).json();
  1077. instanceJson = await (await fetch('build/contracts/Instance.abi.json')).json();
  1078. circuit = await (await fetch('build/circuits/tornado.json')).json();
  1079. proving_key = await (await fetch('build/circuits/tornadoProvingKey.bin')).arrayBuffer();
  1080. MERKLE_TREE_HEIGHT = 20;
  1081. ETH_AMOUNT = 1e18;
  1082. TOKEN_AMOUNT = 1e19;
  1083. senderAccount = (await web3.eth.getAccounts())[0];
  1084. } else {
  1085. let ipOptions = {};
  1086. if (torPort && rpc.includes("https")) {
  1087. console.log("Using tor network");
  1088. web3Options = { agent: { https: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort) }, timeout: 60000 };
  1089. // Use forked web3-providers-http from local file to modify user-agent header value which improves privacy.
  1090. web3 = new Web3(new Web3HttpProvider(rpc, web3Options), null, { transactionConfirmationBlocks: 1 });
  1091. ipOptions = { httpsAgent: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0' } };
  1092. } else if (torPort && rpc.includes("http")) {
  1093. console.log("Using tor network");
  1094. web3Options = { agent: { http: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort) }, timeout: 60000 };
  1095. // Use forked web3-providers-http from local file to modify user-agent header value which improves privacy.
  1096. web3 = new Web3(new Web3HttpProvider(rpc, web3Options), null, { transactionConfirmationBlocks: 1 });
  1097. ipOptions = { httpsAgent: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0' } };
  1098. } else if (rpc.includes("ipc")) {
  1099. console.log("Using ipc connection");
  1100. web3 = new Web3(new Web3.providers.IpcProvider(rpc, net), null, { transactionConfirmationBlocks: 1 });
  1101. } else if (rpc.includes("ws") || rpc.includes("wss")) {
  1102. console.log("Using websocket connection (Note: Tor is not supported for Websocket providers)");
  1103. web3Options = { clientConfig: { keepalive: true, keepaliveInterval: -1 }, reconnect: { auto: true, delay: 1000, maxAttempts: 10, onTimeout: false } };
  1104. web3 = new Web3(new Web3.providers.WebsocketProvider(rpc, web3Options), net, { transactionConfirmationBlocks: 1 });
  1105. } else {
  1106. console.log("Connecting to remote node");
  1107. web3 = new Web3(rpc, null, { transactionConfirmationBlocks: 1 });
  1108. }
  1109. const rpcHost = new URL(rpc).hostname;
  1110. const isIpPrivate = is_ip_private(rpcHost);
  1111. if (!isIpPrivate && !rpc.includes("localhost") && !privateRpc) {
  1112. try {
  1113. const fetchRemoteIP = await axios.get('https://ip.tornado.cash', ipOptions);
  1114. const { country, ip } = fetchRemoteIP.data;
  1115. console.log('Your remote IP address is', ip, 'from', country + '.');
  1116. } catch (error) {
  1117. console.error('Could not fetch remote IP from ip.tornado.cash, use VPN if the problem repeats.');
  1118. }
  1119. } else if (isIpPrivate || rpc.includes("localhost")) {
  1120. console.log('Local RPC detected');
  1121. privateRpc = true;
  1122. }
  1123. contractJson = require('./build/contracts/TornadoProxy.abi.json');
  1124. instanceJson = require('./build/contracts/Instance.abi.json');
  1125. circuit = require('./build/circuits/tornado.json');
  1126. proving_key = fs.readFileSync('build/circuits/tornadoProvingKey.bin').buffer;
  1127. MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20;
  1128. ETH_AMOUNT = process.env.ETH_AMOUNT;
  1129. TOKEN_AMOUNT = process.env.TOKEN_AMOUNT;
  1130. const privKey = process.env.PRIVATE_KEY;
  1131. if (privKey) {
  1132. if (privKey.includes("0x")) {
  1133. PRIVATE_KEY = process.env.PRIVATE_KEY.substring(2);
  1134. } else {
  1135. PRIVATE_KEY = process.env.PRIVATE_KEY;
  1136. }
  1137. }
  1138. if (PRIVATE_KEY) {
  1139. const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY);
  1140. web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY);
  1141. web3.eth.defaultAccount = account.address;
  1142. senderAccount = account.address;
  1143. }
  1144. erc20ContractJson = require('./build/contracts/ERC20Mock.json');
  1145. erc20tornadoJson = require('./build/contracts/ERC20Tornado.json');
  1146. }
  1147. // groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI
  1148. groth16 = await buildGroth16();
  1149. netId = await web3.eth.net.getId();
  1150. netName = getCurrentNetworkName();
  1151. netSymbol = getCurrentNetworkSymbol();
  1152. if (noteNetId && Number(noteNetId) !== netId) {
  1153. throw new Error('This note is for a different network. Specify the --rpc option explicitly');
  1154. }
  1155. if (netName === "testRPC") {
  1156. isTestRPC = true;
  1157. }
  1158. if (localMode) {
  1159. console.log("Local mode detected: will not submit signed TX to remote node");
  1160. doNotSubmitTx = true;
  1161. }
  1162. if (isTestRPC) {
  1163. tornadoAddress = currency === netSymbol.toLowerCase() ? contractJson.networks[netId].address : erc20tornadoJson.networks[netId].address;
  1164. tokenAddress = currency !== netSymbol.toLowerCase() ? erc20ContractJson.networks[netId].address : null;
  1165. deployedBlockNumber = 0;
  1166. senderAccount = (await web3.eth.getAccounts())[0];
  1167. } else {
  1168. try {
  1169. if (balanceCheck) {
  1170. currency = netSymbol.toLowerCase();
  1171. amount = Object.keys(config.deployments[`netId${netId}`][currency].instanceAddress)[0];
  1172. }
  1173. tornadoAddress = config.deployments[`netId${netId}`].proxy;
  1174. multiCall = config.deployments[`netId${netId}`].multicall;
  1175. subgraph = config.deployments[`netId${netId}`].subgraph;
  1176. tornadoInstance = config.deployments[`netId${netId}`][currency].instanceAddress[amount];
  1177. deployedBlockNumber = config.deployments[`netId${netId}`][currency].deployedBlockNumber[amount];
  1178. if (!tornadoAddress) {
  1179. throw new Error();
  1180. }
  1181. tokenAddress = currency !== netSymbol.toLowerCase() ? config.deployments[`netId${netId}`][currency].tokenAddress : null;
  1182. } catch (e) {
  1183. console.error('There is no such tornado instance, check the currency and amount you provide', e);
  1184. process.exit(1);
  1185. }
  1186. }
  1187. tornado = new web3.eth.Contract(contractJson, tornadoAddress);
  1188. tornadoContract = new web3.eth.Contract(instanceJson, tornadoInstance);
  1189. contractAddress = tornadoAddress;
  1190. erc20 = currency !== netSymbol.toLowerCase() ? new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) : {};
  1191. erc20Address = tokenAddress;
  1192. }
  1193. async function main() {
  1194. if (inBrowser) {
  1195. const instance = { currency: 'eth', amount: '0.1' };
  1196. await init(instance);
  1197. window.deposit = async () => {
  1198. await deposit(instance);
  1199. }
  1200. window.withdraw = async () => {
  1201. const noteString = prompt('Enter the note to withdraw');
  1202. const recipient = (await web3.eth.getAccounts())[0];
  1203. const { currency, amount, netId, deposit } = parseNote(noteString);
  1204. await init({ noteNetId: netId, currency, amount });
  1205. await withdraw({ deposit, currency, amount, recipient });
  1206. }
  1207. } else {
  1208. program
  1209. .option('-r, --rpc <URL>', 'The RPC that CLI should interact with', 'http://localhost:8545')
  1210. .option('-R, --relayer <URL>', 'Withdraw via relayer')
  1211. .option('-T, --tor <PORT>', 'Optional tor port')
  1212. .option('-L, --local', 'Local Node - Does not submit signed transaction to the node')
  1213. .option('-o, --onlyrpc', 'Only rpc mode - Does not enable thegraph api nor remote ip detection');
  1214. program
  1215. .command('createNote <currency> <amount> <chainId>')
  1216. .description(
  1217. 'Create deposit note and invoice, allows generating private key like deposit notes from secure, offline environment. The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). The amount depends on currency, see config.js file or visit https://tornado.cash.'
  1218. )
  1219. .action(async (currency, amount, chainId) => {
  1220. currency = currency.toLowerCase();
  1221. await createInvoice({ currency, amount, chainId });
  1222. });
  1223. program
  1224. .command('depositInvoice <invoice>')
  1225. .description(
  1226. 'Submit a deposit of invoice from default eth account and return the resulting note.'
  1227. )
  1228. .action(async (invoice) => {
  1229. if (program.onlyrpc) {
  1230. privateRpc = true;
  1231. }
  1232. torPort = program.tor;
  1233. const { currency, amount, netId, commitmentNote } = parseInvoice(invoice);
  1234. await init({ rpc: program.rpc, currency, amount, localMode: program.local });
  1235. console.log("Creating", currency.toUpperCase(), amount, "deposit for", netName, "Tornado Cash Instance");
  1236. await deposit({ currency, amount, commitmentNote });
  1237. });
  1238. program
  1239. .command('deposit <currency> <amount>')
  1240. .description(
  1241. 'Submit a deposit of specified currency and amount from default eth account and return the resulting note. The currency is one of (ETH|DAI|cDAI|USDC|cUSDC|USDT). The amount depends on currency, see config.js file or visit https://tornado.cash.'
  1242. )
  1243. .action(async (currency, amount) => {
  1244. if (program.onlyrpc) {
  1245. privateRpc = true;
  1246. }
  1247. currency = currency.toLowerCase();
  1248. torPort = program.tor;
  1249. await init({ rpc: program.rpc, currency, amount, localMode: program.local });
  1250. await deposit({ currency, amount });
  1251. });
  1252. program
  1253. .command('withdraw <note> <recipient> [ETH_purchase]')
  1254. .description(
  1255. 'Withdraw a note to a recipient account using relayer or specified private key. You can exchange some of your deposit`s tokens to ETH during the withdrawal by specifing ETH_purchase (e.g. 0.01) to pay for gas in future transactions. Also see the --relayer option.'
  1256. )
  1257. .action(async (noteString, recipient, refund) => {
  1258. if (program.onlyrpc) {
  1259. privateRpc = true;
  1260. }
  1261. const { currency, amount, netId, deposit } = parseNote(noteString);
  1262. torPort = program.tor;
  1263. await init({ rpc: program.rpc, noteNetId: netId, currency, amount, localMode: program.local });
  1264. await withdraw({
  1265. deposit,
  1266. currency,
  1267. amount,
  1268. recipient,
  1269. refund,
  1270. relayerURL: program.relayer
  1271. });
  1272. });
  1273. program
  1274. .command('balance [address] [token_address]')
  1275. .description('Check ETH and ERC20 balance')
  1276. .action(async (address, tokenAddress) => {
  1277. if (program.onlyrpc) {
  1278. privateRpc = true;
  1279. }
  1280. torPort = program.tor;
  1281. await init({ rpc: program.rpc, balanceCheck: true });
  1282. if (!address && senderAccount) {
  1283. console.log("Using address", senderAccount, "from private key");
  1284. address = senderAccount;
  1285. }
  1286. await printETHBalance({ address, name: 'Account' });
  1287. if (tokenAddress) {
  1288. await printERC20Balance({ address, name: 'Account', tokenAddress });
  1289. }
  1290. });
  1291. program
  1292. .command('send <address> [amount] [token_address]')
  1293. .description('Send ETH or ERC to address')
  1294. .action(async (address, amount, tokenAddress) => {
  1295. if (program.onlyrpc) {
  1296. privateRpc = true;
  1297. }
  1298. torPort = program.tor;
  1299. await init({ rpc: program.rpc, balanceCheck: true, localMode: program.local });
  1300. await send({ address, amount, tokenAddress });
  1301. });
  1302. program
  1303. .command('broadcast <signedTX>')
  1304. .description('Submit signed TX to the remote node')
  1305. .action(async (signedTX) => {
  1306. if (program.onlyrpc) {
  1307. privateRpc = true;
  1308. }
  1309. torPort = program.tor;
  1310. await init({ rpc: program.rpc, balanceCheck: true });
  1311. await submitTransaction(signedTX);
  1312. });
  1313. program
  1314. .command('compliance <note>')
  1315. .description(
  1316. 'Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.'
  1317. )
  1318. .action(async (noteString) => {
  1319. if (program.onlyrpc) {
  1320. privateRpc = true;
  1321. }
  1322. const { currency, amount, netId, deposit } = parseNote(noteString);
  1323. torPort = program.tor;
  1324. await init({ rpc: program.rpc, noteNetId: netId, currency, amount });
  1325. const depositInfo = await loadDepositData({ amount, currency, deposit });
  1326. const depositDate = new Date(depositInfo.timestamp * 1000);
  1327. console.log('\n=============Deposit=================');
  1328. console.log('Deposit :', amount, currency.toUpperCase());
  1329. console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString());
  1330. console.log('From :', `https://${getExplorerLink()}/address/${depositInfo.from}`);
  1331. console.log('Transaction :', `https://${getExplorerLink()}/tx/${depositInfo.txHash}`);
  1332. console.log('Commitment :', depositInfo.commitment);
  1333. console.log('Spent :', depositInfo.isSpent);
  1334. if (!depositInfo.isSpent) {
  1335. console.log('The note was not spent');
  1336. return;
  1337. }
  1338. console.log('=====================================', '\n');
  1339. const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit });
  1340. const withdrawalDate = new Date(withdrawInfo.timestamp * 1000);
  1341. console.log('\n=============Withdrawal==============');
  1342. console.log('Withdrawal :', withdrawInfo.amount, currency);
  1343. console.log('Relayer Fee :', withdrawInfo.fee, currency);
  1344. console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString());
  1345. console.log('To :', `https://${getExplorerLink()}/address/${withdrawInfo.to}`);
  1346. console.log('Transaction :', `https://${getExplorerLink()}/tx/${withdrawInfo.txHash}`);
  1347. console.log('Nullifier :', withdrawInfo.nullifier);
  1348. console.log('=====================================', '\n');
  1349. });
  1350. program
  1351. .command('syncEvents <type> <currency> <amount>')
  1352. .description(
  1353. 'Sync the local cache file of deposit / withdrawal events for specific currency.'
  1354. )
  1355. .action(async (type, currency, amount) => {
  1356. if (program.onlyrpc) {
  1357. privateRpc = true;
  1358. }
  1359. console.log("Starting event sync command");
  1360. currency = currency.toLowerCase();
  1361. torPort = program.tor;
  1362. await init({ rpc: program.rpc, type, currency, amount });
  1363. const cachedEvents = await fetchEvents({ type, currency, amount });
  1364. console.log("Synced event for", type, amount, currency.toUpperCase(), netName, "Tornado instance to block", cachedEvents[cachedEvents.length - 1].blockNumber);
  1365. });
  1366. program
  1367. .command('test')
  1368. .description('Perform an automated test. It deposits and withdraws one ETH and one ERC20 note. Uses ganache.')
  1369. .action(async () => {
  1370. privateRpc = true;
  1371. console.log('Start performing ETH deposit-withdraw test');
  1372. let currency = 'eth';
  1373. let amount = '0.1';
  1374. await init({ rpc: program.rpc, currency, amount });
  1375. let noteString = await deposit({ currency, amount });
  1376. let parsedNote = parseNote(noteString);
  1377. await withdraw({
  1378. deposit: parsedNote.deposit,
  1379. currency,
  1380. amount,
  1381. recipient: senderAccount,
  1382. relayerURL: program.relayer
  1383. });
  1384. console.log('\nStart performing DAI deposit-withdraw test');
  1385. currency = 'dai';
  1386. amount = '100';
  1387. await init({ rpc: program.rpc, currency, amount });
  1388. noteString = await deposit({ currency, amount });
  1389. parsedNote = parseNote(noteString);
  1390. await withdraw({
  1391. deposit: parsedNote.deposit,
  1392. currency,
  1393. amount,
  1394. recipient: senderAccount,
  1395. refund: '0.02',
  1396. relayerURL: program.relayer
  1397. });
  1398. });
  1399. try {
  1400. await program.parseAsync(process.argv);
  1401. process.exit(0);
  1402. } catch (e) {
  1403. console.log('Error:', e);
  1404. process.exit(1);
  1405. }
  1406. }
  1407. }
  1408. main();