diff --git a/.eslintrc.js b/.eslintrc.js index f1e2882..23389a3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -42,10 +42,12 @@ module.exports = { } ], "import/order": ["error"], + /** "indent": [ "error", 2 ], + **/ "linebreak-style": [ "error", "unix" diff --git a/package.json b/package.json index c45ea46..d8308e4 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "compliance": "ts-node src/cli.ts compliance", "syncEvents": "ts-node src/cli.ts syncEvents", "relayers": "ts-node src/cli.ts relayers", - "createNoteAccount": "ts-node src/cli.ts createNoteAccount", + "createAccount": "ts-node src/cli.ts createAccount", "decryptNotes": "ts-node src/cli.ts decryptNotes", "send": "ts-node src/cli.ts send", "balance": "ts-node src/cli.ts balance", diff --git a/src/merkleTreeWorker.ts b/src/merkleTreeWorker.ts index f4e2201..05c268d 100644 --- a/src/merkleTreeWorker.ts +++ b/src/merkleTreeWorker.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-explicit-any, prettier/prettier */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import workerThreads from 'worker_threads'; import { MerkleTree, Element, TreeEdge, PartialMerkleTree } from '@tornado/fixed-merkle-tree'; import { mimc, isNode } from './services'; @@ -65,5 +65,5 @@ if (isNode && workerThreads) { postMessage(merkleTree.toString()); }); } else { - throw new Error('This browser / environment doesn\'t support workers!'); + throw new Error('This browser / environment does not support workers!'); } diff --git a/src/program.ts b/src/program.ts index f7518f0..b46eb27 100644 --- a/src/program.ts +++ b/src/program.ts @@ -524,13 +524,14 @@ export function tornadoProgram() { .action(async (netId: string | number, currency: string, amount: string, cmdOptions: commonProgramOptions) => { const { options, fetchDataOptions } = await getProgramOptions(cmdOptions); currency = currency.toLowerCase(); - const { rpc } = options; + const { rpc, accountKey } = options; const config = networkConfig[`netId${netId}`]; const { multicall: multicallAddress, routerContract, + echoContract, nativeCurrency, tokens: { [currency]: currencyConfig }, } = config; @@ -553,6 +554,14 @@ export function tornadoProgram() { provider, }); + const noteAccount = accountKey + ? new NoteAccount({ + netId, + recoveryKey: accountKey, + Echoer: Echoer__factory.connect(echoContract, provider), + }) + : undefined; + if (!signer) { throw new Error( 'Signer not defined, make sure you have either viewOnly address, mnemonic, or private key configured', @@ -569,22 +578,20 @@ export function tornadoProgram() { name: 'getEthBalance', params: [signer.address], }, - /* eslint-disable prettier/prettier */ ...(!isEth ? [ - { - contract: Token as ERC20, - name: 'balanceOf', - params: [signer.address], - }, - { - contract: Token as ERC20, - name: 'allowance', - params: [signer.address, routerContract], - }, - ] + { + contract: Token as ERC20, + name: 'balanceOf', + params: [signer.address], + }, + { + contract: Token as ERC20, + name: 'allowance', + params: [signer.address, routerContract], + }, + ] : []), - /* eslint-enable prettier/prettier */ ]); if (isEth && denomination > ethBalance) { @@ -616,18 +623,36 @@ export function tornadoProgram() { const { note, noteHex, commitmentHex } = deposit; + const encryptedNote = noteAccount + ? noteAccount.encryptNote({ + address: instanceAddress, + noteHex, + }) + : '0x'; + + const backupFile = `./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`; + console.log(`New deposit: ${deposit.toString()}\n`); - await writeFile(`./backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`, note, { - encoding: 'utf8', - }); + console.log(`Writing note backup at ${backupFile}\n`); + + await writeFile(backupFile, note, { encoding: 'utf8' }); + + if (encryptedNote !== '0x') { + console.log(`Storing encrypted note on-chain for backup (Account key: ${accountKey})\n`); + } await programSendTransaction({ signer, options, - populatedTransaction: await TornadoProxy.deposit.populateTransaction(instanceAddress, commitmentHex, '0x', { - value: isEth ? denomination : BigInt(0), - }), + populatedTransaction: await TornadoProxy.deposit.populateTransaction( + instanceAddress, + commitmentHex, + encryptedNote, + { + value: isEth ? denomination : BigInt(0), + }, + ), }); process.exit(0); @@ -689,22 +714,20 @@ export function tornadoProgram() { name: 'getEthBalance', params: [signer.address], }, - /* eslint-disable prettier/prettier */ ...(!isEth ? [ - { - contract: Token as ERC20, - name: 'balanceOf', - params: [signer.address], - }, - { - contract: Token as ERC20, - name: 'allowance', - params: [signer.address, routerContract], - }, - ] + { + contract: Token as ERC20, + name: 'balanceOf', + params: [signer.address], + }, + { + contract: Token as ERC20, + name: 'allowance', + params: [signer.address, routerContract], + }, + ] : []), - /* eslint-enable prettier/prettier */ ]); if (isEth && denomination > ethBalance) { @@ -923,15 +946,13 @@ export function tornadoProgram() { readFile(CIRCUIT_PATH, { encoding: 'utf8' }).then((s) => JSON.parse(s)), readFile(KEY_PATH).then((b) => new Uint8Array(b).buffer), depositTreePromise, - /* eslint-disable prettier/prettier */ !walletWithdrawal ? getProgramRelayer({ - options, - fetchDataOptions, - netId, - }).then(({ relayerClient }) => relayerClient) + options, + fetchDataOptions, + netId, + }).then(({ relayerClient }) => relayerClient) : undefined, - /* eslint-enable prettier/prettier */ tornadoFeeOracle.fetchL1OptimismFee(), !isEth ? tokenPriceOracle.fetchPrices([tokenAddress as string]).then((p) => p[0]) : BigInt(0), provider.getFeeData(), @@ -1466,7 +1487,7 @@ export function tornadoProgram() { }); program - .command('createNoteAccount') + .command('createAccount') .description( 'Creates and save on-chain account that would store encrypted notes. \n\n' + 'Would first lookup on on-chain records to see if the notes are stored. \n\n' + @@ -1532,7 +1553,7 @@ export function tornadoProgram() { const userEvents = echoEvents.filter(({ address }) => address === signer.address); - const existingAccounts = userEvents.map((e) => newAccount.decryptAccountWithWallet(signer, e)); + const existingAccounts = newAccount.decryptAccountsWithWallet(signer, userEvents); const accountsTable = new Table(); @@ -1587,7 +1608,7 @@ export function tornadoProgram() { .argument('', 'Network Chain ID to connect with (see https://chainlist.org for examples)', parseNumber) .argument( '[accountKey]', - 'Account key generated from UI or the createNoteAccount to store encrypted notes on-chain', + 'Account key generated from UI or the createAccount to store encrypted notes on-chain', parseRecoveryKey, ) .action(async (netId: string | number, accountKey: string | undefined, cmdOptions: commonProgramOptions) => { @@ -1614,7 +1635,7 @@ export function tornadoProgram() { if (!accountKey) { throw new Error( - 'No account key find! Please supply correct account key from either UI or find one with createNoteAccount command', + 'No account key find! Please supply correct account key from either UI or find one with createAccount command', ); } @@ -1720,15 +1741,14 @@ export function tornadoProgram() { const txGasPrice = feeData.maxFeePerGas ? feeData.maxFeePerGas + (feeData.maxPriorityFeePerGas || BigInt(0)) : feeData.gasPrice || BigInt(0); - /* eslint-disable prettier/prettier */ const txFees = feeData.maxFeePerGas ? { - maxFeePerGas: feeData.maxFeePerGas, - maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, - } + maxFeePerGas: feeData.maxFeePerGas, + maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, + } : { - gasPrice: feeData.gasPrice, - }; + gasPrice: feeData.gasPrice, + }; let toSend: bigint; @@ -1764,10 +1784,10 @@ export function tornadoProgram() { ...txFees, }); - const bumpedGas = (estimatedGas !== BigInt(21000) && signer.gasLimitBump - ? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000) - : estimatedGas - ); + const bumpedGas = + estimatedGas !== BigInt(21000) && signer.gasLimitBump + ? (estimatedGas * (BigInt(10000) + BigInt(signer.gasLimitBump))) / BigInt(10000) + : estimatedGas; toSend = ethBalance - txGasPrice * bumpedGas; } @@ -1779,15 +1799,14 @@ export function tornadoProgram() { populatedTransaction: tokenAddress ? await Token.transfer.populateTransaction(to, toSend) : await signer.populateTransaction({ - type: txType, - from: signer.address, - to, - value: toSend, - nonce, - ...txFees, - }), + type: txType, + from: signer.address, + to, + value: toSend, + nonce, + ...txFees, + }), }); - /* eslint-enable prettier/prettier */ process.exit(0); }, @@ -1891,7 +1910,6 @@ export function tornadoProgram() { options, populatedTransaction: deserializedTx, }); - /* eslint-enable prettier/prettier */ process.exit(0); }); @@ -1924,14 +1942,24 @@ export function tornadoProgram() { }); // common options - /* eslint-disable prettier/prettier */ program.commands.forEach((cmd) => { cmd.option('-r, --rpc ', 'The RPC that CLI should interact with', parseUrl); cmd.option('-e, --eth-rpc ', 'The Ethereum Mainnet RPC that CLI should interact with', parseUrl); cmd.option('-g, --graph ', 'The Subgraph API that CLI should interact with', parseUrl); - cmd.option('-G, --eth-graph ', 'The Ethereum Mainnet Subgraph API that CLI should interact with', parseUrl); - cmd.option('-d, --disable-graph', 'Disable Graph API - Does not enable Subgraph API and use only local RPC as an event source'); - cmd.option('-a, --account-key ', 'Account key generated from UI or the createNoteAccount to store encrypted notes on-chain', parseRecoveryKey); + cmd.option( + '-G, --eth-graph ', + 'The Ethereum Mainnet Subgraph API that CLI should interact with', + parseUrl, + ); + cmd.option( + '-d, --disable-graph', + 'Disable Graph API - Does not enable Subgraph API and use only local RPC as an event source', + ); + cmd.option( + '-a, --account-key ', + 'Account key generated from UI or the createAccount to store encrypted notes on-chain', + parseRecoveryKey, + ); cmd.option('-R, --relayer ', 'Withdraw via relayer (Should be either .eth name or URL)', parseRelayer); cmd.option('-w, --wallet-withdrawal', 'Withdrawal via wallet (Should not be linked with deposits)'); cmd.option('-T, --tor-port ', 'Optional tor port', parseNumber); @@ -1943,13 +1971,13 @@ export function tornadoProgram() { ); cmd.option( '-m, --mnemonic ', - 'Wallet BIP39 Mnemonic Phrase - If you didn\'t add it to .env file and it is needed for operation', + 'Wallet BIP39 Mnemonic Phrase - If you did not add it to .env file and it is needed for operation', parseMnemonic, ); cmd.option('-i, --mnemonic-index ', 'Optional wallet mnemonic index', parseNumber); cmd.option( '-p, --private-key ', - 'Wallet private key - If you didn\'t add it to .env file and it is needed for operation', + 'Wallet private key - If you did not add it to .env file and it is needed for operation', parseKey, ); cmd.option( @@ -1958,7 +1986,6 @@ export function tornadoProgram() { ); cmd.option('-l, --local-rpc', 'Local node mode - Does not submit signed transaction to the node'); }); - /* eslint-enable prettier/prettier */ return program; } diff --git a/src/services/encryptedNotes.ts b/src/services/encryptedNotes.ts index ebae695..6bbe7c8 100644 --- a/src/services/encryptedNotes.ts +++ b/src/services/encryptedNotes.ts @@ -4,6 +4,11 @@ import { Wallet, computeAddress } from 'ethers'; import { crypto, base64ToBytes, bytesToBase64, bytesToHex, hexToBytes, toFixedHex, concatBytes } from './utils'; import { EchoEvents, EncryptedNotesEvents } from './events'; +export interface NoteToEncrypt { + address: string; + noteHex: string; +} + export interface DecryptedNotes { blockNumber: number; address: string; @@ -110,26 +115,39 @@ export class NoteAccount { /** * Decrypt Echoer backuped note encryption account with private keys */ - decryptAccountWithWallet(wallet: Wallet, event: EchoEvents): NoteAccount { + decryptAccountsWithWallet(wallet: Wallet, events: EchoEvents[]): NoteAccount[] { let { privateKey } = wallet; if (privateKey.startsWith('0x')) { privateKey = privateKey.replace('0x', ''); } - const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount); + const decryptedEvents = []; - const recoveryKey = decrypt({ - encryptedData: unpackedMessage, - privateKey, - }); + for (const event of events) { + try { + const unpackedMessage = unpackEncryptedMessage(event.encryptedAccount); - return new NoteAccount({ - netId: this.netId, - blockNumber: event.blockNumber, - recoveryKey, - Echoer: this.Echoer, - }); + const recoveryKey = decrypt({ + encryptedData: unpackedMessage, + privateKey, + }); + + decryptedEvents.push( + new NoteAccount({ + netId: this.netId, + blockNumber: event.blockNumber, + recoveryKey, + Echoer: this.Echoer, + }), + ); + } catch { + // decryption may fail for invalid accounts + continue; + } + } + + return decryptedEvents; } decryptNotes(events: EncryptedNotesEvents[]): DecryptedNotes[] { @@ -157,4 +175,14 @@ export class NoteAccount { return decryptedEvents; } + + encryptNote({ address, noteHex }: NoteToEncrypt) { + const encryptedData = encrypt({ + publicKey: this.recoveryPublicKey, + data: `${address}-${noteHex}`, + version: 'x25519-xsalsa20-poly1305', + }); + + return packEncryptedMessage(encryptedData); + } } diff --git a/src/services/merkleTree.ts b/src/services/merkleTree.ts index 829670f..166e31a 100644 --- a/src/services/merkleTree.ts +++ b/src/services/merkleTree.ts @@ -8,7 +8,7 @@ import type { DepositsEvents } from './events'; export type MerkleTreeConstructor = DepositType & { Tornado: Tornado; - commitment?: string; + commitmentHex?: string; merkleTreeHeight?: number; emptyElement?: string; merkleWorkerPath?: string; @@ -19,7 +19,7 @@ export class MerkleTreeService { amount: string; netId: number; Tornado: Tornado; - commitment?: string; + commitmentHex?: string; instanceName: string; merkleTreeHeight: number; @@ -32,7 +32,7 @@ export class MerkleTreeService { amount, currency, Tornado, - commitment, + commitmentHex, merkleTreeHeight = 20, emptyElement = '21663839004416932945382355908790599225266501822907911457504978515578255421292', merkleWorkerPath, @@ -45,7 +45,7 @@ export class MerkleTreeService { this.Tornado = Tornado; this.instanceName = instanceName; - this.commitment = commitment; + this.commitmentHex = commitmentHex; this.merkleTreeHeight = merkleTreeHeight; this.emptyElement = emptyElement; diff --git a/src/services/providers.ts b/src/services/providers.ts index d91900a..1bd1d00 100644 --- a/src/services/providers.ts +++ b/src/services/providers.ts @@ -213,47 +213,47 @@ export async function fetchData(url: string, options: fetchDataOptions = {}) { throw errorObject; } -/* eslint-disable prettier/prettier, @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-explicit-any */ export const fetchGetUrlFunc = (options: fetchDataOptions = {}): FetchGetUrlFunc => - async (req, _signal) => { - let signal; + async (req, _signal) => { + let signal; - if (_signal) { - const controller = new AbortController(); - signal = controller.signal; - _signal.addListener(() => { - controller.abort(); - }); - } - - const init = { - ...options, - method: req.method || 'POST', - headers: req.headers, - body: req.body || undefined, - signal, - returnResponse: true, - }; - - const resp = await fetchData(req.url, init); - - const headers = {} as { [key in string]: any }; - resp.headers.forEach((value: any, key: string) => { - headers[key.toLowerCase()] = value; + if (_signal) { + const controller = new AbortController(); + signal = controller.signal; + _signal.addListener(() => { + controller.abort(); }); + } - const respBody = await resp.arrayBuffer(); - const body = respBody == null ? null : new Uint8Array(respBody); - - return { - statusCode: resp.status, - statusMessage: resp.statusText, - headers, - body, - }; + const init = { + ...options, + method: req.method || 'POST', + headers: req.headers, + body: req.body || undefined, + signal, + returnResponse: true, }; -/* eslint-enable prettier/prettier, @typescript-eslint/no-explicit-any */ + + const resp = await fetchData(req.url, init); + + const headers = {} as { [key in string]: any }; + resp.headers.forEach((value: any, key: string) => { + headers[key.toLowerCase()] = value; + }); + + const respBody = await resp.arrayBuffer(); + const body = respBody == null ? null : new Uint8Array(respBody); + + return { + statusCode: resp.status, + statusMessage: resp.statusText, + headers, + body, + }; + }; +/* eslint-enable @typescript-eslint/no-explicit-any */ // caching to improve performance const oracleMapper = new Map();