This commit is contained in:
Alexey 2021-02-14 21:23:17 +03:00
parent 94d43e31a3
commit 496680b379
2 changed files with 131 additions and 57 deletions

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"semi": false,
"printWidth": 130
}

181
cli.js
View File

@ -23,14 +23,14 @@ let web3, tornado, mixerContract, tornadoInstance, circuit, proving_key, groth16
let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY let MERKLE_TREE_HEIGHT, ETH_AMOUNT, TOKEN_AMOUNT, PRIVATE_KEY
/** Whether we are in a browser or node.js */ /** Whether we are in a browser or node.js */
const inBrowser = (typeof window !== 'undefined') const inBrowser = typeof window !== 'undefined'
let isLocalRPC = false let isLocalRPC = false
/** Generate random number of specified byte length */ /** Generate random number of specified byte length */
const rbigint = nbytes => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes)) const rbigint = (nbytes) => snarkjs.bigInt.leBuff2int(crypto.randomBytes(nbytes))
/** Compute pedersen hash */ /** Compute pedersen hash */
const pedersenHash = data => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0] const pedersenHash = (data) => circomlib.babyJub.unpackPoint(circomlib.pedersenHash.hash(data))[0]
/** BigNumber to hex string of specified length */ /** BigNumber to hex string of specified length */
function toHex(number, length = 32) { function toHex(number, length = 32) {
@ -69,7 +69,10 @@ function createDeposit({ nullifier, secret }) {
* @param amount Deposit amount * @param amount Deposit amount
*/ */
async function deposit({ currency, amount }) { async function deposit({ currency, amount }) {
const deposit = createDeposit({ nullifier: rbigint(31), secret: rbigint(31) }) const deposit = createDeposit({
nullifier: rbigint(31),
secret: rbigint(31)
})
const note = toHex(deposit.preimage, 62) const note = toHex(deposit.preimage, 62)
const noteString = `tornado-${currency}-${amount}-${netId}-${note}` const noteString = `tornado-${currency}-${amount}-${netId}-${note}`
console.log(`Your note: ${noteString}`) console.log(`Your note: ${noteString}`)
@ -81,7 +84,8 @@ async function deposit({ currency, amount }) {
await tornado.methods.deposit(tornadoInstance, toHex(deposit.commitment), []).send({ value, from: senderAccount, gas: 2e6 }) await tornado.methods.deposit(tornadoInstance, toHex(deposit.commitment), []).send({ value, from: senderAccount, gas: 2e6 })
await printETHBalance({ address: tornado._address, name: 'Tornado' }) await printETHBalance({ address: tornado._address, name: 'Tornado' })
await printETHBalance({ address: senderAccount, name: 'Sender account' }) await printETHBalance({ address: senderAccount, name: 'Sender account' })
} else { // a token } else {
// a token
await printERC20Balance({ address: tornado._address, name: 'Tornado' }) await printERC20Balance({ address: tornado._address, name: 'Tornado' })
await printERC20Balance({ address: senderAccount, name: 'Sender account' }) await printERC20Balance({ address: senderAccount, name: 'Sender account' })
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
@ -116,7 +120,10 @@ async function deposit({ currency, amount }) {
async function generateMerkleProof(deposit) { async function generateMerkleProof(deposit) {
let leafIndex = -1 let leafIndex = -1
// Get all deposit events from smart contract and assemble merkle tree from them // Get all deposit events from smart contract and assemble merkle tree from them
const events = await mixerContract.getPastEvents('Deposit', { fromBlock: 0, toBlock: 'latest' }) const events = await mixerContract.getPastEvents('Deposit', {
fromBlock: 0,
toBlock: 'latest'
})
const leaves = events const leaves = events
.sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order .sort((a, b) => a.returnValues.leafIndex - b.returnValues.leafIndex) // Sort events in chronological order
@ -168,7 +175,7 @@ async function generateProof({ deposit, recipient, relayerAddress = 0, fee = 0,
nullifier: deposit.nullifier, nullifier: deposit.nullifier,
secret: deposit.secret, secret: deposit.secret,
pathElements: path_elements, pathElements: path_elements,
pathIndices: path_index, pathIndices: path_index
} }
console.log('Generating SNARK proof') console.log('Generating SNARK proof')
@ -206,22 +213,40 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
const relayerStatus = await axios.get(relayerURL + '/status') const relayerStatus = await axios.get(relayerURL + '/status')
const { rewardAccount, netId, ethPrices, tornadoServiceFee } = relayerStatus.data const { rewardAccount, netId, ethPrices, tornadoServiceFee } = relayerStatus.data
assert(netId === await web3.eth.net.getId() || netId === '*', 'This relay is for different network') assert(netId === (await web3.eth.net.getId()) || netId === '*', 'This relay is for different network')
console.log('Relay address: ', rewardAccount) console.log('Relay address: ', rewardAccount)
const gasPrice = await fetchGasPrice() const gasPrice = await fetchGasPrice()
const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals const decimals = isLocalRPC ? 18 : config.deployments[`netId${netId}`][currency].decimals
const fee = calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerServiceFee: tornadoServiceFee, decimals }) const fee = calculateFee({
currency,
gasPrice,
amount,
refund,
ethPrices,
relayerServiceFee: tornadoServiceFee,
decimals
})
if (fee.gt(fromDecimals({ amount, decimals }))) { if (fee.gt(fromDecimals({ amount, decimals }))) {
throw new Error('Too high refund') throw new Error('Too high refund')
} }
const { proof, args } = await generateProof({ deposit, recipient, relayerAddress: rewardAccount, fee, refund }) const { proof, args } = await generateProof({
deposit,
recipient,
relayerAddress: rewardAccount,
fee,
refund
})
console.log('Sending withdraw transaction through relay') console.log('Sending withdraw transaction through relay')
try { try {
const response = await axios.post(relayerURL + '/v1/tornadoWithdraw', { contract: tornadoInstance, proof, args }) const response = await axios.post(relayerURL + '/v1/tornadoWithdraw', {
contract: tornadoInstance,
proof,
args
})
const { id } = response.data const { id } = response.data
@ -234,25 +259,29 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
console.error(e.message) console.error(e.message)
} }
} }
} else { // using private key } else {
// using private key
const { proof, args } = await generateProof({ deposit, recipient, refund }) const { proof, args } = await generateProof({ deposit, recipient, refund })
console.log('Submitting withdraw transaction') console.log('Submitting withdraw transaction')
await tornado.methods.withdraw(tornadoInstance, proof, ...args).send({ from: senderAccount, value: refund.toString(), gas: 1e6 }) await tornado.methods
.withdraw(tornadoInstance, proof, ...args)
.send({ from: senderAccount, value: refund.toString(), gas: 1e6 })
.on('transactionHash', function (txHash) { .on('transactionHash', function (txHash) {
if (netId === 1 || netId === 42) { if (netId === 1 || netId === 42) {
console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`) console.log(`View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`)
} else { } else {
console.log(`The transaction hash is ${txHash}`) console.log(`The transaction hash is ${txHash}`)
} }
}).on('error', function (e) { })
.on('error', function (e) {
console.error('on transactionHash error', e.message) console.error('on transactionHash error', e.message)
}) })
} }
console.log('Done') console.log('Done')
} }
function getStatus (id, relayerURL) { function getStatus(id, relayerURL) {
return new Promise((resolve) => { return new Promise((resolve) => {
async function getRelayerStatus() { async function getRelayerStatus() {
const responseStatus = await axios.get(relayerURL + '/v1/jobs/' + id) const responseStatus = await axios.get(relayerURL + '/v1/jobs/' + id)
@ -268,7 +297,9 @@ function getStatus (id, relayerURL) {
if (status === 'CONFIRMED') { if (status === 'CONFIRMED') {
const receipt = await waitForTxReceipt({ txHash }) const receipt = await waitForTxReceipt({ txHash })
console.log(`Transaction submitted through the relay. View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`) console.log(
`Transaction submitted through the relay. View transaction on etherscan https://${getCurrentNetworkName()}etherscan.io/tx/${txHash}`
)
console.log('Transaction mined in block', receipt.blockNumber) console.log('Transaction mined in block', receipt.blockNumber)
resolve(status) resolve(status)
} }
@ -301,9 +332,7 @@ function fromDecimals({ amount, decimals }) {
// Split it into a whole and fractional part // Split it into a whole and fractional part
const comps = ether.split('.') const comps = ether.split('.')
if (comps.length > 2) { if (comps.length > 2) {
throw new Error( throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points')
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal points'
)
} }
let whole = comps[0] let whole = comps[0]
@ -316,9 +345,7 @@ function fromDecimals({ amount, decimals }) {
fraction = '0' fraction = '0'
} }
if (fraction.length > baseLength) { if (fraction.length > baseLength) {
throw new Error( throw new Error('[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places')
'[ethjs-unit] while converting number ' + amount + ' to wei, too many decimal places'
)
} }
while (fraction.length < baseLength) { while (fraction.length < baseLength) {
@ -373,14 +400,13 @@ function toDecimals(value, decimals, fixed) {
function getCurrentNetworkName() { function getCurrentNetworkName() {
switch (netId) { switch (netId) {
case 1: case 1:
return '' return ''
case 5: case 5:
return 'goerli.' return 'goerli.'
case 42: case 42:
return 'kovan.' return 'kovan.'
} }
} }
function gasPrices(value = 80) { function gasPrices(value = 80) {
@ -402,26 +428,26 @@ async function fetchGasPrice() {
} }
function calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerServiceFee, decimals }) { function calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerServiceFee, decimals }) {
const decimalsPoint = Math.floor(relayerServiceFee) === Number(relayerServiceFee) ? const decimalsPoint =
0 : Math.floor(relayerServiceFee) === Number(relayerServiceFee) ? 0 : relayerServiceFee.toString().split('.')[1].length
relayerServiceFee.toString().split('.')[1].length
const roundDecimal = 10 ** decimalsPoint const roundDecimal = 10 ** decimalsPoint
const total = toBN(fromDecimals({ amount, decimals })) const total = toBN(fromDecimals({ amount, decimals }))
const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100)) const feePercent = total.mul(toBN(relayerServiceFee * roundDecimal)).div(toBN(roundDecimal * 100))
const expense = toBN(gasPrice).mul(toBN(5e5)) const expense = toBN(gasPrice).mul(toBN(5e5))
let desiredFee let desiredFee
switch (currency) { switch (currency) {
case 'eth': { case 'eth': {
desiredFee = expense.add(feePercent) desiredFee = expense.add(feePercent)
break break
} }
default: { default: {
desiredFee = expense.add(toBN(refund)) desiredFee = expense
.mul(toBN(10 ** decimals)) .add(toBN(refund))
.div(toBN(ethPrices[currency])) .mul(toBN(10 ** decimals))
desiredFee = desiredFee.add(feePercent) .div(toBN(ethPrices[currency]))
break desiredFee = desiredFee.add(feePercent)
} break
}
} }
return desiredFee return desiredFee
} }
@ -467,7 +493,12 @@ function parseNote(noteString) {
const deposit = createDeposit({ nullifier, secret }) const deposit = createDeposit({ nullifier, secret })
const netId = Number(match.groups.netId) const netId = Number(match.groups.netId)
return { currency: match.groups.currency, amount: match.groups.amount, netId, deposit } return {
currency: match.groups.currency,
amount: match.groups.amount,
netId,
deposit
}
} }
async function loadDepositData({ deposit }) { async function loadDepositData({ deposit }) {
@ -488,7 +519,13 @@ async function loadDepositData({ deposit }) {
const isSpent = await tornado.methods.isSpent(deposit.nullifierHex).call() const isSpent = await tornado.methods.isSpent(deposit.nullifierHex).call()
const receipt = await web3.eth.getTransactionReceipt(txHash) const receipt = await web3.eth.getTransactionReceipt(txHash)
return { timestamp, txHash, isSpent, from: receipt.from, commitment: deposit.commitmentHex } return {
timestamp,
txHash,
isSpent,
from: receipt.from,
commitment: deposit.commitmentHex
}
} catch (e) { } catch (e) {
console.error('loadDepositData', e) console.error('loadDepositData', e)
} }
@ -507,9 +544,7 @@ async function loadWithdrawalData({ amount, currency, deposit }) {
const fee = withdrawEvent.returnValues.fee const fee = withdrawEvent.returnValues.fee
const decimals = config.deployments[`netId${netId}`][currency].decimals const decimals = config.deployments[`netId${netId}`][currency].decimals
const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub( const withdrawalAmount = toBN(fromDecimals({ amount, decimals })).sub(toBN(fee))
toBN(fee)
)
const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockHash) const { timestamp } = await web3.eth.getBlock(withdrawEvent.blockHash)
return { return {
amount: toDecimals(withdrawalAmount, decimals, 9), amount: toDecimals(withdrawalAmount, decimals, 9),
@ -533,7 +568,9 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100' }) {
if (inBrowser) { if (inBrowser) {
// Initialize using injected web3 (Metamask) // Initialize using injected web3 (Metamask)
// To assemble web version run `npm run browserify` // To assemble web version run `npm run browserify`
web3 = new Web3(window.web3.currentProvider, null, { transactionConfirmationBlocks: 1 }) web3 = new Web3(window.web3.currentProvider, null, {
transactionConfirmationBlocks: 1
})
contractJson = await (await fetch('build/contracts/TornadoProxy.abi.json')).json() contractJson = await (await fetch('build/contracts/TornadoProxy.abi.json')).json()
mixerJson = await (await fetch('build/contracts/Mixer.abi.json')).json() mixerJson = await (await fetch('build/contracts/Mixer.abi.json')).json()
circuit = await (await fetch('build/circuits/tornado.json')).json() circuit = await (await fetch('build/circuits/tornado.json')).json()
@ -616,7 +653,9 @@ async function main() {
.option('-R, --relayer <URL>', 'Withdraw via relayer') .option('-R, --relayer <URL>', 'Withdraw via relayer')
program program
.command('deposit <currency> <amount>') .command('deposit <currency> <amount>')
.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://tornado.cash.') .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://tornado.cash.'
)
.action(async (currency, amount) => { .action(async (currency, amount) => {
currency = currency.toLowerCase() currency = currency.toLowerCase()
await init({ rpc: program.rpc, currency, amount }) await init({ rpc: program.rpc, currency, amount })
@ -624,11 +663,20 @@ async function main() {
}) })
program program
.command('withdraw <note> <recipient> [ETH_purchase]') .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.') .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) => { .action(async (noteString, recipient, refund) => {
const { currency, amount, netId, deposit } = parseNote(noteString) const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ rpc: program.rpc, noteNetId: netId, currency, amount }) await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
await withdraw({ deposit, currency, amount, recipient, refund, relayerURL: program.relayer }) await withdraw({
deposit,
currency,
amount,
recipient,
refund,
relayerURL: program.relayer
})
}) })
program program
.command('balance <address> [token_address]') .command('balance <address> [token_address]')
@ -642,7 +690,9 @@ async function main() {
}) })
program program
.command('compliance <note>') .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.') .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) => { .action(async (noteString) => {
const { currency, amount, netId, deposit } = parseNote(noteString) const { currency, amount, netId, deposit } = parseNote(noteString)
await init({ rpc: program.rpc, noteNetId: netId, currency, amount }) await init({ rpc: program.rpc, noteNetId: netId, currency, amount })
@ -658,7 +708,11 @@ async function main() {
console.log('The note was not spent') console.log('The note was not spent')
} }
const withdrawInfo = await loadWithdrawalData({ amount, currency, deposit }) const withdrawInfo = await loadWithdrawalData({
amount,
currency,
deposit
})
const withdrawalDate = new Date(withdrawInfo.timestamp * 1000) const withdrawalDate = new Date(withdrawInfo.timestamp * 1000)
console.log('\n=============Withdrawal==============') console.log('\n=============Withdrawal==============')
console.log('Withdrawal :', withdrawInfo.amount, currency) console.log('Withdrawal :', withdrawInfo.amount, currency)
@ -678,15 +732,28 @@ async function main() {
await init({ rpc: program.rpc, currency, amount }) await init({ rpc: program.rpc, currency, amount })
let noteString = await deposit({ currency, amount }) let noteString = await deposit({ currency, amount })
let parsedNote = parseNote(noteString) let parsedNote = parseNote(noteString)
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, relayerURL: program.relayer }) await withdraw({
deposit: parsedNote.deposit,
currency,
amount,
recipient: senderAccount,
relayerURL: program.relayer
})
console.log('\nStart performing DAI deposit-withdraw test') console.log('\nStart performing DAI deposit-withdraw test')
currency = 'dai' currency = 'dai'
amount = '100' amount = '100'
await init({ rpc: program.rpc, currency, amount }) await init({ rpc: program.rpc, currency, amount })
noteString = await deposit({ currency, amount }) noteString = await deposit({ currency, amount })
; (parsedNote = parseNote(noteString)) parsedNote = parseNote(noteString)
await withdraw({ deposit: parsedNote.deposit, currency, amount, recipient: senderAccount, refund: '0.02', relayerURL: program.relayer }) await withdraw({
deposit: parsedNote.deposit,
currency,
amount,
recipient: senderAccount,
refund: '0.02',
relayerURL: program.relayer
})
}) })
try { try {
await program.parseAsync(process.argv) await program.parseAsync(process.argv)