Compare commits

...

2 Commits

3 changed files with 141 additions and 78 deletions

@ -71,7 +71,7 @@ $ node cli.js withdraw <note> <recipient> --rpc <rpc url> --relayer <relayer url
Note that `--relayer <relayer url>`, `--tor <torPort>` and `--rpc <rpc url>` are optional parameters, and use `--private-key <private key>` only if you withdraw without relayer.
You can don't provide RPC link and withdrawal will be made via default RPC for the chain to which note belongs.
If you want to use Tornado Cash relayer for your first withdrawal to your new ethereum account, you can use `listRelayers` command to get list of available relayers for your chain.
If you want to use Tornado Cash relayer for your first withdrawal to your new ethereum account, you can use `listRelayers` command to get list of available relayers for your chain or just don't specify relayer at all - it will fetch relayer autimatic, as in UI.
If you don't need relayer while doing withdrawals, you must provide your withdrawal account's private key - either as parameter, or by adding it to `.env` file.

215
cli.js

@ -461,8 +461,8 @@ async function generateProof({ deposit, currency, amount, recipient, relayerAddr
* @param noteString Note to withdraw
* @param recipient Recipient address
*/
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund }) {
let options = {};
async function withdraw({ deposit, currency, amount, recipient, relayerURL, refund, privateKey }) {
let options = {timeout: 5000};
if (currency === netSymbol.toLowerCase() && refund && refund !== '0') {
throw new Error('The ETH purchase is supposed to be 0 for ETH withdrawals');
}
@ -474,30 +474,58 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
throw new Error('Recipient address is not valid');
}
if (relayerURL) {
if (relayerURL.endsWith('.eth')) {
throw new Error(
'ENS name resolving is not supported. Please provide DNS name of the relayer. See instuctions in README.md'
);
}
if (torPort) {
if (privateKey || process.env.PRIVATE_KEY) {
// 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() == senderAccount.toLowerCase(),
'Withdrawal recepient mismatches with the account of provided private key from environment file'
);
const checkBalance = await web3.eth.getBalance(senderAccount);
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,
tornado.methods.withdraw(tornadoInstance, proof, ...args).encodeABI(),
toBN(args[5]),
'user_withdrawal'
);
}
else {
if (torPort)
options = {
...options,
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' }
};
let relayerInfo;
if (relayerURL) {
try {
relayerURL = new URL(relayerURL).origin;
res = await axios.get(relayerURL + '/status', options);
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}`)
}
let relayerStatus;
try {
relayerURL = new URL(relayerURL).origin;
relayerStatus = await axios.get(relayerURL + '/status', options);
} catch (err) {
console.error(err);
throw new Error('Cannot get relayer status');
}
const { rewardAccount, netId, ethPrices, tornadoServiceFee } = relayerStatus.data;
assert(netId === (await web3.eth.net.getId()) || netId === '*', 'This relay is for different network');
const { rewardAccount, netId: relayerNetId, ethPrices, tornadoServiceFee } = relayerInfo;
assert(relayerNetId === (await web3.eth.net.getId()) || relayerNetId === '*', 'This relay is for different network');
console.log('Relay address:', rewardAccount);
const decimals = isTestRPC ? 18 : config.deployments[`netId${netId}`]['tokens'][currency].decimals;
@ -577,27 +605,8 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, refu
} catch (e) {
console.error(e.message);
}
} else {
// 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() == senderAccount.toLowerCase(),
'Withdrawal recepient mismatches with the account of provided private key from environment file'
);
const checkBalance = await web3.eth.getBalance(senderAccount);
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,
tornado.methods.withdraw(tornadoInstance, proof, ...args).encodeABI(),
toBN(args[5]),
'user_withdrawal'
);
}
if (currency === netSymbol.toLowerCase()) {
await printETHBalance({ address: recipient, name: 'Recipient' });
} else {
@ -914,9 +923,10 @@ async function selectDefaultRpc(chainId){
/**
* Get available relayers data for selected chain
* @param {string | number} chainId
* @returns {Array<Object>} List of available relayers
* @returns {Promise<Array<Object>>} List of available relayers
*/
async function getRelayers(chainId){
console.log("Fetching relayers...")
let localWeb3 = web3;
if (netId != 1) {
mainnetRpc = await selectDefaultRpc(1);
@ -931,12 +941,20 @@ async function getRelayers(chainId){
const relayerGraphApi = config.deployments["netId1"].relayerSubgraph;
const relayerSubdomains = Object.values(config.deployments).map(({ ensSubdomainKey }) => ensSubdomainKey)
let options;
if (torPort) {
options = {
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' }
};
}
async function fetchGraphRelayers() {
try{
const res = await axios.post(relayerGraphApi, { 'query': '{ relayers(first: 1000) {\n id\n address\n ensName\n ensHash\n }\n}' });
const res = await axios.post(relayerGraphApi, { 'query': '{ relayers(first: 1000) {\n id\n address\n ensName\n ensHash\n }\n}' }, options);
return res.data.data.relayers;
} catch(e){
console.error("Relayers subgraph API not responding");
throw new Error("Relayers subgraph API not responding");
}
}
@ -981,12 +999,12 @@ async function getRelayers(chainId){
let statuses = [];
for (const relayer of relayers) {
try {
const res = await axios.get(`https://${relayer.hostname}/status`, {timeout: 5000});
const res = await axios.get(`https://${relayer.hostname}/status`, Object.assign({timeout: 5000}, options));
const statusData = res.data;
if (statusData.rewardAccount && statusData.health.status == 'true') {
statuses.push({
...relayer,
statusData
...statusData
});
}
} catch (e) {
@ -1001,9 +1019,60 @@ async function getRelayers(chainId){
const validRelayers = await getValidRelayers(registeredRelayers, 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 (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]
}
/**
* Erase all zero events from events tree array
* @param events Events tree array
@ -1014,16 +1083,14 @@ function filterZeroEvents(events) {
function loadCachedEvents({ type, currency, amount }) {
try {
const module = require(`./cache/${netName.toLowerCase()}/${type}s_${currency.toLowerCase()}_${amount}.json`);
const events = require(`./cache/${netName.toLowerCase()}/${type}s_${currency.toLowerCase()}_${amount}.json`);
if (module) {
const events = module;
return {
events: filterZeroEvents(events),
lastBlock: events[events.length - 1].blockNumber
};
}
if (!events || events.length === 0) throw new Error("Invalid cached events file")
return {
events: filterZeroEvents(events),
lastBlock: events[events.length - 1].blockNumber
};
} catch (err) {
console.log('Error fetching cached files, syncing from block', deployedBlockNumber);
return {
@ -1045,15 +1112,23 @@ async function fetchEvents({ type, currency, amount, filterEvents }) {
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);
try {
const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency.toLowerCase()}_${amount}.json`;
const localEvents = await initJson(fileName);
const events = filterZeroEvents(localEvents).concat(fetchedEvents);
const savedEvents = await initJson(fileName);
const cachedEvents = filterZeroEvents(savedEvents);
// Because we fetch some events twice from graph, and we assume that cached events are sorted, we can erase duplicated events simply from the start of fetched array
const deduplicatedFetchedEvents = fetchedEvents.slice(fetchedEvents.findIndex(event => event?.transactionHash === cachedEvents[cachedEvents.length - 1]?.transactionHash) + 1)
const events = cachedEvents.concat(deduplicatedFetchedEvents);
fs.writeFileSync(fileName, JSON.stringify(events, null, 2), 'utf8');
} catch (error) {
console.log(error)
throw new Error('Writing cache file failed:', error);
}
}
@ -1276,28 +1351,15 @@ async function fetchEvents({ type, currency, amount, filterEvents }) {
const latestTimestamp = await queryLatestTimestamp();
if (latestTimestamp) {
const getCachedBlock = await web3.eth.getBlock(startBlock);
console.log(getCachedBlock)
const cachedTimestamp = getCachedBlock.timestamp;
for (let i = cachedTimestamp; i < latestTimestamp; ) {
const result = await queryFromGraph(i);
if (Object.keys(result).length === 0) {
i = latestTimestamp;
} else {
if (type === 'deposit') {
const resultBlock = result[result.length - 1].blockNumber;
const resultTimestamp = result[result.length - 1].timestamp;
await updateCache(result);
i = resultTimestamp;
console.log('Fetched', amount, currency.toUpperCase(), type, 'events to block:', Number(resultBlock));
} else {
const resultBlock = result[result.length - 1].blockNumber;
const getResultBlock = await web3.eth.getBlock(resultBlock);
const resultTimestamp = getResultBlock.timestamp;
await updateCache(result);
i = resultTimestamp;
console.log('Fetched', amount, currency.toUpperCase(), type, 'events to block:', Number(resultBlock));
}
}
const result = await queryFromGraph(i - 1);
if (Object.keys(result).length === 0) break;
const resultBlockNumber = result[result.length - 1].blockNumber;
const resultTimestamp = type === "deposit" ? result[result.length - 1].timestamp : (await web3.eth.getBlock(resultBlockNumber)).timestamp;
await updateCache(result);
i = resultTimestamp;
console.log('Fetched', amount, currency.toUpperCase(), type, 'events to block:', Number(resultBlockNumber));
}
} else {
console.log('Fallback to web3 events');
@ -1686,6 +1748,7 @@ async function main() {
amount,
recipient,
refund,
privateKey: program.privateKey,
relayerURL: program.relayer
});
});
@ -1703,7 +1766,7 @@ async function main() {
'hostname': 'https://' + relayer.hostname + '/',
'ensName': relayer.ensName,
'stakeBalance': Number(web3.utils.fromWei(relayer.stakeBalance, 'ether')).toFixed(2)+" TORN",
'tornadoServiceFee': relayer.statusData.tornadoServiceFee + "%"
'tornadoServiceFee': relayer.tornadoServiceFee + "%"
});
}
});

@ -136,7 +136,7 @@ module.exports = {
ensSubdomainKey: 'bsc-tornado',
proxy: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
multicall: '0x41263cBA59EB80dC200F3E2544eda4ed6A90E76C',
subgraph: 'https://tornadocash-rpc.com/subgraphs/name/tornadocash/bsc-tornado-subgraph',
subgraph: 'https://gateway.thegraph.com/api/6a217817dd87d33db10beed79b044a91/subgraphs/id/CiwGzefDBZCavXRPnwarnnF8xDDoLw4boBuySomJWYnV',
defaultRpc: 'https://1rpc.io/bnb'
},
netId100: {