#!/usr/bin/env node const fs = require('fs'); const crypto = require('crypto').webcrypto; const BN = require('bn.js'); const { Command } = require('commander'); const circomlibjs = require('circomlibjs'); const { MaxUint256, Interface } = require('ethers'); const ERC20ABI = require('./abi/ERC20.abi.json'); const RouterABI = require('./abi/TornadoProxy.abi.json'); const { enabledChains, networkConfig } = require('./networkConfig'); const { description, version } = require('./package.json'); const program = new Command(); const erc20Interface = new Interface(ERC20ABI); const routerInterface = new Interface(RouterABI); const bytesToHex = (bytes) => '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); // Convert BE encoded bytes (Buffer | Uint8Array) array to BigInt const bytesToBN = (bytes) => BigInt(bytesToHex(bytes)); // Convert LE encoded bytes (Buffer | Uint8Array) array to BigInt // const leBuff2Int = (bytes) => new BN(bytes, 16, 'le'); // Convert BigInt to LE encoded Uint8Array type const leInt2Buff = (bigint) => Uint8Array.from(new BN(bigint).toArray('le', 31)); const toFixedHex = (numberish, length = 32) => { return '0x' + BigInt(numberish).toString(16).padStart(length * 2, '0'); }; const rBigInt = (nbytes = 31) => bytesToBN(crypto.getRandomValues(new Uint8Array(nbytes))); // https://github.com/tornadocash/tornado-classic-ui/blob/master/services/pedersen.js class Pedersen { constructor() { this.pedersenPromise = this.initPedersen(); } async initPedersen() { this.pedersenHash = await circomlibjs.buildPedersenHash(); this.babyJub = this.pedersenHash.babyJub; } async unpackPoint(buffer) { await this.pedersenPromise; return this.babyJub?.unpackPoint(this.pedersenHash?.hash(buffer)); } toStringBuffer(bytes) { return this.babyJub.F.toString(bytes); } } const pedersen = new Pedersen(); const buffPedersenHash = async (bytes) => { const [hash] = await pedersen.unpackPoint(bytes); return pedersen.toStringBuffer(hash); }; const createDeposit = async ({ nullifier, secret }) => { const preimage = new Uint8Array([...leInt2Buff(nullifier), ...leInt2Buff(secret)]); const noteHex = toFixedHex(bytesToBN(preimage), 62); const commitment = BigInt(await buffPedersenHash(preimage)); const commitmentHex = toFixedHex(commitment); const nullifierHash = BigInt(await buffPedersenHash(leInt2Buff(nullifier))); const nullifierHex = toFixedHex(nullifierHash); return { preimage, noteHex, commitment, commitmentHex, nullifierHash, nullifierHex, }; }; /** * Create Tornado Deposit Note and Invoice */ const createNote = async ({ netId, currency, amount }) => { const nullifier = rBigInt(31); const secret = rBigInt(31); const depositObject = await createDeposit({ nullifier, secret, }); return { currency, amount, netId, note: `tornado-${currency}-${amount}-${netId}-${depositObject.noteHex}`, invoice: `tornadoInvoice-${currency}-${amount}-${netId}-${depositObject.commitmentHex}`, nullifier, secret, ...depositObject, }; }; const getInstanceInfo = ({ netId, currency, amount }) => { const networkObject = networkConfig[`netId${netId}`]; const routerAddress = networkObject['tornado-router.contract.tornadocash.eth'] || networkObject['tornado-proxy-light.contract.tornadocash.eth'] || networkObject['tornado-proxy.contract.tornadocash.eth']; const instanceAddress = networkObject.tokens[currency]?.instanceAddress[amount]; const instanceDecimals = Number(networkObject.tokens[currency]?.decimals); const tokenAddress = networkObject.tokens[currency]?.tokenAddress; const isNativeCurrency = networkObject.nativeCurrency === currency; if (!instanceAddress) { const errMsg = `Could not find a tornado instance ${netId} ${currency} ${amount}`; throw new Error(errMsg); } const denomination = BigInt(amount * 10 ** instanceDecimals); return { routerAddress, instanceAddress, instanceDecimals, tokenAddress, isNativeCurrency, denomination, }; }; const newDeposit = async ({ netId, currency, amount }) => { currency = String(currency).toLowerCase(); amount = Number(amount); netId = Number(netId); if (!enabledChains.includes(netId.toString())) { const errMsg = `Unsupported chain ${netId}`; throw new Error(errMsg); } const { routerAddress, instanceAddress, tokenAddress, isNativeCurrency, denomination, } = getInstanceInfo({ netId, currency, amount }); const { note, noteHex, invoice, commitmentHex: commitment, nullifierHex: nullifierHash } = await createNote({ netId, currency, amount }); const depositData = routerInterface.encodeFunctionData('deposit', [instanceAddress, commitment, '0x']); console.log( `New deposit: ${JSON.stringify({ note, invoice, commitment, nullifierHash }, null, 2)}\n` ); // Backup locally fs.writeFileSync(`./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`, note, { encoding: 'utf8' }); if (isNativeCurrency) { console.log( `Transaction Data: ${JSON.stringify({ to: routerAddress, value: denomination.toString(), data: depositData }, null, 2)}` ); return; } const approveData = erc20Interface.encodeFunctionData('approve', [routerAddress, MaxUint256]); console.log( `Approve Data: ${JSON.stringify({ to: tokenAddress, data: approveData }, null, 2)}]\n` ); console.log( `Transaction Data: ${JSON.stringify({ to: routerAddress, data: depositData }, null, 2)}` ); }; program .name('tornadoOffline') .description(description) .version(version) .argument('', 'netId of the supported network (Ethereum Mainnet: 1, Goerli Testnet: 5, BSC: 56)') .argument('', 'Native Currency or Token supported by Tornado Cash') .argument('', 'Amount to deposit (Check the UI for supported amount)') .action((netId, currency, amount) => { console.log('Creating offline Tornado Cash Note\n'); newDeposit({ netId, currency, amount }); }); program .command('list') .description('List tornado cash backup notes on local') .action(() => { const backups = fs.readdirSync('.').filter(f => f.includes('backup')); const context = backups.map(b => fs.readFileSync(b, { encoding: 'utf8' })).join('\n'); console.log(context); }); program.parse();