229 lines
6.4 KiB
JavaScript
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();
|