forked from tornadocash/tornado-cli
2078 lines
84 KiB
JavaScript
Executable File
2078 lines
84 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
require('dotenv').config();
|
||
const fs = require('fs');
|
||
const axios = require('axios');
|
||
const assert = require('assert');
|
||
const snarkjs = require('@tornado/snarkjs');
|
||
const crypto = require('crypto');
|
||
const circomlib = require('@tornado/circomlib');
|
||
const bigInt = snarkjs.bigInt;
|
||
const MerkleTree = require('@tornado/fixed-merkle-tree');
|
||
const Web3 = require('web3');
|
||
const web3Utils = require("web3-utils")
|
||
const buildGroth16 = require('@tornado/websnark/src/groth16');
|
||
const websnarkUtils = require('@tornado/websnark/src/utils');
|
||
const { toWei, fromWei, toBN, BN } = require('web3-utils');
|
||
const BigNumber = require('bignumber.js');
|
||
const program = require('commander');
|
||
const { TornadoFeeOracleV4, TornadoFeeOracleV5 } = require('@tornado/tornado-oracles');
|
||
const { SocksProxyAgent } = require('socks-proxy-agent');
|
||
const is_ip_private = require('private-ip');
|
||
const readline = require('readline');
|
||
|
||
const prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||
|
||
const config = require('./config');
|
||
const erc20Abi = require('./abis/ERC20.abi.json');
|
||
const tornadoProxyAbi = require('./abis/TornadoProxy.abi.json');
|
||
const tornadoInstanceAbi = require('./abis/Instance.abi.json');
|
||
const relayerRegistryAbi = require("./abis/RelayerRegistry.abi.json");
|
||
const relayerAggregatorAbi = require('./abis/Aggregator.abi');
|
||
const tornadoGovernanceAbi = require("./abis/Governance.abi.json");
|
||
const stakingRewardsAbi = require("./abis/StakingRewards.abi.json");
|
||
|
||
const relayerAggregatorAddress = config.deployments[`netId1`].relayerAggregator;
|
||
const relayerRegistryAddress = config.deployments[`netId1`].relayerRegistry;
|
||
const relayerRegistryDeployedBlockNumber = config.deployments["netId1"].relayerRegistryDeployedBlockNumber;
|
||
const relayerSubdomains = Object.values(config.deployments).map(({ ensSubdomainKey }) => ensSubdomainKey);
|
||
const tornTokenAddress = "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C";
|
||
const governanceAddress = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce";
|
||
const stakingRewardsAddress = "0x5B3f656C80E8ddb9ec01Dd9018815576E9238c29";
|
||
|
||
/** @typedef {import ("web3-eth-contract").Contract} Web3Contract */
|
||
/** @typedef {import ("web3-eth").Eth} Web3Eth */
|
||
/** @typedef {('deposit' | 'withdrawal' | 'relayer')} EventType */
|
||
|
||
/**
|
||
* @typedef RequestOptions
|
||
* @type {Object}
|
||
* @property {number} timeout Timeout in milliseconds
|
||
* @property {SocksProxyAgent} [httpsAgent] Proxy agent instance, if user selected Tor proxy
|
||
* @property {Object} [headers] Headers for request, for exmple, User-Agent, if needed
|
||
*/
|
||
|
||
/**
|
||
* @typedef ProgramGlobals
|
||
* @type {Object}
|
||
* @property {string} [privateKey] User-provided private key for Ethereum account
|
||
* @property {Web3Eth} [web3Instance] Instance of Web3 Eth to interact with blockchain networks
|
||
* @property {Web3Eth} [relayerWeb3Instance] Instance of Web3 Eth to interact with Ethereum Mainnet
|
||
* @property {boolean} useOnlyRpc If true, use only RPC without third-party requests (IP detection, subgraph API)
|
||
* @property {boolean} shouldPromptConfirmation Ask user for confirmation in interactive mode for crucial actions (withdraw note, send money)
|
||
* @property {boolean} shouldSubmitTx If false, don't broadcast signed transaction to the node
|
||
* @property {number} [torPort] Port to send all requests through tor
|
||
* @property {RequestOptions} requestOptions Axios options for network requests (timeout, proxy)
|
||
* @property {string} [multiCallAddress] Address of Multicall contract for selected chain
|
||
* @property {string} [tornadoProxyAddress] Address of Tornado Proxy contract for selected chain
|
||
* @property {string} [tornadoInstanceAddress] Tornado Cash instance contract address for selected pool (chain/currency/value)
|
||
* @property {string} [instanceTokenAddress] Token address for Tornado Cash pool instance for selected token
|
||
* @property {number} [instanceDeployedBlockNumber] Block number in which instance contract was deployed in blockchain
|
||
* @property {string} [signerAddress] Address of signer account (user account, generated from provided private key)
|
||
* @property {TornadoFeeOracleV4 | TornadoFeeOracleV5} [feeOracle] Oracle instance for fetching gas price and calculate network fees (gas) for Tornado transactions
|
||
* @property {Web3Contract} [tornadoInstanceContract] Tornado cash instance contract for selected pool (chain/currency/value)
|
||
* @property {Web3Contract} [tornadoProxyContract] Tornado cash proxy contract for selected chain
|
||
* @property {Web3Contract} [tornadoTokenInstanceContract] Tornado Cash instance contract for selected token pool (for ERC20 token mixing pools, e.g. DAI)
|
||
* @property {Web3Contract} [governanceContract] Tornado Cash Governance contract instance to access staking/voting functionality
|
||
* @property {Web3Contract} [tornTokenContract] TORN token contract instance to approve/send tokens
|
||
* @property {Web3Contract} [stakingRewardsContract] Staking rewards contract to distribute TORN tokens earned by Tornado protocol between TORN stakers
|
||
* @property {string} netName Network (chain) human-readable name
|
||
* @property {string} netSymbol Network main token symbol (ETH for Ethereum mainnet and so on)
|
||
* @property {string} netId Network (chain) ID
|
||
*/
|
||
|
||
/** @type {ProgramGlobals} */
|
||
const globals =
|
||
{
|
||
privateKey: undefined,
|
||
web3Instance: undefined,
|
||
relayerWeb3Instance: undefined,
|
||
useOnlyRpc: false,
|
||
shouldPromptConfirmation: true,
|
||
shouldSubmitTx: true,
|
||
torPort: undefined,
|
||
requestOptions: { timeout: 10000 },
|
||
multiCallAddress: undefined,
|
||
tornadoProxyAddress: undefined,
|
||
tornadoInstanceAddress: undefined,
|
||
instanceTokenAddress: undefined,
|
||
instanceDeployedBlockNumber: undefined,
|
||
signerAddress: undefined,
|
||
feeOracle: undefined,
|
||
tornadoInstanceContract: undefined,
|
||
tornadoProxyContract: undefined,
|
||
governanceContract: undefined,
|
||
tornTokenContract: undefined,
|
||
stakingRewardsContract: undefined,
|
||
netName: "Ethereum",
|
||
netSymbol: "ETH",
|
||
netId: 1
|
||
}
|
||
|
||
/** Generate random number of specified byte length */
|
||
const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes));
|
||
|
||
/** Compute pedersen hash */
|
||
const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0];
|
||
|
||
/** BigNumber to hex string of specified length */
|
||
function toHex(number, length = 32) {
|
||
const str = number instanceof Buffer ? number.toString('hex') : bigInt(number).toString(16);
|
||
return '0x' + str.padStart(length * 2, '0');
|
||
}
|
||
|
||
/** Remove Decimal without rounding with BigNumber */
|
||
function rmDecimalBN(bigNum, decimals = 6) {
|
||
return new BigNumber(bigNum)
|
||
.times(BigNumber(10).pow(decimals))
|
||
.integerValue(BigNumber.ROUND_DOWN)
|
||
.div(BigNumber(10).pow(decimals))
|
||
.toNumber();
|
||
}
|
||
|
||
/** Use MultiCall Contract */
|
||
async function useMultiCall(queryArray) {
|
||
const multiCallABI = require('./abis/Multicall.abi.json');
|
||
const multiCallContract = new globals.web3Instance.Contract(multiCallABI, globals.multiCallAddress);
|
||
const { returnData } = await multiCallContract.methods.aggregate(queryArray).call();
|
||
return returnData;
|
||
}
|
||
|
||
/** Display ETH account balance */
|
||
async function printETHBalance({ address, name }) {
|
||
const { netSymbol, web3Instance } = globals;
|
||
const checkBalance = new BigNumber(await web3Instance.getBalance(address)).div(BigNumber(10).pow(18));
|
||
console.log(`${name} balance is`, rmDecimalBN(checkBalance), `${netSymbol}`);
|
||
}
|
||
|
||
/** Display ERC20 account balance */
|
||
async function printERC20Balance({ address, name, tokenAddress }) {
|
||
const { web3Instance, multiCallAddress } = globals;
|
||
let tokenDecimals, tokenBalance, tokenName, tokenSymbol;
|
||
const erc20Contract = tokenAddress ? new web3Instance.Contract(erc20Abi, tokenAddress) : globals.tornadoTokenInstanceContract;
|
||
if (!multiCallAddress) {
|
||
const tokenCall = await useMultiCall([
|
||
[tokenAddress, erc20Contract.methods.balanceOf(address).encodeABI()],
|
||
[tokenAddress, erc20Contract.methods.decimals().encodeABI()],
|
||
[tokenAddress, erc20Contract.methods.name().encodeABI()],
|
||
[tokenAddress, erc20Contract.methods.symbol().encodeABI()]
|
||
]);
|
||
tokenDecimals = parseInt(tokenCall[1]);
|
||
tokenBalance = new BigNumber(tokenCall[0]).div(BigNumber(10).pow(tokenDecimals));
|
||
tokenName = web3Instance.abi.decodeParameter('string', tokenCall[2]);
|
||
tokenSymbol = web3Instance.abi.decodeParameter('string', tokenCall[3]);
|
||
} else {
|
||
tokenDecimals = await erc20Contract.methods.decimals().call();
|
||
tokenBalance = new BigNumber(await erc20Contract.methods.balanceOf(address).call()).div(BigNumber(10).pow(tokenDecimals));
|
||
tokenName = await erc20Contract.methods.name().call();
|
||
tokenSymbol = await erc20Contract.methods.symbol().call();
|
||
}
|
||
console.log(`${name}`, tokenName, `Balance is`, rmDecimalBN(tokenBalance), tokenSymbol);
|
||
}
|
||
|
||
/**
|
||
* @typedef TreeData
|
||
* @type {Object}
|
||
* @property {string[]} leaves Commitment hashes converted to decimals
|
||
* @property {MerkleTree} tree Builded merkle tree
|
||
* @property {string} root Merkle tree root
|
||
*/
|
||
|
||
/**
|
||
* Compute merkle tree and its root from array of cached deposit events
|
||
* @param {Array} depositEvents Array of deposit event objects
|
||
* @returns {TreeData}
|
||
|
||
*/
|
||
function computeDepositEventsTree(depositEvents) {
|
||
const leaves = depositEvents
|
||
.sort((a, b) => a.leafIndex - b.leafIndex) // Sort events in chronological order
|
||
.map((e) => toBN(e.commitment).toString(10)); // Leaf = commitment pedersen hash of deposit
|
||
|
||
console.log('Computing deposit events merkle tree and its root');
|
||
const merkleTreeHeight = process.env.MERKLE_TREE_HEIGHT || 20;
|
||
const tree = new MerkleTree(merkleTreeHeight, leaves);
|
||
|
||
return { leaves, tree, root: tree.root() };
|
||
}
|
||
|
||
/**
|
||
* Check validity of events merkle tree root via tornado contract
|
||
* @async
|
||
* @param {Array} depositEvents
|
||
* @returns {Promise<boolean>} True, if root is valid, else false
|
||
* @throws {Error}
|
||
*/
|
||
async function isRootValid(depositEvents) {
|
||
const { root } = computeDepositEventsTree(depositEvents);
|
||
const isRootValid = await globals.tornadoInstanceContract.methods.isKnownRoot(toHex(root)).call();
|
||
|
||
return isRootValid;
|
||
}
|
||
|
||
async function submitTransaction(signedTX) {
|
||
console.log('Submitting transaction to the remote node');
|
||
await globals.web3Instance
|
||
.sendSignedTransaction(signedTX)
|
||
.on('transactionHash', function (txHash) {
|
||
console.log(`View transaction on block explorer https://${getExplorerLink()}/tx/${txHash}`);
|
||
})
|
||
.on('error', function (e) {
|
||
console.error('on transactionHash error', e.message);
|
||
});
|
||
}
|
||
|
||
async function generateTransaction(to, encodedData, value = 0, txType = 'other') {
|
||
const { signerAddress, privateKey, netSymbol, netId, web3Instance, shouldPromptConfirmation } = globals;
|
||
const nonce = await web3Instance.getTransactionCount(signerAddress);
|
||
|
||
value = toBN(value);
|
||
|
||
let incompletedTx = {
|
||
to,
|
||
value: value.toString(),
|
||
data: encodedData
|
||
};
|
||
if (txType === 'send') incompletedTx['from'] = signerAddress;
|
||
const { gasPrice, gasLimit } = await globals.feeOracle.getGasParams({ tx: incompletedTx, txType });
|
||
const gasCosts = toBN(gasPrice).mul(toBN(gasLimit));
|
||
const totalCosts = value.add(gasCosts);
|
||
|
||
/** Transaction details */
|
||
console.log('Gas price: ', web3Utils.hexToNumber(gasPrice));
|
||
console.log('Gas limit: ', gasLimit);
|
||
console.log('Transaction fee: ', rmDecimalBN(fromWei(gasCosts), 12), `${netSymbol}`);
|
||
console.log('Transaction cost: ', rmDecimalBN(fromWei(totalCosts), 12), `${netSymbol}`);
|
||
/** ----------------------------------------- **/
|
||
|
||
function txoptions() {
|
||
// Generate EIP-1559 transaction
|
||
if (netId == 1) {
|
||
return {
|
||
to: to,
|
||
value: value,
|
||
nonce: nonce,
|
||
maxFeePerGas: gasPrice,
|
||
maxPriorityFeePerGas: web3Utils.toHex(web3Utils.toWei('3', 'gwei')),
|
||
gas: gasLimit,
|
||
data: encodedData
|
||
};
|
||
} else if (netId == 5 || netId == 137 || netId == 43114) {
|
||
return {
|
||
to: to,
|
||
value: value,
|
||
nonce: nonce,
|
||
maxFeePerGas: gasPrice,
|
||
maxPriorityFeePerGas: gasPrice,
|
||
gas: gasLimit,
|
||
data: encodedData
|
||
};
|
||
} else {
|
||
return {
|
||
to: to,
|
||
value: value,
|
||
nonce: nonce,
|
||
gasPrice: gasPrice,
|
||
gas: gasLimit,
|
||
data: encodedData
|
||
};
|
||
}
|
||
}
|
||
|
||
if (shouldPromptConfirmation) await promptConfirmation();
|
||
|
||
const tx = txoptions();
|
||
const signed = await web3Instance.accounts.signTransaction(tx, privateKey);
|
||
|
||
if (globals.shouldSubmitTx) {
|
||
await submitTransaction(signed.rawTransaction);
|
||
} else {
|
||
console.log('\n=============Raw TX=================', '\n');
|
||
console.log(
|
||
`Please submit this raw tx to https://${getExplorerLink()}/pushTx, or otherwise broadcast with node cli.js broadcast command.`,
|
||
`\n`
|
||
);
|
||
console.log(signed.rawTransaction, `\n`);
|
||
console.log('=====================================', '\n');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create deposit object from secret and nullifier
|
||
*/
|
||
function createDeposit({ nullifier, secret }) {
|
||
let deposit = { nullifier, secret };
|
||
deposit.preimage = Buffer.concat([deposit.nullifier.leInt2Buff(31), deposit.secret.leInt2Buff(31)]);
|
||
deposit.commitment = pedersenHash(deposit.preimage);
|
||
deposit.commitmentHex = toHex(deposit.commitment);
|
||
deposit.nullifierHash = pedersenHash(deposit.nullifier.leInt2Buff(31));
|
||
deposit.nullifierHex = toHex(deposit.nullifierHash);
|
||
return deposit;
|
||
}
|
||
|
||
async function backupNote({ currency, amount, netId, note, noteString }) {
|
||
try {
|
||
fs.writeFileSync(`./backup-tornado-${currency}-${amount}-${netId}-${note.slice(0, 10)}.txt`, noteString, 'utf8');
|
||
console.log('Backed up deposit note as', `./backup-tornado-${currency}-${amount}-${netId}-${note.slice(0, 10)}.txt`);
|
||
} catch (e) {
|
||
throw new Error('Writing backup note failed:', e);
|
||
}
|
||
}
|
||
|
||
async function backupInvoice({ currency, amount, netId, commitmentNote, invoiceString }) {
|
||
try {
|
||
fs.writeFileSync(
|
||
`./backup-tornadoInvoice-${currency}-${amount}-${netId}-${commitmentNote.slice(0, 10)}.txt`,
|
||
invoiceString,
|
||
'utf8'
|
||
);
|
||
console.log(
|
||
'Backed up invoice as',
|
||
`./backup-tornadoInvoice-${currency}-${amount}-${netId}-${commitmentNote.slice(0, 10)}.txt`
|
||
);
|
||
} catch (e) {
|
||
throw new Error('Writing backup invoice failed:', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* create a deposit invoice.
|
||
* @param currency Сurrency
|
||
* @param amount Deposit amount
|
||
*/
|
||
async function createInvoice({ currency, amount, chainId }) {
|
||
const deposit = createDeposit({
|
||
nullifier: rbigint(31),
|
||
secret: rbigint(31)
|
||
});
|
||
const note = toHex(deposit.preimage, 62);
|
||
const noteString = `tornado-${currency}-${amount}-${chainId}-${note}`;
|
||
console.log(`Your note: ${noteString}`);
|
||
|
||
const commitmentNote = toHex(deposit.commitment);
|
||
const invoiceString = `tornadoInvoice-${currency}-${amount}-${chainId}-${commitmentNote}`;
|
||
console.log(`Your invoice for deposit: ${invoiceString}`);
|
||
|
||
await backupNote({ currency, amount, netId: chainId, note, noteString });
|
||
await backupInvoice({ currency, amount, netId: chainId, commitmentNote, invoiceString });
|
||
|
||
return noteString, invoiceString;
|
||
}
|
||
|
||
/**
|
||
* Make a deposit
|
||
* @param currency Сurrency
|
||
* @param amount Deposit amount
|
||
*/
|
||
async function deposit({ currency, amount, commitmentNote }) {
|
||
currency = currency.toLowerCase();
|
||
|
||
const { signerAddress, tornadoProxyAddress, tornadoInstanceAddress, tornadoProxyContract, instanceTokenAddress, tornadoTokenInstanceContract, netSymbol, netId } = globals;
|
||
assert(signerAddress != null, 'Error! Private key not found. Please provide PRIVATE_KEY in .env file or as command argument, if you deposit');
|
||
let commitment, noteString;
|
||
if (!commitmentNote) {
|
||
console.log('Creating new random deposit note');
|
||
const deposit = createDeposit({
|
||
nullifier: rbigint(31),
|
||
secret: rbigint(31)
|
||
});
|
||
const note = toHex(deposit.preimage, 62);
|
||
noteString = `tornado-${currency}-${amount}-${netId}-${note}`;
|
||
console.log(`Your note: ${noteString}`);
|
||
await backupNote({ currency, amount, netId, note, noteString });
|
||
commitment = toHex(deposit.commitment);
|
||
} else {
|
||
console.log('Using supplied invoice for deposit');
|
||
commitment = toHex(commitmentNote);
|
||
}
|
||
if (currency === netSymbol.toLowerCase()) {
|
||
await printETHBalance({ address: tornadoInstanceAddress, name: 'Tornado contract' });
|
||
await printETHBalance({ address: signerAddress, name: 'Sender account' });
|
||
const value = fromDecimals({ amount, decimals: 18 });
|
||
console.log('Submitting deposit transaction');
|
||
await generateTransaction(tornadoProxyAddress, tornadoProxyContract.methods.deposit(tornadoInstanceAddress, commitment, []).encodeABI(), value);
|
||
await printETHBalance({ address: tornadoInstanceAddress, name: 'Tornado contract' });
|
||
await printETHBalance({ address: signerAddress, name: 'Sender account' });
|
||
} else {
|
||
// a token
|
||
await printERC20Balance({ address: tornadoInstanceAddress, name: 'Tornado contract' });
|
||
await printERC20Balance({ address: signerAddress, name: 'Sender account' });
|
||
const decimals = config.deployments[`netId${netId}`]['tokens'][currency].decimals;
|
||
const tokenAmount = fromDecimals({ amount, decimals });
|
||
|
||
const allowance = await tornadoTokenInstanceContract.methods.allowance(signerAddress, tornadoProxyAddress).call({ from: signerAddress });
|
||
console.log('Current allowance is', fromWei(allowance));
|
||
if (toBN(allowance).lt(toBN(tokenAmount))) {
|
||
console.log('Approving tokens for deposit');
|
||
await generateTransaction(tornTokenAddress, tornadoTokenInstanceContract.methods.approve(tornadoProxyAddress, tokenAmount).call(), 0, 'send')
|
||
}
|
||
|
||
console.log('Submitting deposit transaction');
|
||
await generateTransaction(tornadoProxyAddress, tornadoProxyContract.methods.deposit(tornadoInstanceAddress, commitment, []).encodeABI());
|
||
await printERC20Balance({ address: tornadoInstanceAddress, name: 'Tornado contract' });
|
||
await printERC20Balance({ address: signerAddress, name: 'Sender account' });
|
||
}
|
||
|
||
if (!commitmentNote) {
|
||
return noteString;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @typedef {Object} MerkleProof Use pregenerated merkle proof
|
||
* @property {string} root Merkle tree root
|
||
* @property {Array<number|string>} pathElements Number of hashes and hashes
|
||
* @property {Array<number>} pathIndices Indicies
|
||
*/
|
||
|
||
/**
|
||
* Generate merkle tree for a deposit.
|
||
* Download deposit events from the tornado, reconstructs merkle tree, finds our deposit leaf
|
||
* in it and generates merkle proof
|
||
* @param {Object} deposit Deposit object
|
||
* @param {string} currency Currency ticker, like 'ETH' or 'BNB'
|
||
* @param {number} amount Tornado instance amount, like 0.1 (ETH or BNB) or 10
|
||
* @return {Promise<MerkleProof>} Calculated valid merkle tree (proof)
|
||
*/
|
||
async function generateMerkleProof(deposit, currency, amount) {
|
||
const { web3Instance, multiCallAddress, tornadoInstanceContract } = globals;
|
||
|
||
// Get all deposit events from smart contract and assemble merkle tree from them
|
||
const cachedEvents = await fetchEvents({ type: 'deposit', currency, amount });
|
||
const { tree, leaves, root } = computeDepositEventsTree(cachedEvents);
|
||
|
||
// Validate that merkle tree is valid, deposit data is correct and note not spent.
|
||
const leafIndex = leaves.findIndex((commitment) => toBN(deposit.commitmentHex).toString(10) === commitment);
|
||
let isValidRoot, isSpent;
|
||
if (!multiCallAddress) {
|
||
const callContract = await useMultiCall([
|
||
[tornadoInstanceContract._address, tornadoInstanceContract.methods.isKnownRoot(toHex(root)).encodeABI()],
|
||
[tornadoInstanceContract._address, tornadoInstanceContract.methods.isSpent(toHex(deposit.nullifierHash)).encodeABI()]
|
||
]);
|
||
isValidRoot = web3Instance.abi.decodeParameter('bool', callContract[0]);
|
||
isSpent = web3Instance.abi.decodeParameter('bool', callContract[1]);
|
||
} else {
|
||
isValidRoot = await tornadoInstanceContract.methods.isKnownRoot(toHex(root)).call();
|
||
isSpent = await tornadoInstanceContract.methods.isSpent(toHex(deposit.nullifierHash)).call();
|
||
}
|
||
assert(isValidRoot === true, 'Merkle tree is corrupted');
|
||
assert(isSpent === false, 'The note is already spent');
|
||
assert(leafIndex >= 0, 'The deposit is not found in the tree');
|
||
|
||
// Compute merkle proof of our commitment
|
||
const { pathElements, pathIndices } = tree.path(leafIndex);
|
||
return { root, pathElements, pathIndices };
|
||
}
|
||
|
||
/**
|
||
* @typedef {Object} ProofData
|
||
* @property {string} proof - ZK-SNARK proof
|
||
* @property {Array<string>} args - Withdrawal transaction proofed arguments
|
||
*/
|
||
|
||
/**
|
||
* Generate SNARK proof for withdrawal
|
||
* @param {Object} args Arguments
|
||
* @param {Object} args.deposit Deposit object
|
||
* @param {string} args.recipient Funds recipient
|
||
* @param {string | 0 } args.relayer Relayer address
|
||
* @param {number} args.fee Relayer fee
|
||
* @param {string} args.refund Receive ether for exchanged tokens
|
||
* @param {MerkleProof} [args.merkleProof] Valid merkle tree proof
|
||
* @returns {Promise<ProofData>} Proof data
|
||
*/
|
||
async function generateProof({ deposit, currency, amount, recipient, relayerAddress = 0, fee = 0, refund = 0, merkleProof }) {
|
||
// Compute merkle proof of our commitment
|
||
if (merkleProof === undefined)
|
||
merkleProof = await generateMerkleProof(deposit, currency, amount);
|
||
const { root, pathElements, pathIndices } = merkleProof;
|
||
|
||
// Prepare circuit input
|
||
const input =
|
||
{
|
||
// Public snark inputs
|
||
root: root,
|
||
nullifierHash: deposit.nullifierHash,
|
||
recipient: bigInt(recipient),
|
||
relayer: bigInt(relayerAddress),
|
||
fee: bigInt(fee),
|
||
refund: bigInt(refund),
|
||
|
||
// Private snark inputs
|
||
nullifier: deposit.nullifier,
|
||
secret: deposit.secret,
|
||
pathElements: pathElements,
|
||
pathIndices: pathIndices
|
||
};
|
||
|
||
console.log('Generating SNARK proof');
|
||
console.time('Proof time');
|
||
// groth16 initialises a lot of Promises that will never be resolved, that's why we need to use process.exit to terminate the CLI
|
||
const groth16 = await buildGroth16();
|
||
const circuit = require('./circuits/tornado.json');
|
||
const provingKey = fs.readFileSync('./circuits/tornadoProvingKey.bin').buffer;
|
||
const proofData = await websnarkUtils.genWitnessAndProve(groth16, input, circuit, provingKey);
|
||
const { proof } = websnarkUtils.toSolidityInput(proofData);
|
||
console.timeEnd('Proof time');
|
||
|
||
const args = [
|
||
toHex(input.root),
|
||
toHex(input.nullifierHash),
|
||
toHex(input.recipient, 20),
|
||
toHex(input.relayer, 20),
|
||
toHex(input.fee),
|
||
toHex(input.refund)
|
||
];
|
||
|
||
return { proof, args };
|
||
}
|
||
|
||
/**
|
||
* Do an ETH withdrawal
|
||
* @param noteString Note to withdraw
|
||
* @param recipient Recipient address
|
||
*/
|
||
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund, privateKey }) {
|
||
const { web3Instance, signerAddress, tornadoProxyAddress, requestOptions, feeOracle, tornadoInstanceAddress, tornadoProxyContract, netSymbol, netId, shouldPromptConfirmation } = globals;
|
||
if (currency === netSymbol.toLowerCase() && refund && refund !== '0') {
|
||
throw new Error('The ETH purchase is supposed to be 0 for ETH withdrawals');
|
||
}
|
||
|
||
if (!isNaN(Number(refund)))
|
||
refund = toWei(refund, 'ether');
|
||
else
|
||
refund = toBN(await feeOracle.fetchRefundInETH(currency.toLowerCase()));
|
||
|
||
if (!web3Utils.isAddress(recipient)) {
|
||
throw new Error('Recipient address is not valid');
|
||
}
|
||
|
||
const depositInfo = await loadDepositData({ amount, currency, deposit });
|
||
const allDeposits = loadCachedEvents({ type: "deposit", currency, amount });
|
||
if ((depositInfo.leafIndex > allDeposits[allDeposits.length - 1].leafIndex - 10)
|
||
&& allDeposits.length > 10) {
|
||
console.log("\nWARNING: you're trying to withdraw your deposit too early, there are not enough subsequent deposits to ensure good anonymity level. Read: https://docs.tornado.ws/general/guides/opsec.html");
|
||
if (shouldPromptConfirmation)
|
||
await promptConfirmation("Continue withdrawal with risks to anonymity? [Y/n]: ")
|
||
}
|
||
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit });
|
||
if (withdrawInfo) {
|
||
console.error("\nError: note has already been withdrawn. Use `compliance` command to check deposit and withdrawal info.\n");
|
||
process.exit(1);
|
||
}
|
||
|
||
if (global.chainId !== 1 && (privateKey || globals.privateKey)) {
|
||
// using private key
|
||
|
||
// 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.
|
||
assert
|
||
(
|
||
recipient.toLowerCase() == signerAddress.toLowerCase(),
|
||
'Withdrawal recepient mismatches with the account of provided private key from environment file'
|
||
);
|
||
const checkBalance = await web3Instance.getBalance(signerAddress);
|
||
assert
|
||
(
|
||
checkBalance !== 0,
|
||
'You have 0 balance, make sure to fund account by withdrawing from tornado using relayer first'
|
||
);
|
||
|
||
const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund });
|
||
|
||
console.log('Submitting withdraw transaction');
|
||
await generateTransaction
|
||
(
|
||
tornadoProxyAddress,
|
||
tornadoProxyContract.methods.withdraw(tornadoInstanceAddress, proof, ...args).encodeABI(),
|
||
toBN(args[5]),
|
||
'user_withdrawal'
|
||
);
|
||
}
|
||
else {
|
||
let relayerInfo;
|
||
if (relayerURL) {
|
||
try {
|
||
relayerURL = new URL(relayerURL).origin;
|
||
res = await axios.get(relayerURL + '/status', requestOptions);
|
||
relayerInfo = res.data;
|
||
} catch (err) {
|
||
console.error(err);
|
||
throw new Error('Cannot get relayer status');
|
||
}
|
||
}
|
||
else {
|
||
const availableRelayers = await getRelayers(netId);
|
||
if (availableRelayers.length === 0) throw new Error("Cannot automatically pick a relayer to withdraw your note. Provide relayer manually with `--relayer` cmd option or use private key withdrawal")
|
||
relayerInfo = pickWeightedRandomRelayer(availableRelayers);
|
||
relayerURL = "https://" + relayerInfo.hostname
|
||
console.log(`Selected relayer: ${relayerURL}`)
|
||
}
|
||
|
||
|
||
|
||
const { rewardAccount, netId: relayerNetId, ethPrices, tornadoServiceFee } = relayerInfo;
|
||
assert(relayerNetId === (await web3Instance.net.getId()) || relayerNetId === '*', 'This relay is for different network');
|
||
console.log('Relay address:', rewardAccount);
|
||
|
||
const decimals = config.deployments[`netId${netId}`]['tokens'][currency].decimals;
|
||
|
||
const merkleWithdrawalProof = await generateMerkleProof(deposit, currency, amount);
|
||
|
||
async function calculateDataForRelayer(totalRelayerFee = 0) {
|
||
const { proof, args } = await generateProof({
|
||
deposit,
|
||
currency,
|
||
amount,
|
||
recipient,
|
||
relayerAddress: rewardAccount,
|
||
fee: toBN(totalRelayerFee),
|
||
refund,
|
||
merkleProof: merkleWithdrawalProof
|
||
});
|
||
return { proof, args };
|
||
}
|
||
|
||
const relayerFee = feeOracle.calculateRelayerFeeInWei(tornadoServiceFee, amount, decimals);
|
||
const { proof: dummyProof, args: dummyArgs } = await calculateDataForRelayer(relayerFee);
|
||
|
||
const withdrawalTxCalldata = tornadoProxyContract.methods.withdraw(tornadoProxyAddress, dummyProof, ...dummyArgs);
|
||
const incompleteWithdrawalTx = {
|
||
to: tornadoProxyAddress,
|
||
data: withdrawalTxCalldata,
|
||
value: toBN(dummyArgs[5]) || 0
|
||
};
|
||
|
||
const totalWithdrawalFeeViaRelayer = await feeOracle.calculateWithdrawalFeeViaRelayer({
|
||
tx: incompleteWithdrawalTx,
|
||
txType: 'user_withdrawal',
|
||
relayerFeePercent: tornadoServiceFee,
|
||
currency,
|
||
amount,
|
||
decimals,
|
||
refund,
|
||
tokenPriceInEth: ethPrices?.[currency]
|
||
});
|
||
|
||
const { proof, args } = await calculateDataForRelayer(totalWithdrawalFeeViaRelayer);
|
||
|
||
console.log('Sending withdraw transaction through relay');
|
||
|
||
/** Relayer fee details **/
|
||
console.log('Relayer fee: ', rmDecimalBN(fromWei(toBN(relayerFee)), 12), `${currency.toUpperCase()}`);
|
||
console.log('Total fees: ', rmDecimalBN(fromWei(toBN(totalWithdrawalFeeViaRelayer)), 12), `${currency.toUpperCase()}`);
|
||
const toReceive = toBN(fromDecimals({ amount, decimals })).sub(toBN(totalWithdrawalFeeViaRelayer));
|
||
console.log(
|
||
'Amount to receive: ',
|
||
rmDecimalBN(fromWei(toReceive), 12),
|
||
`${currency.toUpperCase()}`,
|
||
toBN(refund).gt(toBN(0)) ? ` + ${rmDecimalBN(fromWei(refund), 12)} ${netSymbol}` : ''
|
||
);
|
||
/** -------------------- **/
|
||
|
||
if (globals.shouldPromptConfirmation) await promptConfirmation();
|
||
|
||
try {
|
||
const response = await axios.post(
|
||
relayerURL + '/v1/tornadoWithdraw',
|
||
{
|
||
contract: tornadoInstanceAddress,
|
||
proof,
|
||
args
|
||
},
|
||
requestOptions
|
||
);
|
||
|
||
const { id } = response.data;
|
||
|
||
const result = await getStatus(id, relayerURL, requestOptions);
|
||
console.log('STATUS', result);
|
||
} catch (e) {
|
||
console.error(e.message);
|
||
}
|
||
}
|
||
|
||
if (currency === netSymbol.toLowerCase()) {
|
||
await printETHBalance({ address: recipient, name: 'Recipient' });
|
||
}
|
||
else {
|
||
await printERC20Balance({ address: recipient, name: 'Recipient' });
|
||
}
|
||
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 }) {
|
||
const { web3Instance, signerAddress, feeOracle, multiCallAddress, netSymbol, netId } = globals;
|
||
|
||
// using private key
|
||
assert(signerAddress != null, 'Error! Private key not found. Please provide PRIVATE_KEY in .env file if you send');
|
||
if (tokenAddress) {
|
||
const erc20Contract = new web3Instance.Contract(erc20Abi, tokenAddress);
|
||
let tokenBalance, tokenDecimals, tokenSymbol;
|
||
if (multiCallAddress) {
|
||
const callToken = await useMultiCall([
|
||
[tokenAddress, erc20Contract.methods.balanceOf(signerAddress).encodeABI()],
|
||
[tokenAddress, erc20Contract.methods.decimals().encodeABI()],
|
||
[tokenAddress, erc20Contract.methods.symbol().encodeABI()]
|
||
]);
|
||
tokenBalance = new BigNumber(callToken[0]);
|
||
tokenDecimals = parseInt(callToken[1]);
|
||
tokenSymbol = web3Instance.abi.decodeParameter('string', callToken[2]);
|
||
} else {
|
||
tokenBalance = new BigNumber(await erc20Contract.methods.balanceOf(signerAddress).call());
|
||
tokenDecimals = await erc20Contract.methods.decimals().call();
|
||
tokenSymbol = await erc20Contract.methods.symbol().call();
|
||
}
|
||
const toSend = new BigNumber(amount).times(BigNumber(10).pow(tokenDecimals));
|
||
if (tokenBalance.lt(toSend)) {
|
||
console.error(
|
||
'You have',
|
||
rmDecimalBN(tokenBalance.div(BigNumber(10).pow(tokenDecimals))),
|
||
tokenSymbol,
|
||
", you can't send more than you have"
|
||
);
|
||
process.exit(1);
|
||
}
|
||
const encodeTransfer = erc20Contract.methods.transfer(address, toSend).encodeABI();
|
||
await generateTransaction(tokenAddress, encodeTransfer, 0, 'send');
|
||
console.log('Sent', amount, tokenSymbol, 'to', address);
|
||
} else {
|
||
const balance = new BigNumber(await web3Instance.getBalance(signerAddress));
|
||
assert(balance.toNumber() !== 0, "You have 0 balance, can't send transaction");
|
||
let toSend = new BigNumber(0);
|
||
if (amount) {
|
||
toSend = new BigNumber(amount).times(BigNumber(10).pow(18));
|
||
if (balance.lt(toSend)) {
|
||
console.error(
|
||
'You have',
|
||
rmDecimalBN(balance.div(BigNumber(10).pow(18))),
|
||
netSymbol + ", you can't send more than you have."
|
||
);
|
||
process.exit(1);
|
||
}
|
||
} else {
|
||
console.log('Amount not defined, sending all available amounts');
|
||
const gasPrice = new BigNumber(await feeOracle.getGasPrice('other'));
|
||
const gasLimit = new BigNumber(21000);
|
||
if (netId == 1) {
|
||
const priorityFee = new BigNumber(await gasPrices(3));
|
||
toSend = balance.minus(gasLimit.times(gasPrice.plus(priorityFee)));
|
||
} else {
|
||
toSend = balance.minus(gasLimit.times(gasPrice));
|
||
}
|
||
}
|
||
await generateTransaction(address, null, toSend);
|
||
console.log('Sent', rmDecimalBN(toSend.div(BigNumber(10).pow(18))), netSymbol, 'to', address);
|
||
}
|
||
}
|
||
|
||
function getStatus(id, relayerURL, options) {
|
||
return new Promise((resolve) => {
|
||
async function getRelayerStatus() {
|
||
const responseStatus = await axios.get(relayerURL + '/v1/jobs/' + id, options);
|
||
|
||
if (responseStatus.status === 200) {
|
||
const { txHash, status, confirmations, failedReason } = responseStatus.data;
|
||
|
||
console.log(`Current job status ${status}, confirmations: ${confirmations}`);
|
||
|
||
if (status === 'FAILED') {
|
||
throw new Error(status + ' failed reason:' + failedReason);
|
||
}
|
||
|
||
if (status === 'CONFIRMED') {
|
||
const receipt = await waitForTxReceipt({ txHash });
|
||
console.log(
|
||
`Transaction submitted through the relay. View transaction on block explorer https://${getExplorerLink()}/tx/${txHash}`
|
||
);
|
||
console.log('Transaction mined in block', receipt.blockNumber);
|
||
resolve(status);
|
||
}
|
||
}
|
||
|
||
setTimeout(() => {
|
||
getRelayerStatus(id, relayerURL);
|
||
}, 3000);
|
||
}
|
||
|
||
getRelayerStatus();
|
||
});
|
||
}
|
||
|
||
function capitalizeFirstLetter(string) {
|
||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||
}
|
||
|
||
function fromDecimals({ amount, decimals }) {
|
||
amount = amount.toString();
|
||
let ether = amount.toString();
|
||
const base = new BN('10').pow(new BN(decimals));
|
||
const baseLength = base.toString(10).length - 1 || 1;
|
||
|
||
const negative = ether.substring(0, 1) === '-';
|
||
if (negative) {
|
||
ether = ether.substring(1);
|
||
}
|
||
|
||
if (ether === '.') {
|
||
throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, invalid value');
|
||
}
|
||
|
||
// Split it into a whole and fractional part
|
||
const comps = ether.split('.');
|
||
if (comps.length > 2) {
|
||
throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points');
|
||
}
|
||
|
||
let whole = comps[0];
|
||
let fraction = comps[1];
|
||
|
||
if (!whole) {
|
||
whole = '0';
|
||
}
|
||
if (!fraction) {
|
||
fraction = '0';
|
||
}
|
||
if (fraction.length > baseLength) {
|
||
throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places');
|
||
}
|
||
|
||
while (fraction.length < baseLength) {
|
||
fraction += '0';
|
||
}
|
||
|
||
whole = new BN(whole);
|
||
fraction = new BN(fraction);
|
||
let wei = whole.mul(base).add(fraction);
|
||
|
||
if (negative) {
|
||
wei = wei.mul(negative);
|
||
}
|
||
|
||
return new BN(wei.toString(10), 10);
|
||
}
|
||
|
||
function toDecimals(value, decimals, fixed) {
|
||
const zero = new BN(0);
|
||
const negative1 = new BN(-1);
|
||
decimals = decimals || 18;
|
||
fixed = fixed || 7;
|
||
|
||
value = new BN(value);
|
||
const negative = value.lt(zero);
|
||
const base = new BN('10').pow(new BN(decimals));
|
||
const baseLength = base.toString(10).length - 1 || 1;
|
||
|
||
if (negative) {
|
||
value = value.mul(negative1);
|
||
}
|
||
|
||
let fraction = value.mod(base).toString(10);
|
||
while (fraction.length < baseLength) {
|
||
fraction = `0${fraction}`;
|
||
}
|
||
fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1];
|
||
|
||
const whole = value.div(base).toString(10);
|
||
value = `${whole}${fraction === '0' ? '' : `.${fraction}`}`;
|
||
|
||
if (negative) {
|
||
value = `-${value}`;
|
||
}
|
||
|
||
if (fixed) {
|
||
value = value.slice(0, fixed);
|
||
}
|
||
|
||
return value;
|
||
}
|
||
|
||
// List fetched from https://github.com/ethereum-lists/chains/blob/master/_data/chains
|
||
function getExplorerLink() {
|
||
switch (globals.netId) {
|
||
case 61:
|
||
return 'etc.blockscout.com';
|
||
case 11155111:
|
||
return 'sepolia.etherscan.io';
|
||
case 56:
|
||
return 'bscscan.com';
|
||
case 100:
|
||
return 'blockscout.com/poa/xdai';
|
||
case 137:
|
||
return 'polygonscan.com';
|
||
case 42161:
|
||
return 'arbiscan.io';
|
||
case 43114:
|
||
return 'snowtrace.io';
|
||
case 5:
|
||
return 'goerli.etherscan.io';
|
||
case 42:
|
||
return 'kovan.etherscan.io';
|
||
case 10:
|
||
return 'optimistic.etherscan.io';
|
||
default:
|
||
return 'etherscan.io';
|
||
}
|
||
}
|
||
|
||
// List fetched from https://github.com/trustwallet/assets/tree/master/blockchains
|
||
function getCurrentNetworkName() {
|
||
switch (globals.netId) {
|
||
case 61:
|
||
return 'EthereumClassic';
|
||
case 11155111:
|
||
return 'Sepolia';
|
||
case 56:
|
||
return 'BinanceSmartChain';
|
||
case 100:
|
||
return 'GnosisChain';
|
||
case 137:
|
||
return 'Polygon';
|
||
case 42161:
|
||
return 'Arbitrum';
|
||
case 43114:
|
||
return 'Avalanche';
|
||
case 42:
|
||
return 'Kovan';
|
||
case 10:
|
||
return 'Optimism';
|
||
default:
|
||
return 'Ethereum';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get native currency symbol for selected chain
|
||
* @param {number | string} chainId
|
||
* @returns {string}
|
||
*/
|
||
function getCurrentNetworkSymbol(chainId) {
|
||
switch (Number(chainId)) {
|
||
case 61:
|
||
return 'ETC';
|
||
case 56:
|
||
return 'BNB';
|
||
case 100:
|
||
return 'xDAI';
|
||
case 137:
|
||
return 'MATIC';
|
||
case 43114:
|
||
return 'AVAX';
|
||
default:
|
||
return 'ETH';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Waits for transaction to be mined
|
||
* @param txHash Hash of transaction
|
||
* @param attempts
|
||
* @param delay
|
||
*/
|
||
async function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
|
||
let retryAttempt = 0;
|
||
while (retryAttempt < attempts) {
|
||
const result = await globals.web3Instance.getTransactionReceipt(txHash);
|
||
if (!result?.blockNumber) {
|
||
retryAttempt++;
|
||
await sleep(delay);
|
||
continue;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
throw new Error(`Cannot get transaction receipt in ${attempts} retry attempts`);
|
||
}
|
||
|
||
/**
|
||
* Select one of default RPCs that works correctly
|
||
* @param {number | string} chainId
|
||
* @param {EventType} eventType Possible type of events in Tornado: deposit, withdrawal or fetch relayers
|
||
* @param {boolean} [isSubgraphAvailable=false] If subgraph for required user action is already available, we can lower requirements for RPC (for example,
|
||
* if we can fetch all deposit events from subgraph, we shouldn't require archive node)
|
||
* @returns {Promise<string>} Full RPC link
|
||
*/
|
||
async function selectDefaultRpc(chainId, eventType, isSubgraphAvailable = false) {
|
||
const candidates = config.deployments[`netId${chainId}`].defaultRpcs;
|
||
|
||
for (const candidate of candidates) {
|
||
const localWeb3 = await createWeb3Instance(candidate);
|
||
|
||
try {
|
||
if (!(await localWeb3.net.isListening())) throw new Error('Cannot connect to websocket provider');
|
||
|
||
if (eventType === "relayer") {
|
||
const relayerRegistryContract = new localWeb3.Contract(relayerRegistryAbi, relayerRegistryAddress);
|
||
const registeredRelayers = loadCachedEvents({ type: "relayer" });
|
||
|
||
if (registeredRelayers.length > 0) {
|
||
const relayerAggregatorContract = new localWeb3.Contract(relayerAggregatorAbi, relayerAggregatorAddress);
|
||
const relayerNameHashes = registeredRelayers.map(r => r.ensHash);
|
||
// Here it checks RPC returndata size limit: when getRelayer function aggregates onchain data for all relayers, returndata size will be big
|
||
await relayerAggregatorContract.methods.relayersData(relayerNameHashes, relayerSubdomains).call();
|
||
}
|
||
|
||
const lastBlock = await localWeb3.getBlockNumber();
|
||
const lastCachedBlock = registeredRelayers.length > 0 ? registeredRelayers[registeredRelayers.length - 1].blockNumber : relayerRegistryDeployedBlockNumber;
|
||
const fromBlock = isSubgraphAvailable ? lastBlock - 1000 : lastCachedBlock;
|
||
const toBlock = isSubgraphAvailable ? lastBlock : lastCachedBlock + 1000;
|
||
await relayerRegistryContract.getPastEvents("RelayerRegistered", { fromBlock, toBlock });
|
||
}
|
||
else if (eventType === "withdrawal") {
|
||
const oldTransactionHash = config.deployments[`netId${chainId}`].firstDeploymentTransaction;
|
||
const testReceipt = await localWeb3.getTransactionReceipt(oldTransactionHash);
|
||
|
||
const netSymbol = getCurrentNetworkSymbol(chainId).toLowerCase();
|
||
const [tornadoInstanceAmount, tornadoInstanceAddress] = Object.entries(config.deployments[`netId${chainId}`]['tokens'][netSymbol].instanceAddress)[0];;
|
||
const instanceDeployedBlockNumber = config.deployments[`netId${chainId}`]['tokens'][netSymbol].deployedBlockNumber[tornadoInstanceAmount];
|
||
const tornadoInstanceContract = new localWeb3.Contract(tornadoInstanceAbi, tornadoInstanceAddress);
|
||
|
||
if (!testReceipt) throw new Error("RPC cannot get receipt of old transaction");
|
||
const lastBlock = await localWeb3.getBlockNumber();
|
||
const fromBlock = isSubgraphAvailable ? lastBlock - 1000 : instanceDeployedBlockNumber;
|
||
const toBlock = isSubgraphAvailable ? lastBlock : instanceDeployedBlockNumber + 1000;
|
||
await tornadoInstanceContract.getPastEvents("Deposit", { fromBlock, toBlock });
|
||
}
|
||
else await localWeb3.getBlockNumber();
|
||
|
||
console.log("Selected RPC: " + candidate);
|
||
return candidate;
|
||
} catch (e) {
|
||
console.log(e)
|
||
}
|
||
}
|
||
|
||
throw new Error("All default RPC cannot be used, provide a working one");
|
||
}
|
||
|
||
/**
|
||
* Select one of default subgraphs that works correctly
|
||
* @param {number | string } chainId
|
||
* @param {EventType} eventType Possible type of events in Tornado: deposit, withdrawal or fetch relayers
|
||
* @returns {Promise<string>} Full subgraph link
|
||
*/
|
||
async function selectDefaultGraph(chainId, eventType) {
|
||
let candidates = config.deployments[`netId${chainId}`].subgraphs;
|
||
if (eventType === "relayer") {
|
||
if (chainId != 1) throw new Error("Relayer subgraph is available only for mainnet");
|
||
candidates = config.deployments[`netId${chainId}`].relayerSubgraphs;
|
||
query = '{ relayers(first: 10) { address, ensName, ensHash, blockRegistration } }'
|
||
}
|
||
else if (eventType === "deposit") query = `{ deposits(first: 1, orderBy: timestamp) { blockNumber, index } }`;
|
||
else query = `{ withdrawals(first: 1, orderBy: timestamp) { timestamp } }`
|
||
|
||
for (const candidate of candidates) {
|
||
try {
|
||
const response = await axios.post(candidate, { query }, globals.requestOptions);
|
||
const result = response.data.data[`${eventType}s`];
|
||
if (!result) throw new Error("Invalid response from subgraph");
|
||
console.log(`Selected subgraph for ${eventType}s - ${candidate}`);
|
||
return candidate;
|
||
} catch (e) {
|
||
console.log(e)
|
||
}
|
||
}
|
||
|
||
console.log(`There is no available subgraph for ${eventType}s`);
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* Get available relayers data for selected chain
|
||
* @param {string | number} chainId
|
||
* @returns {Promise<Array<Object>>} List of available relayers
|
||
*/
|
||
async function getRelayers(chainId) {
|
||
console.log("Fetching relayers...");
|
||
|
||
const MIN_STAKE_LISTED_BALANCE = '0X1B1AE4D6E2EF500000'; // 500 TORN
|
||
const aggregator = new globals.relayerWeb3Instance.Contract(relayerAggregatorAbi, relayerAggregatorAddress);
|
||
const ensSubdomainKey = config.deployments[`netId${chainId}`].ensSubdomainKey;
|
||
|
||
function filterRelayers(acc, curr, ensSubdomainKey, relayer) {
|
||
const subdomainIndex = relayerSubdomains.indexOf(ensSubdomainKey);
|
||
const mainnetSubdomain = curr.records[0];
|
||
const hostname = curr.records[subdomainIndex];
|
||
const isHostWithProtocol = hostname.includes('http');
|
||
|
||
const isOwner = relayer.address.toLowerCase() === curr.owner.toLowerCase();
|
||
const hasMinBalance = new BigNumber(curr.balance).gte(MIN_STAKE_LISTED_BALANCE);
|
||
|
||
if (
|
||
hostname &&
|
||
isOwner &&
|
||
mainnetSubdomain &&
|
||
curr.isRegistered &&
|
||
hasMinBalance &&
|
||
!isHostWithProtocol
|
||
) {
|
||
acc.push({
|
||
hostname,
|
||
ensName: relayer.ensName,
|
||
stakeBalance: curr.balance,
|
||
relayerAddress: relayer.address.toLowerCase()
|
||
});
|
||
}
|
||
return acc;
|
||
}
|
||
|
||
async function getValidRelayers(relayers, ensSubdomainKey) {
|
||
const relayerNameHashes = relayers.map((r) => r.ensHash);
|
||
const relayersData = await aggregator.methods.relayersData(relayerNameHashes, relayerSubdomains).call();
|
||
const validRelayers = relayersData.reduce(
|
||
(acc, curr, index) => filterRelayers(acc, curr, ensSubdomainKey, relayers[index]),
|
||
[]
|
||
);
|
||
return validRelayers;
|
||
}
|
||
|
||
async function getAvailableRelayersData(relayers) {
|
||
let statuses = [];
|
||
for (const relayer of relayers) {
|
||
try {
|
||
const res = await axios.get(`https://${relayer.hostname}/status`, globals.requestOptions);
|
||
const statusData = res.data;
|
||
if (statusData.rewardAccount && statusData.health.status == 'true') {
|
||
statuses.push({
|
||
...relayer,
|
||
...statusData
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// console.error(`Failed to fetch status for ${relayer.hostname}:`);
|
||
}
|
||
}
|
||
|
||
return statuses;
|
||
}
|
||
|
||
const registeredRelayers = await fetchEvents({ type: "relayer" });
|
||
// Some relayers can be unregistered and then registrered again
|
||
const deduplicatedRelayers = registeredRelayers.filter((relayer, index, relayers) => index === relayers.findIndex(r => relayer.ensName === r.ensName));
|
||
const validRelayers = await getValidRelayers(deduplicatedRelayers, ensSubdomainKey);
|
||
const availableRelayersData = await getAvailableRelayersData(validRelayers);
|
||
|
||
console.log(`Found ${availableRelayersData.length} available relayers`)
|
||
|
||
return availableRelayersData;
|
||
}
|
||
|
||
/**
|
||
* Select random relayer from provided list using formula from Tornado Cash docs: https://docs.tornado.ws/general/guides/relayer.html
|
||
* @param {Array<Object>} relayers List of relayers
|
||
* @returns {Object} One selected relayer
|
||
*/
|
||
function pickWeightedRandomRelayer(relayers) {
|
||
function calculateScore({ stakeBalance, tornadoServiceFee }, minFee = 0.33, maxFee = 0.53) {
|
||
if (tornadoServiceFee < minFee) {
|
||
tornadoServiceFee = minFee
|
||
} else if (tornadoServiceFee >= maxFee) {
|
||
return new BigNumber(0)
|
||
}
|
||
const serviceFeeCoefficient = (tornadoServiceFee - minFee) ** 2
|
||
const feeDiffCoefficient = 1 / (maxFee - minFee) ** 2
|
||
const coefficientsMultiplier = 1 - feeDiffCoefficient * serviceFeeCoefficient
|
||
|
||
return new BigNumber(stakeBalance).multipliedBy(coefficientsMultiplier)
|
||
}
|
||
|
||
function getWeightRandom(weightsScores, random) {
|
||
for (let i = 0; i < weightsScores.length; i++) {
|
||
if (random.isLessThan(weightsScores[i])) {
|
||
return i
|
||
}
|
||
random = random.minus(weightsScores[i])
|
||
}
|
||
return Math.floor(Math.random() * weightsScores.length)
|
||
}
|
||
|
||
|
||
let minFee, maxFee
|
||
|
||
if (globals.netId != 1) {
|
||
minFee = 0.01
|
||
maxFee = 0.3
|
||
}
|
||
|
||
const weightsScores = relayers.map((el) => calculateScore(el, minFee, maxFee))
|
||
const totalWeight = weightsScores.reduce((acc, curr) => {
|
||
return (acc = acc.plus(curr))
|
||
}, new BigNumber('0'))
|
||
|
||
const random = totalWeight.multipliedBy(Math.random())
|
||
const weightRandomIndex = getWeightRandom(weightsScores, random)
|
||
|
||
return relayers[weightRandomIndex]
|
||
}
|
||
|
||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||
|
||
/**
|
||
* Send post request several times until successful response (200-300 https statuses) until the allowed number of attempts is used up
|
||
* @param {string} url Post requets url
|
||
* @param {Object} data Post request data
|
||
* @param {RequestOptions} requestOptions Post request connection options (timeout, proxy)
|
||
* @param {number} retryAttempts Retry attempts count, it limits the maximum number of requests
|
||
* @param {number} waitingTimeIncrease In milliseconds. The waiting time is increased before each new attempt, (this value) * (current retry attempt number)
|
||
* @returns {Promise<any>} Axios reonse
|
||
*/
|
||
async function retryPostRequest(url, data, requestOptions, retryAttempts, waitingTimeIncrease = 2000) {
|
||
let retryAttempt = 0;
|
||
while (1) {
|
||
await sleep(waitingTimeIncrease * retryAttempt);
|
||
try {
|
||
return await axios.post(url, data, requestOptions);
|
||
} catch (e) {
|
||
if (retryAttempt === retryAttempts) throw e;
|
||
retryAttempt++;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get file path for cached events flile
|
||
* @param {EventType} eventType Event type
|
||
* @param {string} currency Tornado instance currency symbol
|
||
* @param {number | string} amount Tornado instance currency amount
|
||
* @returns {string} Path to cache file
|
||
*/
|
||
function getEventsFilePath(eventType, currency, amount) {
|
||
return eventType === "relayer" ? "./cache/relayer/register.json" : `./cache/${globals.netName.toLowerCase()}/${eventType}s_${currency.toLowerCase()}_${amount}.json`;
|
||
}
|
||
|
||
/**
|
||
* Load events from cache file
|
||
* @param {Object} args
|
||
* @param {EventType} args.type Event type
|
||
* @param {string} args.currency Tornado instance currency symbol
|
||
* @param {string | number} amount Tornado instance currency amount
|
||
* @returns {Array} Cached events
|
||
*/
|
||
function loadCachedEvents({ type, currency, amount }) {
|
||
try {
|
||
const events = JSON.parse(fs.readFileSync(getEventsFilePath(type, currency, amount)));
|
||
|
||
if (!events || events.length === 0) throw new Error("Invalid cached events file")
|
||
return events;
|
||
} catch (err) {
|
||
console.log(`Error fetching cached ${type} events from file`);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update cache for events of selected type (currency and amount also, if applicable) to actual blockchain state
|
||
* @param {Object} args
|
||
* @param {EventType} args.type Events type
|
||
* @param {string} [args.currency] Currency to select Tornado pool instance from which it should fetch events (for deposits and withdrawals)
|
||
* @param {number} [args.amount] Amount to select Tornado pool instance from which it should fetch events (for deposits and withdrawals)
|
||
* @returns {Promise<Array<Object>>} All events (cached and newly fetched)
|
||
*/
|
||
async function fetchEvents({ type, currency, amount }) {
|
||
if (currency) currency = currency.toLowerCase();
|
||
if (type === 'withdraw') {
|
||
type = 'withdrawal';
|
||
}
|
||
|
||
const { netName, netId, instanceDeployedBlockNumber, useOnlyRpc, tornadoInstanceContract } = globals;
|
||
const web3Instance = type === 'relayer' ? globals.relayerWeb3Instance : globals.web3Instance;
|
||
const subgraph = useOnlyRpc ? null : await selectDefaultGraph(type === 'relayer' ? 1 : netId, type);
|
||
|
||
const cachedEvents = loadCachedEvents({ type, currency, amount });
|
||
const startBlock = cachedEvents.length ? cachedEvents[cachedEvents.length - 1].blockNumber + 1 : (type === "relayer" ? relayerRegistryDeployedBlockNumber : instanceDeployedBlockNumber);
|
||
|
||
if (type !== "relayer") {
|
||
console.log('Loaded cached', amount, currency.toUpperCase(), type, 'events for', startBlock, 'block');
|
||
console.log('Fetching', amount, currency.toUpperCase(), type, 'events for', netName, 'network');
|
||
}
|
||
|
||
/**
|
||
* Updates local events cache file for one Tornado cash instance, for example, deposit events for 1 ETH pool
|
||
* @param {Array<Object>} fetchedEvents Array of new events fetched from RPC or Graph
|
||
*/
|
||
async function updateCache(fetchedEvents) {
|
||
if (type === 'deposit') fetchedEvents.sort((firstLeaf, secondLeaf) => firstLeaf.leafIndex - secondLeaf.leafIndex);
|
||
if (type === 'relayer') fetchedEvents.sort((first, second) => first.blockRegistration - second.blockRegistration);
|
||
|
||
try {
|
||
const cachedEvents = loadCachedEvents({ type, currency, amount });
|
||
const events = cachedEvents.concat(fetchedEvents);
|
||
fs.writeFileSync(getEventsFilePath(type, currency, amount), JSON.stringify(events, null, 2), { flag: 'w+', encoding: 'utf-8' });
|
||
} catch (error) {
|
||
console.log(error)
|
||
throw new Error('Writing cache file failed:', error);
|
||
}
|
||
}
|
||
|
||
async function syncEvents() {
|
||
try {
|
||
const targetBlock = await web3Instance.getBlockNumber();
|
||
const chunks = 1000;
|
||
const contract = type === "relayer" ? new web3Instance.Contract(relayerRegistryAbi, relayerRegistryAddress) : tornadoInstanceContract;
|
||
const eventNameInContract = type === "relayer" ? "RelayerRegistered" : capitalizeFirstLetter(type);
|
||
console.log('Querying latest events from RPC');
|
||
|
||
for (let i = startBlock; i < targetBlock; i += chunks) {
|
||
let mapFunction;
|
||
if (type === "relayer")
|
||
mapFunction = ({ blockNumber, returnValues: { relayer, relayerAddress, ensName } }) => ({ blockNumber, ensHash: relayer, ensName, address: relayerAddress });
|
||
else if (type === "deposit")
|
||
mapFunction = ({ blockNumber, transactionHash, returnValues: { commitment, leafIndex, timestamp } }) =>
|
||
({ blockNumber, transactionHash, commitment, leafIndex: Number(leafIndex), timestamp: Number(timestamp) });
|
||
else mapFunction = ({ blockNumber, transactionHash, returnValues: { nullifierHash, to, fee } }) => ({ blockNumber, transactionHash, nullifierHash, to, fee });
|
||
|
||
const finalBlock = Math.min(i + chunks - 1, targetBlock);
|
||
try {
|
||
const fetchedEvents = await contract.getPastEvents(eventNameInContract, { fromBlock: i, toBlock: finalBlock });
|
||
console.log('Fetched', type === "relayer" ? type : `${amount} ${currency.toUpperCase()} ${type}`, 'events to block:', finalBlock);
|
||
if (fetchedEvents.length === 0) continue;
|
||
await updateCache(fetchedEvents.map(mapFunction));
|
||
} catch (err) {
|
||
console.error(`Failed fetching ${type} events from node on block ${i}: `, err)
|
||
process.exit(1);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log(error);
|
||
throw new Error('Error while updating cache');
|
||
}
|
||
}
|
||
|
||
async function syncGraphEvents() {
|
||
|
||
/**
|
||
* Query events from graph (1000 events for a time maximum)
|
||
* @param {number} blockNumber Block number in blockchain, from which it will fetch events
|
||
* @param {('' | 'gt')} filter If "_gt", it fetches 1000 events with blockNumber greater then provided, if empty, it fetches events only with provided blockNumber
|
||
* @returns {Promise<Array>}
|
||
*/
|
||
async function queryFromGraph(blockNumber, filter = "_gt") {
|
||
try {
|
||
const variables = type === 'relayer' ? { blockRegistration: blockNumber } : {
|
||
currency: currency.toString().toLowerCase(),
|
||
amount: amount.toString().toLowerCase(),
|
||
blockNumber
|
||
};
|
||
|
||
let query, mapFunction;
|
||
if (type === 'relayer') {
|
||
query = `
|
||
query($blockRegistration: Int){
|
||
relayers(orderBy: blockRegistration, first: 1000, where: {blockRegistration${filter}: $blockRegistration}) {
|
||
address, ensName, ensHash, blockRegistration
|
||
}
|
||
}`;
|
||
mapFunction = ({ blockRegistration, address, ensName, ensHash }) => ({ address, ensHash, ensName, blockNumber: Number(blockRegistration) });
|
||
}
|
||
else if (type === 'deposit') {
|
||
query = `
|
||
query($currency: String, $amount: String, $blockNumber: Int){
|
||
deposits(orderBy: blockNumber, first: 1000, where: {currency: $currency, amount: $amount, blockNumber${filter}: $blockNumber}) {
|
||
blockNumber, transactionHash, commitment, index
|
||
}
|
||
}`;
|
||
mapFunction = ({ blockNumber, transactionHash, commitment, index }) => ({ blockNumber: Number(blockNumber), transactionHash, commitment, leafIndex: Number(index) });
|
||
}
|
||
else if (type === "withdrawal") {
|
||
query = `
|
||
query($currency: String, $amount: String, $blockNumber: Int){
|
||
withdrawals(orderBy: blockNumber, first: 1000, where: {currency: $currency, amount: $amount, blockNumber${filter}: $blockNumber}) {
|
||
blockNumber, transactionHash, nullifier, to, fee
|
||
}
|
||
}`;
|
||
mapFunction = ({ blockNumber, transactionHash, nullifier, to, fee }) => ({ blockNumber: Number(blockNumber), transactionHash, nullifierHash: nullifier, to, fee });
|
||
}
|
||
|
||
const querySubgraph = await retryPostRequest(subgraph, { query, variables }, globals.requestOptions, 3);
|
||
const queryResult = querySubgraph.data.data[`${type}s`];
|
||
return queryResult.map(mapFunction);
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
async function fetchGraphEvents() {
|
||
console.log('Querying latest events from subgraph');
|
||
const latestBlock = await web3Instance.getBlockNumber();
|
||
try {
|
||
for (let i = startBlock; i < latestBlock;) {
|
||
let result = await queryFromGraph(i);
|
||
if (Object.keys(result).length === 0) break;
|
||
const resultBlockNumber = result[result.length - 1].blockNumber;
|
||
while (result.length > 0 && result[result.length - 1].blockNumber === resultBlockNumber) result.pop();
|
||
result = result.concat(await queryFromGraph(resultBlockNumber, ""));
|
||
await updateCache(result);
|
||
i = resultBlockNumber;
|
||
console.log('Fetched', type === 'relayer' ? type : `${amount} ${currency.toUpperCase()} ${type}`, 'events to block:', Number(resultBlockNumber));
|
||
}
|
||
} catch {
|
||
console.log('Fallback to web3 events');
|
||
await syncEvents();
|
||
}
|
||
}
|
||
await fetchGraphEvents();
|
||
}
|
||
if (subgraph && !useOnlyRpc) {
|
||
await syncGraphEvents();
|
||
} else {
|
||
await syncEvents();
|
||
}
|
||
|
||
const updatedEvents = loadCachedEvents({ type, currency, amount })
|
||
const updatedBlock = updatedEvents[updatedEvents.length - 1].blockNumber;
|
||
console.log('Cache updated for Tornado', type === 'relayer' ? type : `${amount} ${currency.toUpperCase()} instance to block`, updatedBlock, 'successfully');
|
||
console.log(`Total ${type}s:`, updatedEvents.length - 1);
|
||
return updatedEvents;
|
||
}
|
||
|
||
/**
|
||
* Parses Tornado Cash note
|
||
* @param {string} noteString the note
|
||
*/
|
||
function parseNote(noteString) {
|
||
const noteRegex = /tornado-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<note>[0-9a-fA-F]{124})/g;
|
||
const match = noteRegex.exec(noteString);
|
||
if (!match) {
|
||
throw new Error('The note has invalid format');
|
||
}
|
||
|
||
const buf = Buffer.from(match.groups.note, 'hex');
|
||
const nullifier = bigInt.leBuff2int(buf.slice(0, 31));
|
||
const secret = bigInt.leBuff2int(buf.slice(31, 62));
|
||
const deposit = createDeposit({ nullifier, secret });
|
||
const netId = Number(match.groups.netId);
|
||
|
||
return {
|
||
currency: match.groups.currency,
|
||
amount: match.groups.amount,
|
||
netId,
|
||
deposit
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Parses Tornado Cash deposit invoice
|
||
* @param invoiceString the note
|
||
*/
|
||
function parseInvoice(invoiceString) {
|
||
const noteRegex = /tornadoInvoice-(?<currency>\w+)-(?<amount>[\d.]+)-(?<netId>\d+)-0x(?<commitmentNote>[0-9a-fA-F]{64})/g;
|
||
const match = noteRegex.exec(invoiceString);
|
||
if (!match) {
|
||
throw new Error('The invoice has invalid format');
|
||
}
|
||
|
||
const netId = Number(match.groups.netId);
|
||
const buf = Buffer.from(match.groups.commitmentNote, 'hex');
|
||
const commitmentNote = toHex(buf.slice(0, 32));
|
||
|
||
return {
|
||
currency: match.groups.currency,
|
||
amount: match.groups.amount,
|
||
netId,
|
||
commitmentNote
|
||
};
|
||
}
|
||
|
||
async function loadDepositData({ amount, currency, deposit }) {
|
||
const { web3Instance, tornadoInstanceContract } = globals;
|
||
const cachedEvents = await fetchEvents({ type: 'deposit', currency, amount });
|
||
const depositEvent = cachedEvents.find(event => event.commitment === deposit.commitmentHex);
|
||
if (!depositEvent) throw new Error('There is no related deposit, the note is invalid');
|
||
|
||
const { timestamp } = await web3Instance.getBlock(depositEvent.blockNumber);
|
||
const txHash = depositEvent.transactionHash;
|
||
const isSpent = await tornadoInstanceContract.methods.isSpent(deposit.nullifierHex).call();
|
||
const receipt = await web3Instance.getTransactionReceipt(txHash);
|
||
|
||
return {
|
||
timestamp,
|
||
txHash,
|
||
isSpent,
|
||
leafIndex: depositEvent.leafIndex,
|
||
from: receipt.from,
|
||
commitment: deposit.commitmentHex
|
||
};
|
||
}
|
||
async function loadWithdrawalData({ amount, currency, deposit }) {
|
||
const { netId, web3Instance } = globals;
|
||
|
||
try {
|
||
const cachedEvents = await fetchEvents({ type: 'withdrawal', currency, amount });
|
||
|
||
const withdrawEvent = cachedEvents.filter((event) => {
|
||
return event.nullifierHash === deposit.nullifierHex;
|
||
})[0];
|
||
|
||
if (!withdrawEvent) return null;
|
||
|
||
const fee = withdrawEvent.fee;
|
||
const decimals = config.deployments[`netId${netId}`]['tokens'][currency].decimals;
|
||
const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(toBN(fee));
|
||
const { timestamp } = await web3Instance.getBlock(withdrawEvent.blockNumber);
|
||
return {
|
||
amount: toDecimals(withdrawalAmount, decimals, 9),
|
||
txHash: withdrawEvent.transactionHash,
|
||
to: withdrawEvent.to,
|
||
timestamp,
|
||
nullifier: deposit.nullifierHex,
|
||
fee: toDecimals(fee, decimals, 9)
|
||
};
|
||
} catch (e) {
|
||
console.error('loadWithdrawalData', e);
|
||
}
|
||
}
|
||
|
||
async function promptConfirmation(query) {
|
||
query = query || 'Confirm the transaction [Y/n] ';
|
||
const confirmation = await new Promise((resolve) => prompt.question(query, resolve));
|
||
|
||
if (confirmation.toUpperCase() !== 'Y' && confirmation.toLowerCase() !== "yes") {
|
||
console.error('User rejected this action');
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Stake TORN tokens in Governance contract to earn rewards from withdrawals and perticate in Governance with voting
|
||
* @param {number | string} amount Amount of tokens. Can be fractional, for example stake 100.8432 TORN
|
||
*/
|
||
async function stakeTorn(amount) {
|
||
const { tornTokenContract, governanceContract, signerAddress } = globals;
|
||
const tokenAmount = fromDecimals({ amount, decimals: 18 });
|
||
const allowance = await tornTokenContract.methods.allowance(signerAddress, governanceAddress).call();
|
||
console.log('Current TORN allowance is', fromWei(allowance));
|
||
if (toBN(allowance).lt(toBN(tokenAmount))) {
|
||
console.log('Approving tokens for stake');
|
||
await generateTransaction(tornTokenAddress, tornTokenContract.methods.approve(governanceAddress, tokenAmount).encodeABI(), 0, 'send');
|
||
}
|
||
console.log("Sending stake transaction...");
|
||
await generateTransaction(governanceAddress, governanceContract.methods.lockWithApproval(tokenAmount).encodeABI(), 0, 'send');
|
||
|
||
const stakedAmount = await governanceContract.methods.lockedBalance(signerAddress).call();
|
||
console.log("Staked successfull: your current stake balance is", fromWei(stakedAmount), "TORN");
|
||
}
|
||
|
||
/**
|
||
* Withdraw TORN tokens from Governance staking (without rewards)
|
||
* @param {number | string} amount Amount of TORn tokens to withdraw, can be fractional
|
||
*/
|
||
async function unstakeTorn(amount) {
|
||
const { governanceContract, signerAddress } = globals;
|
||
const tokenAmount = fromDecimals({ amount, decimals: 18 });
|
||
const stakedAmount = await governanceContract.methods.lockedBalance(signerAddress).call();
|
||
if (toBN(stakedAmount).lt(toBN(tokenAmount))) throw new Error(`Not enough tokens in stake. You have ${fromWei(stakedAmount)} tokens, but you're trying to withdraw ${amount}.`);
|
||
|
||
console.log("Sending unstake transaction...");
|
||
await generateTransaction(governanceAddress, governanceContract.methods.unlock(tokenAmount).encodeABI(), 0, 'send');
|
||
}
|
||
|
||
/**
|
||
* Delegate voting power in Tornado Cash governance to another address: delegatee can vote and create proposals on your behalf
|
||
* @param {string} address Delegatee address
|
||
*/
|
||
async function delegate(address) {
|
||
if (!web3Utils.isAddress(address)) throw new Error("Cannot delegate: invalid delegatee address provided");
|
||
|
||
await generateTransaction(governanceAddress, globals.governanceContract.methods.delegate(address).encodeABI(), 0, 'send');
|
||
}
|
||
|
||
/**
|
||
* Remove Tornado Cash governance delegation. After doing it, nobody can vote or create proposals on your behalf
|
||
*/
|
||
async function undelegate() {
|
||
const { governanceContract, signerAddress } = globals;
|
||
const currentDelegatee = await governanceContract.methods.delegatedTo(signerAddress).call();
|
||
if (currentDelegatee.toLowerCase() === "0x0000000000000000000000000000000000000000") {
|
||
console.log("No actual delegatee: already undelegated");
|
||
return;
|
||
}
|
||
|
||
await generateTransaction(governanceAddress, governanceContract.methods.undelegate().encodeABI(), 0, 'send');
|
||
}
|
||
|
||
/**
|
||
* Loads actual data from contract and prints information about Tornado Cash staking for specified address: stake amount, unclaimed staking rewards and voting power delegation
|
||
* @param {string} address
|
||
*/
|
||
async function printStakeInfo(address) {
|
||
if (!web3Utils.isAddress(address)) throw new Error("Cannot check stake info: invalid address provided");
|
||
|
||
const { governanceContract, stakingRewardsContract } = globals;
|
||
|
||
const stakedAmount = await governanceContract.methods.lockedBalance(address).call();
|
||
const rewardsAmount = await stakingRewardsContract.methods.checkReward(address).call();
|
||
const delegatee = await governanceContract.methods.delegatedTo(address).call();
|
||
|
||
console.log('\n====================Staking info====================');
|
||
console.log('Account :', address);
|
||
console.log('Staked balance :', `${fromWei(stakedAmount)} TORN`);
|
||
console.log('Unclaimed rewards :', `${fromWei(rewardsAmount)} TORN`);
|
||
console.log('Delegation status :', delegatee.toLowerCase() === "0x0000000000000000000000000000000000000000" ? "not delegated" : `voting power delegated to ${delegatee}`);
|
||
console.log('====================================================', '\n');
|
||
}
|
||
|
||
/**
|
||
* Vote in governance from signer address with all staked tokens for or against specified proposal by ID
|
||
* @param {string | number} proposalId Proposal ID
|
||
* @param {string} decision For or against proposal ("yes" is for, "no" is against, true is for, false is against)
|
||
*/
|
||
async function vote(proposalId, decision){
|
||
const { governanceContract, signerAddress } = globals;
|
||
|
||
const signerStakedBalance = await governanceContract.methods.lockedBalance(signerAddress).call();
|
||
if (signerStakedBalance.lte(0)) throw new Error("You have no staked balance, therefore you cannot vote.");
|
||
|
||
let support;
|
||
if (decision === "yes" || decision === "for") support = true;
|
||
else if (decision === "no" || decision === "against") support = false;
|
||
else throw new Error("Invalid user decision: cannot vote for or against proposal");
|
||
|
||
|
||
await generateTransaction(governanceAddress, governanceContract.methods.castVote(Number(proposalId), support).encodeABI(), 0, 'send');
|
||
}
|
||
|
||
/**
|
||
* Initiate transaction to claim staking rewards from Tornado Cash staking
|
||
*/
|
||
async function claimStakingRewards(){
|
||
await generateTransaction(stakingRewardsAddress, globals.stakingRewardsContract.methods.getReward().encodeABI(), 0, 'send');
|
||
}
|
||
|
||
/**
|
||
* Create web3 eth instance with provider using RPC link
|
||
* @param {string} rpc Full RPC link
|
||
* @returns {Promise<Web3Eth>} Initialized Web3 instance object
|
||
*/
|
||
async function createWeb3Instance(rpc) {
|
||
const { torPort } = globals;
|
||
|
||
let web3;
|
||
if (torPort && rpc.startsWith('https')) {
|
||
web3Options = { agent: { https: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort) }, timeout: 20000 };
|
||
web3 = new Web3(new Web3.providers.HttpProvider(rpc, web3Options), null, { transactionConfirmationBlocks: 1 });
|
||
} else if (torPort && rpc.startsWith('http')) {
|
||
web3Options = { agent: { http: new SocksProxyAgent('socks5h://127.0.0.1:' + torPort) }, timeout: 20000 };
|
||
web3 = new Web3(new Web3.providers.HttpProvider(rpc, web3Options), null, { transactionConfirmationBlocks: 1 });
|
||
} else if (rpc.includes('ipc')) {
|
||
console.log('Using ipc connection');
|
||
web3 = new Web3(new Web3.providers.IpcProvider(rpc, {}), null, { transactionConfirmationBlocks: 1 });
|
||
} else if (rpc.startsWith('ws') || rpc.startsWith('wss')) {
|
||
console.log('Using websocket connection (Note: Tor is not supported for Websocket providers)');
|
||
web3Options = {
|
||
clientConfig: { keepalive: true, keepaliveInterval: -1 },
|
||
reconnect: { auto: true, delay: 1000, maxAttempts: 10, onTimeout: false }
|
||
};
|
||
web3 = new Web3(new Web3.providers.WebsocketProvider(rpc, web3Options), null, { transactionConfirmationBlocks: 1 });
|
||
} else {
|
||
console.log(`Connecting to remote node ${rpc}`);
|
||
web3 = new Web3(new Web3.providers.HttpProvider(rpc, { timeout: 10000, keepAlive: true }), null, { transactionConfirmationBlocks: 1 });
|
||
}
|
||
|
||
return web3.eth;
|
||
}
|
||
|
||
/**
|
||
* Initialize TORN contracts and then call initNetwork
|
||
* @param {Object} args Arguments to pass to initNetwork function
|
||
*/
|
||
async function initTorn(args) {
|
||
initPreferences(args);
|
||
await initNetwork({ ...args, chainId: 1, onlyRpc: true });
|
||
const { web3Instance } = globals;
|
||
|
||
globals.governanceContract = new web3Instance.Contract(tornadoGovernanceAbi, governanceAddress);
|
||
globals.tornTokenContract = new web3Instance.Contract(erc20Abi, tornTokenAddress);
|
||
globals.stakingRewardsContract = new web3Instance.Contract(stakingRewardsAbi, stakingRewardsAddress);
|
||
}
|
||
|
||
/**
|
||
* Init web3 network from user parameters for all program
|
||
* @param {Object} args Arguments
|
||
* @param {string} [args.rpc] Full link to RPC node
|
||
* @param {number} [args.chainId] Chain ID (1 - ETH, 56 - BSC etc)
|
||
* @param {string} [args.privateKey] Private key from user account (64 symbols or 66 if starts from 0x)
|
||
* @param {string | number} [args.torPort] Port for Tor proxy, if user want to use it
|
||
* @param {boolean} [args.onlyRpc] Use only RPC without other network requests
|
||
* @param {EventType} [args.eventType] Applicable event type for user actions
|
||
* @param {string} [relayer] User-provided relayer link
|
||
*/
|
||
async function initNetwork({ rpc, chainId, privateKey, torPort, onlyRpc, eventType, relayer }) {
|
||
|
||
if (torPort) {
|
||
globals.torPort = torPort;
|
||
globals.requestOptions = {
|
||
...globals.requestOptions,
|
||
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' }
|
||
};
|
||
}
|
||
|
||
if (chainId && !rpc) {
|
||
const subgraphUrl = onlyRpc ? null : await selectDefaultGraph(chainId, eventType)
|
||
rpc = await selectDefaultRpc(chainId, eventType, !!subgraphUrl)
|
||
}
|
||
|
||
globals.web3Instance = await createWeb3Instance(rpc)
|
||
globals.netId = await globals.web3Instance.getChainId()
|
||
globals.netName = getCurrentNetworkName();
|
||
globals.netSymbol = getCurrentNetworkSymbol(globals.netId);
|
||
|
||
globals.feeOracle = Number(globals.netId) === 1 ? new TornadoFeeOracleV4(globals.netId, rpc) : new TornadoFeeOracleV5(globals.netId, rpc);
|
||
|
||
// Create web3 instance to fetch relayer on mainnet, if user want to get relayers list or withdraw, but he didn't provide relayer link
|
||
if (!relayer && !privateKey && (eventType === 'relayer' || eventType === 'withdrawal')) {
|
||
if (globals.netId === 1) globals.relayerWeb3Instance = globals.web3Instance;
|
||
else {
|
||
const subgraphUrl = onlyRpc ? null : await selectDefaultGraph(1, 'relayer');
|
||
const relayerRPC = await selectDefaultRpc(1, 'relayer', subgraphUrl);
|
||
globals.relayerWeb3Instance = await createWeb3Instance(relayerRPC);
|
||
}
|
||
}
|
||
|
||
const rpcHost = new URL(rpc).hostname;
|
||
const isIpPrivate = is_ip_private(rpcHost);
|
||
|
||
if (isIpPrivate || rpc.includes('localhost') || onlyRpc) {
|
||
console.log('Only RPC mode');
|
||
globals.useOnlyRpc = true;
|
||
}
|
||
|
||
if (!globals.useOnlyRpc) {
|
||
try {
|
||
const htmlIPInfo = await axios.get('https://check.torproject.org', globals.requestOptions);
|
||
const ip = htmlIPInfo.data.split('Your IP address appears to be: <strong>').pop().split('</')[0];
|
||
console.log('Your remote IP address is', ip);
|
||
} catch (error) {
|
||
console.error('Could not fetch remote IP from check.torproject.org, use VPN if the problem repeats.');
|
||
}
|
||
}
|
||
|
||
const privKey = privateKey || process.env.PRIVATE_KEY;
|
||
if (privKey) globals.privateKey = privKey.startsWith("0x") ? privKey.substring(2) : privKey;
|
||
|
||
if (globals.privateKey) {
|
||
const account = globals.web3Instance.accounts.privateKeyToAccount('0x' + globals.privateKey);
|
||
globals.web3Instance.accounts.wallet.add('0x' + globals.privateKey);
|
||
globals.web3Instance.defaultAccount = account.address;
|
||
globals.signerAddress = account.address;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Set user preferences in global program options object
|
||
* @param {Object} userPreferences
|
||
* @param {boolean} [userPreferences.nonconfirmation] Don't ask for confirmation for crucial actions
|
||
* @param {boolean} [userPreferences.localMode] Don't submit signed transactions to blockchain (remote nodes)
|
||
*/
|
||
function initPreferences({ nonconfirmation, localMode }) {
|
||
if (nonconfirmation) {
|
||
console.log("Non-confirmation mode detected: program won't ask confirmation for crucial actions")
|
||
globals.shouldPromptConfirmation = false;
|
||
}
|
||
if (localMode) {
|
||
console.log("Local mode detected: program won't submit signed TX to remote node");
|
||
globals.shouldSubmitTx = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Init web3, all Tornado contracts, and snark
|
||
*/
|
||
async function init({ rpc, chainId, currency = 'dai', amount = '100', privateKey, torPort, onlyRpc, nonconfirmation, localMode, eventType, relayer }) {
|
||
currency = currency.toLowerCase()
|
||
|
||
initPreferences({ nonconfirmation, localMode });
|
||
await initNetwork({ rpc, chainId, privateKey, torPort, onlyRpc, eventType, relayer });
|
||
|
||
const { netId, web3Instance } = globals;
|
||
// console.log(netId, chainId);
|
||
if (chainId && Number(chainId) !== netId) {
|
||
throw new Error('This note is for a different network. Specify the --rpc option explicitly');
|
||
}
|
||
|
||
try {
|
||
globals.tornadoProxyAddress = config.deployments[`netId${netId}`].proxy;
|
||
globals.multiCallAddress = config.deployments[`netId${netId}`].multicall;
|
||
globals.tornadoInstanceAddress = config.deployments[`netId${netId}`]['tokens'][currency].instanceAddress[amount];
|
||
globals.instanceDeployedBlockNumber = config.deployments[`netId${netId}`]['tokens'][currency].deployedBlockNumber[amount];
|
||
|
||
if (!globals.tornadoProxyAddress) {
|
||
throw new Error("No Tornado Proxy for selected chain, did you provide correct chain id?");
|
||
}
|
||
globals.instanceTokenAddress =
|
||
currency !== globals.netSymbol.toLowerCase() ? config.deployments[`netId${netId}`]['tokens'][currency].tokenAddress : null;
|
||
} catch (e) {
|
||
console.error('There is no such tornado instance, check the currency and amount you provide', e);
|
||
process.exit(1);
|
||
}
|
||
globals.tornadoProxyContract = new web3Instance.Contract(tornadoProxyAbi, globals.tornadoProxyAddress);
|
||
globals.tornadoInstanceContract = new web3Instance.Contract(tornadoInstanceAbi, globals.tornadoInstanceAddress);
|
||
globals.tornadoTokenInstanceContract = currency !== globals.netSymbol.toLowerCase() ? new web3Instance.Contract(erc20Abi, globals.instanceTokenAddress) : null;
|
||
}
|
||
|
||
async function main() {
|
||
program
|
||
.option('-r, --rpc <URL>', 'The RPC that CLI should interact with')
|
||
.option('-R, --relayer <URL>', 'Withdraw via relayer')
|
||
.option('-T, --tor-port <PORT>', 'Optional tor port')
|
||
.option('-p, --private-key <KEY>', "Wallet private key - If you didn't add it to .env file and it is needed for operation")
|
||
.option('-N --noconfirmation', 'No confirmation mode - Does not query confirmation ')
|
||
.option('-L, --local-mode', 'Local node mode - Does not submit signed transaction to the node')
|
||
.option('-o, --only-rpc', 'Only rpc mode - Does not enable subgraph api nor remote ip detection');
|
||
program
|
||
.command('deposit <currency> <amount> [chain_id]')
|
||
.description(
|
||
'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://tornadocash.eth.link.'
|
||
)
|
||
.action(async (currency, amount, chainId) => {
|
||
await init({ ...program, currency, amount, eventType: 'deposit', chainId });
|
||
await deposit({ currency, amount });
|
||
});
|
||
program
|
||
.command('withdraw <note> <recipient> [ETH_purchase]')
|
||
.description(
|
||
'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.'
|
||
)
|
||
.action(async (noteString, recipient, refund) => {
|
||
const { currency, amount, netId, deposit } = parseNote(noteString);
|
||
|
||
await init({ ...program, chainId: netId, currency, amount, eventType: 'withdrawal' });
|
||
|
||
await withdraw({
|
||
deposit,
|
||
currency,
|
||
amount,
|
||
recipient,
|
||
refund,
|
||
privateKey: program.privateKey,
|
||
relayerURL: program.relayer
|
||
});
|
||
});
|
||
program
|
||
.command('compliance <note>')
|
||
.description(
|
||
'Shows the deposit and withdrawal of the provided note. This might be necessary to show the origin of assets held in your withdrawal address.'
|
||
)
|
||
.action(async (noteString) => {
|
||
|
||
const { currency, amount, netId, deposit } = parseNote(noteString);
|
||
|
||
await init({ ...program, chainId: netId, currency, amount, eventType: 'withdrawal', relayer: "dummy" });
|
||
|
||
const depositInfo = await loadDepositData({ amount, currency, deposit });
|
||
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit });
|
||
|
||
const depositDate = new Date(depositInfo.timestamp * 1000);
|
||
console.log('\n=============Deposit=================');
|
||
console.log('Deposit :', amount, currency.toUpperCase());
|
||
console.log('Date :', depositDate.toLocaleDateString(), depositDate.toLocaleTimeString());
|
||
console.log('From :', `https://${getExplorerLink()}/address/${depositInfo.from}`);
|
||
console.log('Transaction :', `https://${getExplorerLink()}/tx/${depositInfo.txHash}`);
|
||
console.log('Commitment :', depositInfo.commitment);
|
||
console.log('Spent :', depositInfo.isSpent);
|
||
console.log('=====================================', '\n');
|
||
|
||
if (!depositInfo.isSpent) {
|
||
console.log('The note was not spent!');
|
||
return;
|
||
}
|
||
|
||
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000);
|
||
console.log('\n=============Withdrawal==============');
|
||
console.log('Withdrawal :', withdrawInfo.amount, currency);
|
||
console.log('Relayer Fee :', withdrawInfo.fee, currency);
|
||
console.log('Date :', withdrawalDate.toLocaleDateString(), withdrawalDate.toLocaleTimeString());
|
||
console.log('To :', `https://${getExplorerLink()}/address/${withdrawInfo.to}`);
|
||
console.log('Transaction :', `https://${getExplorerLink()}/tx/${withdrawInfo.txHash}`);
|
||
console.log('Nullifier :', withdrawInfo.nullifier);
|
||
console.log('=====================================', '\n');
|
||
});
|
||
program
|
||
.command("stake <amount>")
|
||
.description("Stake TORN tokens in Governance contract to earn rewards and vote. Requires private key")
|
||
.action(async (amount) => {
|
||
await initTorn(program);
|
||
await stakeTorn(Number(amount));
|
||
})
|
||
program
|
||
.command("unstake <amount>")
|
||
.description("Unstake TORN tokens (withdraw from Governance staking). Requires private key")
|
||
.action(async (amount) => {
|
||
await initTorn(program);
|
||
await unstakeTorn(amount);
|
||
})
|
||
program
|
||
.command("checkStake <address>")
|
||
.description("Check Tornado Cash staking information about provided address: stake amount, unclaimed staking rewards and voting power delegation")
|
||
.action(async (address) => {
|
||
await initTorn(program);
|
||
await printStakeInfo(address);
|
||
});
|
||
program
|
||
.command("claim")
|
||
.description("Claim staking rewards. Requires private key")
|
||
.action(async () => {
|
||
await initTorn(program);
|
||
await claimStakingRewards();
|
||
})
|
||
program
|
||
.command("vote <decision> <proposal_id>")
|
||
.description("Vote for or against Tornado Cash governance proposal with all staked tokens. Decision can be `yes/for` or `no/against`. To change your vote, just use this function again with different decision")
|
||
.action(async (decision, proposalId) => {
|
||
await initTorn(program);
|
||
await vote(proposalId, decision.toLowerCase())
|
||
})
|
||
program
|
||
.command("delegate <address>")
|
||
.description("Delegate voting power to another address. Requires private key")
|
||
.action(async (address) => {
|
||
await initTorn(program);
|
||
await delegate(address);
|
||
});
|
||
program
|
||
.command("undelegate")
|
||
.description("Remove current delegatee (nobody can vote or create proposals on your behalf after it). Requires private key")
|
||
.action(async () => {
|
||
await initTorn(program);
|
||
await undelegate();
|
||
});
|
||
program
|
||
.command('createNote <currency> <amount> <chainId>')
|
||
.description(
|
||
'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://tornadocash.eth.link.'
|
||
)
|
||
.action(async (currency, amount, chainId) => {
|
||
currency = currency.toLowerCase();
|
||
await createInvoice({ currency, amount, chainId });
|
||
});
|
||
program
|
||
.command('depositInvoice <invoice>')
|
||
.description('Submit a deposit of invoice from default eth account and return the resulting note.')
|
||
.action(async (invoice) => {
|
||
|
||
const { currency, amount, netId, commitmentNote } = parseInvoice(invoice);
|
||
await init({
|
||
...program,
|
||
currency,
|
||
amount,
|
||
chainId: netId,
|
||
eventType: 'deposit'
|
||
});
|
||
console.log('Creating', currency.toUpperCase(), amount, 'deposit for', globals.netName, 'Tornado Cash Instance');
|
||
await deposit({ currency, amount, commitmentNote });
|
||
});
|
||
program
|
||
.command("listRelayers <chain_id>")
|
||
.description("Check available relayers on selected chain. If you wantue non-default RPC, you should provide ONLY mainnet RPC urls")
|
||
.action(async (chainId) => {
|
||
await initNetwork({ ...program, chainId: 1, eventType: 'relayer' })
|
||
const availableRelayers = await getRelayers(chainId);
|
||
console.log("There are " + availableRelayers.length + " available relayers")
|
||
|
||
for (const relayer of availableRelayers) {
|
||
console.log({
|
||
'hostname': 'https://' + relayer.hostname + '/',
|
||
'ensName': relayer.ensName,
|
||
'stakeBalance': Number(web3Utils.fromWei(relayer.stakeBalance, 'ether')).toFixed(2) + " TORN",
|
||
'tornadoServiceFee': relayer.tornadoServiceFee + "%"
|
||
});
|
||
}
|
||
});
|
||
program
|
||
.command('balance <address> [token_address]')
|
||
.description('Check ETH and ERC20 balance')
|
||
.action(async (address, tokenAddress) => {
|
||
await initNetwork(program);
|
||
|
||
if (!address && signerAddress) {
|
||
console.log('Using address', signerAddress, 'from private key');
|
||
address = signerAddress;
|
||
}
|
||
await printETHBalance({ address, name: 'Account' });
|
||
if (tokenAddress) {
|
||
await printERC20Balance({ address, name: 'Account', tokenAddress });
|
||
}
|
||
});
|
||
program
|
||
.command('send <address> [amount] [token_address]')
|
||
.description('Send ETH or ERC to address')
|
||
.action(async (address, amount, tokenAddress) => {
|
||
initPreferences(program);
|
||
await initNetwork(program);
|
||
|
||
await send({ address, amount, tokenAddress });
|
||
});
|
||
program
|
||
.command('broadcast <signedTX>')
|
||
.description('Submit signed TX to the remote node')
|
||
.action(async (signedTX) => {
|
||
await initNetwork(program);
|
||
await submitTransaction(signedTX);
|
||
});
|
||
program
|
||
.command('syncEvents <type> <currency> <amount> [chain_id]')
|
||
.description('Sync the local cache file of deposit / withdrawal events for specific currency.')
|
||
.action(async (type, currency, amount, chainId) => {
|
||
console.log('Starting event sync command');
|
||
|
||
await init({ ...program, currency, amount, chainId });
|
||
if (type === "withdraw") type === "withdrawal";
|
||
|
||
const cachedEvents = await fetchEvents({ type, currency, amount });
|
||
console.log(
|
||
'Synced event for',
|
||
type,
|
||
amount,
|
||
currency.toUpperCase(),
|
||
globals.netName,
|
||
'Tornado instance to block',
|
||
cachedEvents[cachedEvents.length - 1].blockNumber
|
||
);
|
||
});
|
||
program
|
||
.command('checkCacheValidity <currency> <amount> [chain_id]')
|
||
.description('Check cache file of deposit events for specific currency for validity of the root.')
|
||
.action(async (currency, amount, chainId) => {
|
||
const type = 'deposit';
|
||
|
||
await init({ ...program, currency, amount, chainId, eventType: type });
|
||
const depositCachedEvents = await fetchEvents({ type, currency, amount });
|
||
const isValidRoot = await isRootValid(depositCachedEvents);
|
||
|
||
console.log(
|
||
'\nDeposit events tree for',
|
||
amount,
|
||
currency.toUpperCase(),
|
||
'on',
|
||
globals.netName,
|
||
'chain',
|
||
isValidRoot ? 'has valid root' : 'is invalid, unknown root. You need to reset cache to zero array or to latest git state'
|
||
);
|
||
});
|
||
program.command('parseNote <note>').action(async (noteString) => {
|
||
const parse = parseNote(noteString);
|
||
|
||
netId = parse.netId;
|
||
|
||
console.log('\n=============Note=================');
|
||
console.log('Network:', getCurrentNetworkName());
|
||
console.log('Denomination:', parse.amount, parse.currency.toUpperCase());
|
||
console.log('Commitment: ', parse.deposit.commitmentHex);
|
||
console.log('Nullifier Hash: ', parse.deposit.nullifierHex);
|
||
console.log('=====================================', '\n');
|
||
});
|
||
try {
|
||
await program.parseAsync(process.argv);
|
||
process.exit(0);
|
||
} catch (e) {
|
||
console.log('Error:', e);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
main(); |