forked from tornadocash/tornado-cli
Save encrypted notes on router
This commit is contained in:
parent
195da66ce2
commit
3312f44a4d
@ -42,10 +42,12 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
"import/order": ["error"],
|
||||
/**
|
||||
"indent": [
|
||||
"error",
|
||||
2
|
||||
],
|
||||
**/
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
|
@ -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",
|
||||
|
@ -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!');
|
||||
}
|
||||
|
165
src/program.ts
165
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('<netId>', '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 <RPC_URL>', 'The RPC that CLI should interact with', parseUrl);
|
||||
cmd.option('-e, --eth-rpc <ETHRPC_URL>', 'The Ethereum Mainnet RPC that CLI should interact with', parseUrl);
|
||||
cmd.option('-g, --graph <GRAPH_URL>', 'The Subgraph API that CLI should interact with', parseUrl);
|
||||
cmd.option('-G, --eth-graph <ETHGRAPH_URL>', '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>', 'Account key generated from UI or the createNoteAccount to store encrypted notes on-chain', parseRecoveryKey);
|
||||
cmd.option(
|
||||
'-G, --eth-graph <ETHGRAPH_URL>',
|
||||
'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>',
|
||||
'Account key generated from UI or the createAccount to store encrypted notes on-chain',
|
||||
parseRecoveryKey,
|
||||
);
|
||||
cmd.option('-R, --relayer <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 <TOR_PORT>', 'Optional tor port', parseNumber);
|
||||
@ -1943,13 +1971,13 @@ export function tornadoProgram() {
|
||||
);
|
||||
cmd.option(
|
||||
'-m, --mnemonic <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 <MNEMONIC_INDEX>', 'Optional wallet mnemonic index', parseNumber);
|
||||
cmd.option(
|
||||
'-p, --private-key <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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user