From 16cc0b3e61b95d51e9fc489d1217361db3aad7c2 Mon Sep 17 00:00:00 2001 From: Ayanami Date: Sat, 22 Jan 2022 13:49:45 +0900 Subject: [PATCH 1/5] Update fetchEvents to write files per chunks --- cli.js | 156 +++++++++++++++++++++++++++++++++------------------------ 1 file changed, 90 insertions(+), 66 deletions(-) diff --git a/cli.js b/cli.js index 945be36..87d9667 100755 --- a/cli.js +++ b/cli.js @@ -618,6 +618,21 @@ function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) { }) } +function initJson(file) { + return new Promise((resolve, reject) => { + fs.readFile(file, 'utf8', (error, data) => { + if (error) { + reject(error); + } + try { + resolve(JSON.parse(data)); + } catch (error) { + resolve([]); + } + }); + }); +}; + function loadCachedEvents({ type, currency, amount }) { try { const module = require(`./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`) @@ -641,8 +656,6 @@ function loadCachedEvents({ type, currency, amount }) { async function fetchEvents({ type, currency, amount}) { let leafIndex = -1 - let events = []; - let fetchedEvents, chunks, targetBlock; if (type === "withdraw") { type = "withdrawal" @@ -651,75 +664,86 @@ async function fetchEvents({ type, currency, amount}) { const cachedEvents = loadCachedEvents({ type, currency, amount }) const startBlock = cachedEvents.lastBlock + 1 + console.log("Loaded cached",amount,currency.toUpperCase(),type,"events for",startBlock,"block") console.log("Fetching",amount,currency.toUpperCase(),type,"events for",netName,"network") - async function fetchLatestEvents() { - targetBlock = await web3.eth.getBlockNumber(); - chunks = 1000; - fetchedEvents = []; - for (let i=startBlock; i < targetBlock; i+=chunks) { - await tornadoContract.getPastEvents(capitalizeFirstLetter(type), { - fromBlock: i, - toBlock: i+chunks-1, - }).then(r => { fetchedEvents = fetchedEvents.concat(r); console.log("Fetched",amount,currency.toUpperCase(),type,"events from block:", i) }, err => { console.error(i + " failed fetching",type,"events from node", err) }).catch(console.log); - } - } - await fetchLatestEvents() - - async function mapDepositEvents() { - fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => { - const { commitment, leafIndex, timestamp } = returnValues - return { - blockNumber, - transactionHash, - commitment, - leafIndex: Number(leafIndex), - timestamp - } - }) - } - - async function mapWithdrawEvents() { - fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => { - const { nullifierHash, to, fee } = returnValues - return { - blockNumber, - transactionHash, - nullifierHash, - to, - fee - } - }) - } - - async function mapLatestEvents() { - console.log("Mapping",amount,currency.toUpperCase(),type,"events, please wait") - if (type === "deposit"){ - await mapDepositEvents(); - } else { - await mapWithdrawEvents(); - } - } - await mapLatestEvents(); - - console.log("Gathering cached events + collected events from node") - - async function concatEvents() { - events = cachedEvents.events.concat(fetchedEvents) - } - await concatEvents(); - - console.log('Total events:', events.length) - - async function updateCache() { + async function syncEvents() { try { - await fs.writeFileSync(`./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`, JSON.stringify(events, null, 2), 'utf8') - console.log("Cache updated for Tornado",type,amount,currency,"instance successfully") - } catch (e) { - throw new Error('Writing cache file failed:',e) + let targetBlock = await web3.eth.getBlockNumber(); + let chunks = 1000; + for (let i=startBlock; i < targetBlock; i+=chunks) { + let fetchedEvents = []; + async function fetchLatestEvents(i) { + await tornadoContract.getPastEvents(capitalizeFirstLetter(type), { + fromBlock: i, + toBlock: i+chunks-1, + }).then(r => { fetchedEvents = fetchedEvents.concat(r); console.log("Fetched",amount,currency.toUpperCase(),type,"events from block:", i) }, err => { console.error(i + " failed fetching",type,"events from node", err); process.exit(1); }).catch(console.log); + } + + async function mapDepositEvents() { + fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => { + const { commitment, leafIndex, timestamp } = returnValues + return { + blockNumber, + transactionHash, + commitment, + leafIndex: Number(leafIndex), + timestamp + } + }) + } + + async function mapWithdrawEvents() { + fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => { + const { nullifierHash, to, fee } = returnValues + return { + blockNumber, + transactionHash, + nullifierHash, + to, + fee + } + }) + } + + async function mapLatestEvents() { + if (type === "deposit"){ + await mapDepositEvents(); + } else { + await mapWithdrawEvents(); + } + } + + async function updateCache() { + try { + const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json` + const localEvents = await initJson(fileName); + const events = localEvents.concat(fetchedEvents); + await fs.writeFileSync(fileName, JSON.stringify(events, null, 2), 'utf8') + } catch (error) { + throw new Error('Writing cache file failed:',error) + } + } + await fetchLatestEvents(i); + await mapLatestEvents(); + await updateCache(); + } + } catch (error) { + throw new Error("Error while updating cache") + process.exit(1) } } - await updateCache(); + await syncEvents(); + + async function loadUpdatedEvents() { + const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json` + const updatedEvents = await initJson(fileName); + const updatedBlock = updatedEvents[updatedEvents.length - 1].blockNumber + console.log("Cache updated for Tornado",type,amount,currency,"instance to block",updatedBlock,"successfully") + console.log('Total events:', updatedEvents.length) + return updatedEvents; + } + const events = await loadUpdatedEvents(); return events } From 93fafe0178bcb56b7ccd3c7a90f9819a82704b0a Mon Sep 17 00:00:00 2001 From: Ayanami Date: Sun, 23 Jan 2022 15:52:59 +0900 Subject: [PATCH 2/5] Minor changes + If the function needs to check native coin, use netSymbol instead + Unused variables removed + Support Private Key with '0x' as well --- cli.js | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/cli.js b/cli.js index 87d9667..faa785c 100755 --- a/cli.js +++ b/cli.js @@ -144,7 +144,7 @@ async function deposit({ currency, amount }) { const noteString = `tornado-${currency}-${amount}-${netId}-${note}` console.log(`Your note: ${noteString}`) await backupNote({ currency, amount, netId, note, noteString }) - if (currency === 'eth' || currency === 'bnb' || currency === 'xdai' || currency === 'matic' || currency === 'avax') { + if (currency === netSymbol.toLowerCase()) { await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract', symbol: currency.toUpperCase() }) await printETHBalance({ address: senderAccount, name: 'Sender account', symbol: currency.toUpperCase() }) const value = isLocalRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 }) @@ -269,7 +269,7 @@ async function generateProof({ deposit, currency, amount, recipient, relayerAddr */ async function withdraw({ deposit, currency, amount, recipient, relayerURL, torPort, refund = '0' }) { let options = {}; - if (currency === 'eth' && refund !== '0') { + if (currency === netSymbol.toLowerCase() && refund !== '0') { throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals') } refund = toWei(refund) @@ -328,7 +328,7 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, torP // 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. const { address } = await web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY) - assert(recipient.toLowerCase() == address.toLowerCase(), 'Withdrawal amount recepient',recipient,'mismatches with the account of provided private key from environment file',address) + assert(recipient.toLowerCase() == address.toLowerCase(), 'Withdrawal amount recepient mismatches with the account of provided private key from environment file') const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund }) @@ -562,23 +562,7 @@ function calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerSe const expense = toBN(gasPrice).mul(toBN(5e5)) let desiredFee switch (currency) { - case 'eth': { - desiredFee = expense.add(feePercent) - break - } - case 'bnb': { - desiredFee = expense.add(feePercent) - break - } - case 'xdai': { - desiredFee = expense.add(feePercent) - break - } - case 'matic': { - desiredFee = expense.add(feePercent) - break - } - case 'avax': { + case netSymbol.toLowerCase(): { desiredFee = expense.add(feePercent) break } @@ -655,8 +639,6 @@ function loadCachedEvents({ type, currency, amount }) { } async function fetchEvents({ type, currency, amount}) { - let leafIndex = -1 - if (type === "withdraw") { type = "withdrawal" } @@ -677,7 +659,7 @@ async function fetchEvents({ type, currency, amount}) { await tornadoContract.getPastEvents(capitalizeFirstLetter(type), { fromBlock: i, toBlock: i+chunks-1, - }).then(r => { fetchedEvents = fetchedEvents.concat(r); console.log("Fetched",amount,currency.toUpperCase(),type,"events from block:", i) }, err => { console.error(i + " failed fetching",type,"events from node", err); process.exit(1); }).catch(console.log); + }).then(r => { fetchedEvents = fetchedEvents.concat(r); console.log("Fetched",amount,currency.toUpperCase(),type,"events to block:", i) }, err => { console.error(i + " failed fetching",type,"events from node", err); process.exit(1); }).catch(console.log); } async function mapDepositEvents() { @@ -870,7 +852,12 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100', torPort, MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20 ETH_AMOUNT = process.env.ETH_AMOUNT TOKEN_AMOUNT = process.env.TOKEN_AMOUNT - PRIVATE_KEY = process.env.PRIVATE_KEY + const privKey = process.env.PRIVATE_KEY + if (privKey.includes("0x")) { + PRIVATE_KEY = process.env.PRIVATE_KEY.substring(2) + } else { + PRIVATE_KEY = process.env.PRIVATE_KEY + } if (PRIVATE_KEY) { const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY) web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY) @@ -917,6 +904,7 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100', torPort, process.exit(1) } } + netSymbol = getCurrentNetworkSymbol() tornado = new web3.eth.Contract(contractJson, tornadoAddress) tornadoContract = new web3.eth.Contract(instanceJson, tornadoInstance) contractAddress = tornadoAddress From e865eb9af865ee7015f147c669a6274f65fe1da7 Mon Sep 17 00:00:00 2001 From: Ayanami Date: Sun, 23 Jan 2022 16:26:24 +0900 Subject: [PATCH 3/5] Support EIP-1559 transaction for Polygon & Avalanche Most of the transaction on those chains have same maxFeePerGas and maxPriorityFeePerGas value --- cli.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cli.js b/cli.js index faa785c..31b8cdc 100755 --- a/cli.js +++ b/cli.js @@ -85,6 +85,16 @@ async function generateTransaction(to, encodedData, value = 0) { gas : gasLimit, data : encodedData } + } else if (netId == 137 || netId == 43114) { + tx = { + to : to, + value : value, + nonce : nonce, + maxFeePerGas : gasPrice, + maxPriorityFeePerGas : gasPrice, + gas : gasLimit, + data : encodedData + } } else { tx = { to : to, From 90a34da5b8db2d881266fc82d9b469aac924e914 Mon Sep 17 00:00:00 2001 From: Ayanami Date: Sun, 23 Jan 2022 18:42:08 +0900 Subject: [PATCH 4/5] Add Optimistic network support & update README.md --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++---- cli.js | 4 ++++ config.js | 20 ++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a2d624c..33078fb 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,40 @@ -# Warning! +# Tornado cli + +Command line tool to interact with [Tornado Cash](https://tornadocash.eth.link). + +### Warning! Current cli version doesn't support [Anonymity Mining](https://tornado-cash.medium.com/tornado-cash-governance-proposal-a55c5c7d0703) -### Goerli, Mainnet, Binance Smart Chain +### How to install tornado cli +Download and install [node.js](https://nodejs.org/en/download/). + +If you have git installed on your system, clone the master branch. + +```bash +$ git clone https://github.com/tornadocash/tornado-cli +``` + +Or, download the archive file from github + +https://github.com/tornadocash/tornado-cli/archive/refs/heads/master.zip + +After downloading or cloning the repository, you must install necessary libraries using the following command. + +```bash +$ cd tornado-cli +$ npm install +``` + +If you want to use Tor connection to conceal ip address, install [Tor Browser](https://www.torproject.org/download/) and add `--tor 9150` for `cli.js` if you connect tor with browser. +Note that you should reset your tor connection by restarting the browser every time when you deposit & withdraw otherwise you will have the same exit node used for connection. + +### Goerli, Mainnet, Binance Smart Chain, Gnosis Chain, Polygon Network, Arbitrum, Avalanche 1. Add `PRIVATE_KEY` to `.env` file -2. `./cli.js --help` +2. `node cli.js --help` Example: ```bash -$ ./cli.js deposit ETH 0.1 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --tor 9050 +$ node cli.js deposit ETH 0.1 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --tor 9150 Your note: tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 Tornado ETH balance is 8.9 @@ -18,7 +45,7 @@ Sender account ETH balance is 1004873.361652048361352542 ``` ```bash -$ ./cli.js withdraw tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --relayer https://goerli-frelay.duckdns.org --tor 9050 +$ node cli.js withdraw tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --relayer https://goerli-frelay.duckdns.org --tor 9150 Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754 Getting current state from tornado contract @@ -520,6 +547,41 @@ Infura API key fetched from https://rpc.info (Same one with Metamask) "cachedUrl":"https://goerli-v2.defidevotee.xyz" } } + }, + "netId10":{ + "rpcUrls":{ + "Optimism":{ + "name":"Optimism Public RPC", + "url":"https://mainnet.optimism.io" + } + }, + "relayers":{ + "optimism.t-relay.eth":{ + "url":"optimism.t-relay.eth", + "name":"optimism.t-relay.eth", + "cachedUrl":"https://optimism.t-relay.online/" + }, + "optimism.therelayer.eth":{ + "url":"optimism.therelayer.eth", + "name":"optimism.therelayer.eth", + "cachedUrl":"https://optimism.therelayer.xyz/" + }, + "optimism.relayer-service.eth":{ + "url":"optimism.relayer-service.eth", + "name":"optimism.relayer-service.eth", + "cachedUrl":"https://optimism-relayer.hertz.zone/" + }, + "optimism.torn.eth":{ + "url":"optimism.torn.eth", + "name":"optimism.torn.eth", + "cachedUrl":"https://optimism.torn.cash/" + }, + "optimism.relaymy.eth":{ + "url":"optimism.relaymy.eth", + "name":"optimism.relaymy.eth", + "cachedUrl":"https://optimism.relaymy.xyz/" + } + } } } ``` diff --git a/cli.js b/cli.js index 31b8cdc..a72a0e8 100755 --- a/cli.js +++ b/cli.js @@ -486,6 +486,8 @@ function getExplorerLink() { return 'goerli.etherscan.io' case 42: return 'kovan.etherscan.io' + case 10: + return 'optimistic.etherscan.io' default: return 'etherscan.io' } @@ -510,6 +512,8 @@ function getCurrentNetworkName() { return 'Goerli' case 42: return 'Kovan' + case 137: + return 'Optimism' default: return 'localRPC' } diff --git a/config.js b/config.js index 054434b..e8b431a 100644 --- a/config.js +++ b/config.js @@ -330,5 +330,25 @@ module.exports = { }, proxy: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17', }, + netId42161: { + 'eth': { + 'instanceAddress': { + '0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F', + '1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3', + '10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a', + '100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD' + }, + 'deployedBlockNumber': { + '0.1': 2243707, + '1': 2243709, + '10': 2243735, + '100': 2243749 + }, + 'miningEnabled': false, + 'symbol': 'ETH', + 'decimals': 18 + }, + proxy: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17', + }, } } From a4c511c86f9b07dfd301e70e3c7effece69ad35c Mon Sep 17 00:00:00 2001 From: Ayanami Date: Sun, 23 Jan 2022 21:03:46 +0900 Subject: [PATCH 5/5] Support sending ETH and ERC20 --- cli.js | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/cli.js b/cli.js index a72a0e8..666da53 100755 --- a/cli.js +++ b/cli.js @@ -71,7 +71,11 @@ async function generateTransaction(to, encodedData, value = 0) { const bumped = Math.floor(fetchedGas * 1.3) gasLimit = web3.utils.toHex(bumped) } - await estimateGas(); + if (encodedData) { + await estimateGas(); + } else { + gasLimit = web3.utils.toHex(21000); + } async function txoptions() { // Generate EIP-1559 transaction @@ -348,6 +352,53 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, torP console.log('Done withdrawal from Tornado Cash') } +/** + * Do an ETH / ERC20 send + * @param address Recepient address + * @param amount Amount to send + * @param tokenAddress ERC20 token address + */ +async function send({ address, amount, tokenAddress }) { + // using private key + assert(senderAccount != null, 'Error! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you send') + if (tokenAddress) { + const erc20ContractJson = require('./build/contracts/ERC20Mock.json') + erc20 = new web3.eth.Contract(erc20ContractJson.abi, tokenAddress) + const balance = await erc20.methods.balanceOf(senderAccount).call() + const decimals = await erc20.methods.decimals().call() + const toSend = amount * Math.pow(10, decimals) + if (balance < toSend) { + console.error("You have",toDecimals(balance, decimals, (balance.length + decimals)).toString().replace(/\B(? { async function getRelayerStatus() { @@ -984,6 +1035,13 @@ async function main() { await printERC20Balance({ address, name: 'Account', tokenAddress }) } }) + program + .command('send
[amount] [token_address]') + .description('Send ETH or ERC to address') + .action(async (address, amount, tokenAddress) => { + await init({ rpc: program.rpc, torPort: program.tor, balanceCheck: true }) + await send({ address, amount, tokenAddress }) + }) program .command('compliance ') .description(