tornado-offline-deposit/tornadoOffline.js
2024-07-22 13:02:53 -07:00

229 lines
6.4 KiB
JavaScript

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