mirror of
https://github.com/tornadocash/tornado-cli.git
synced 2024-11-21 17:27:07 +01:00
Merge pull request #20 from 0xAyanami/develop
Bug fixes & Minor updates
This commit is contained in:
commit
80a810e727
72
README.md
72
README.md
@ -1,13 +1,40 @@
|
||||
# Warning!
|
||||
# Tornado cli
|
||||
|
||||
Command line tool to interact with [Tornado Cash](https://tornadocash.eth.link).
|
||||
|
||||
### Warning!
|
||||
Current cli version doesn't support [Anonymity Mining](https://tornado-cash.medium.com/tornado-cash-governance-proposal-a55c5c7d0703)
|
||||
|
||||
### Goerli, Mainnet, Binance Smart Chain
|
||||
### How to install tornado cli
|
||||
Download and install [node.js](https://nodejs.org/en/download/).
|
||||
|
||||
If you have git installed on your system, clone the master branch.
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/tornadocash/tornado-cli
|
||||
```
|
||||
|
||||
Or, download the archive file from github
|
||||
|
||||
https://github.com/tornadocash/tornado-cli/archive/refs/heads/master.zip
|
||||
|
||||
After downloading or cloning the repository, you must install necessary libraries using the following command.
|
||||
|
||||
```bash
|
||||
$ cd tornado-cli
|
||||
$ npm install
|
||||
```
|
||||
|
||||
If you want to use Tor connection to conceal ip address, install [Tor Browser](https://www.torproject.org/download/) and add `--tor 9150` for `cli.js` if you connect tor with browser.
|
||||
Note that you should reset your tor connection by restarting the browser every time when you deposit & withdraw otherwise you will have the same exit node used for connection.
|
||||
|
||||
### Goerli, Mainnet, Binance Smart Chain, Gnosis Chain, Polygon Network, Arbitrum, Avalanche
|
||||
1. Add `PRIVATE_KEY` to `.env` file
|
||||
2. `./cli.js --help`
|
||||
2. `node cli.js --help`
|
||||
|
||||
Example:
|
||||
```bash
|
||||
$ ./cli.js deposit ETH 0.1 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --tor 9050
|
||||
$ node cli.js deposit ETH 0.1 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --tor 9150
|
||||
|
||||
Your note: tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652
|
||||
Tornado ETH balance is 8.9
|
||||
@ -18,7 +45,7 @@ Sender account ETH balance is 1004873.361652048361352542
|
||||
```
|
||||
|
||||
```bash
|
||||
$ ./cli.js withdraw tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --relayer https://goerli-frelay.duckdns.org --tor 9050
|
||||
$ node cli.js withdraw tornado-eth-0.1-5-0xf73dd6833ccbcc046c44228c8e2aa312bf49e08389dadc7c65e6a73239867b7ef49c705c4db227e2fadd8489a494b6880bdcb6016047e019d1abec1c7652 0x8589427373D6D84E98730D7795D8f6f8731FDA16 --rpc https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161 --relayer https://goerli-frelay.duckdns.org --tor 9150
|
||||
|
||||
Relay address: 0x6A31736e7490AbE5D5676be059DFf064AB4aC754
|
||||
Getting current state from tornado contract
|
||||
@ -520,6 +547,41 @@ Infura API key fetched from https://rpc.info (Same one with Metamask)
|
||||
"cachedUrl":"https://goerli-v2.defidevotee.xyz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"netId10":{
|
||||
"rpcUrls":{
|
||||
"Optimism":{
|
||||
"name":"Optimism Public RPC",
|
||||
"url":"https://mainnet.optimism.io"
|
||||
}
|
||||
},
|
||||
"relayers":{
|
||||
"optimism.t-relay.eth":{
|
||||
"url":"optimism.t-relay.eth",
|
||||
"name":"optimism.t-relay.eth",
|
||||
"cachedUrl":"https://optimism.t-relay.online/"
|
||||
},
|
||||
"optimism.therelayer.eth":{
|
||||
"url":"optimism.therelayer.eth",
|
||||
"name":"optimism.therelayer.eth",
|
||||
"cachedUrl":"https://optimism.therelayer.xyz/"
|
||||
},
|
||||
"optimism.relayer-service.eth":{
|
||||
"url":"optimism.relayer-service.eth",
|
||||
"name":"optimism.relayer-service.eth",
|
||||
"cachedUrl":"https://optimism-relayer.hertz.zone/"
|
||||
},
|
||||
"optimism.torn.eth":{
|
||||
"url":"optimism.torn.eth",
|
||||
"name":"optimism.torn.eth",
|
||||
"cachedUrl":"https://optimism.torn.cash/"
|
||||
},
|
||||
"optimism.relaymy.eth":{
|
||||
"url":"optimism.relaymy.eth",
|
||||
"name":"optimism.relaymy.eth",
|
||||
"cachedUrl":"https://optimism.relaymy.xyz/"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
264
cli.js
264
cli.js
@ -71,7 +71,11 @@ async function generateTransaction(to, encodedData, value = 0) {
|
||||
const bumped = Math.floor(fetchedGas * 1.3)
|
||||
gasLimit = web3.utils.toHex(bumped)
|
||||
}
|
||||
await estimateGas();
|
||||
if (encodedData) {
|
||||
await estimateGas();
|
||||
} else {
|
||||
gasLimit = web3.utils.toHex(21000);
|
||||
}
|
||||
|
||||
async function txoptions() {
|
||||
// Generate EIP-1559 transaction
|
||||
@ -85,6 +89,16 @@ async function generateTransaction(to, encodedData, value = 0) {
|
||||
gas : gasLimit,
|
||||
data : encodedData
|
||||
}
|
||||
} else if (netId == 137 || netId == 43114) {
|
||||
tx = {
|
||||
to : to,
|
||||
value : value,
|
||||
nonce : nonce,
|
||||
maxFeePerGas : gasPrice,
|
||||
maxPriorityFeePerGas : gasPrice,
|
||||
gas : gasLimit,
|
||||
data : encodedData
|
||||
}
|
||||
} else {
|
||||
tx = {
|
||||
to : to,
|
||||
@ -144,7 +158,7 @@ async function deposit({ currency, amount }) {
|
||||
const noteString = `tornado-${currency}-${amount}-${netId}-${note}`
|
||||
console.log(`Your note: ${noteString}`)
|
||||
await backupNote({ currency, amount, netId, note, noteString })
|
||||
if (currency === 'eth' || currency === 'bnb' || currency === 'xdai' || currency === 'matic' || currency === 'avax') {
|
||||
if (currency === netSymbol.toLowerCase()) {
|
||||
await printETHBalance({ address: tornadoContract._address, name: 'Tornado contract', symbol: currency.toUpperCase() })
|
||||
await printETHBalance({ address: senderAccount, name: 'Sender account', symbol: currency.toUpperCase() })
|
||||
const value = isLocalRPC ? ETH_AMOUNT : fromDecimals({ amount, decimals: 18 })
|
||||
@ -269,7 +283,7 @@ async function generateProof({ deposit, currency, amount, recipient, relayerAddr
|
||||
*/
|
||||
async function withdraw({ deposit, currency, amount, recipient, relayerURL, torPort, refund = '0' }) {
|
||||
let options = {};
|
||||
if (currency === 'eth' && refund !== '0') {
|
||||
if (currency === netSymbol.toLowerCase() && refund !== '0') {
|
||||
throw new Error('The ETH purchase is supposted to be 0 for ETH withdrawals')
|
||||
}
|
||||
refund = toWei(refund)
|
||||
@ -328,7 +342,7 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, torP
|
||||
|
||||
// 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.
|
||||
const { address } = await web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
|
||||
assert(recipient.toLowerCase() == address.toLowerCase(), 'Withdrawal amount recepient',recipient,'mismatches with the account of provided private key from environment file',address)
|
||||
assert(recipient.toLowerCase() == address.toLowerCase(), 'Withdrawal amount recepient mismatches with the account of provided private key from environment file')
|
||||
|
||||
const { proof, args } = await generateProof({ deposit, currency, amount, recipient, refund })
|
||||
|
||||
@ -338,6 +352,53 @@ async function withdraw({ deposit, currency, amount, recipient, relayerURL, torP
|
||||
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 }) {
|
||||
// using private key
|
||||
assert(senderAccount != null, 'Error! PRIVATE_KEY not found. Please provide PRIVATE_KEY in .env file if you send')
|
||||
if (tokenAddress) {
|
||||
const erc20ContractJson = require('./build/contracts/ERC20Mock.json')
|
||||
erc20 = new web3.eth.Contract(erc20ContractJson.abi, tokenAddress)
|
||||
const balance = await erc20.methods.balanceOf(senderAccount).call()
|
||||
const decimals = await erc20.methods.decimals().call()
|
||||
const toSend = amount * Math.pow(10, decimals)
|
||||
if (balance < toSend) {
|
||||
console.error("You have",toDecimals(balance, decimals, (balance.length + decimals)).toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ","),(await erc20.methods.symbol().call()),", you can't send more than you have")
|
||||
process.exit(1);
|
||||
}
|
||||
await generateTransaction(tokenAddress, await erc20.methods.transfer(address, toBN(toSend)).encodeABI())
|
||||
console.log('Sent',amount,(await erc20.methods.symbol().call()),'to',address);
|
||||
} else {
|
||||
const balance = await web3.eth.getBalance(senderAccount)
|
||||
if (balance == 0) {
|
||||
console.error("You have 0 balance, can't send")
|
||||
process.exit(1);
|
||||
}
|
||||
if (!amount) {
|
||||
console.log('Amount not defined, sending all available amounts')
|
||||
const gasPrice = await fetchGasPrice()
|
||||
const gasLimit = 21000;
|
||||
if (netId == 1 || netId == 5) {
|
||||
const priorityFee = await gasPrices(3)
|
||||
amount = (balance - (gasLimit * (parseInt(gasPrice) + parseInt(priorityFee))))
|
||||
} else {
|
||||
amount = (balance - (gasLimit * parseInt(gasPrice)))
|
||||
}
|
||||
}
|
||||
if (balance < amount) {
|
||||
console.error("You have",web3.utils.fromWei(toHex(balance)),netSymbol,", you can't send more than you have.")
|
||||
process.exit(1);
|
||||
}
|
||||
await generateTransaction(address, null, amount)
|
||||
console.log('Sent',web3.utils.fromWei(toHex(amount)),netSymbol,'to',address);
|
||||
}
|
||||
}
|
||||
|
||||
function getStatus(id, relayerURL, options) {
|
||||
return new Promise((resolve) => {
|
||||
async function getRelayerStatus() {
|
||||
@ -476,6 +537,8 @@ function getExplorerLink() {
|
||||
return 'goerli.etherscan.io'
|
||||
case 42:
|
||||
return 'kovan.etherscan.io'
|
||||
case 10:
|
||||
return 'optimistic.etherscan.io'
|
||||
default:
|
||||
return 'etherscan.io'
|
||||
}
|
||||
@ -500,6 +563,8 @@ function getCurrentNetworkName() {
|
||||
return 'Goerli'
|
||||
case 42:
|
||||
return 'Kovan'
|
||||
case 137:
|
||||
return 'Optimism'
|
||||
default:
|
||||
return 'localRPC'
|
||||
}
|
||||
@ -562,23 +627,7 @@ function calculateFee({ currency, gasPrice, amount, refund, ethPrices, relayerSe
|
||||
const expense = toBN(gasPrice).mul(toBN(5e5))
|
||||
let desiredFee
|
||||
switch (currency) {
|
||||
case 'eth': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
case 'bnb': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
case 'xdai': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
case 'matic': {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
case 'avax': {
|
||||
case netSymbol.toLowerCase(): {
|
||||
desiredFee = expense.add(feePercent)
|
||||
break
|
||||
}
|
||||
@ -618,6 +667,21 @@ function waitForTxReceipt({ txHash, attempts = 60, delay = 1000 }) {
|
||||
})
|
||||
}
|
||||
|
||||
function initJson(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(file, 'utf8', (error, data) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (error) {
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function loadCachedEvents({ type, currency, amount }) {
|
||||
try {
|
||||
const module = require(`./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`)
|
||||
@ -640,10 +704,6 @@ function loadCachedEvents({ type, currency, amount }) {
|
||||
}
|
||||
|
||||
async function fetchEvents({ type, currency, amount}) {
|
||||
let leafIndex = -1
|
||||
let events = [];
|
||||
let fetchedEvents, chunks, targetBlock;
|
||||
|
||||
if (type === "withdraw") {
|
||||
type = "withdrawal"
|
||||
}
|
||||
@ -651,75 +711,86 @@ async function fetchEvents({ type, currency, amount}) {
|
||||
const cachedEvents = loadCachedEvents({ type, currency, amount })
|
||||
const startBlock = cachedEvents.lastBlock + 1
|
||||
|
||||
console.log("Loaded cached",amount,currency.toUpperCase(),type,"events for",startBlock,"block")
|
||||
console.log("Fetching",amount,currency.toUpperCase(),type,"events for",netName,"network")
|
||||
|
||||
async function fetchLatestEvents() {
|
||||
targetBlock = await web3.eth.getBlockNumber();
|
||||
chunks = 1000;
|
||||
fetchedEvents = [];
|
||||
for (let i=startBlock; i < targetBlock; i+=chunks) {
|
||||
await tornadoContract.getPastEvents(capitalizeFirstLetter(type), {
|
||||
fromBlock: i,
|
||||
toBlock: i+chunks-1,
|
||||
}).then(r => { fetchedEvents = fetchedEvents.concat(r); console.log("Fetched",amount,currency.toUpperCase(),type,"events from block:", i) }, err => { console.error(i + " failed fetching",type,"events from node", err) }).catch(console.log);
|
||||
}
|
||||
}
|
||||
await fetchLatestEvents()
|
||||
|
||||
async function mapDepositEvents() {
|
||||
fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => {
|
||||
const { commitment, leafIndex, timestamp } = returnValues
|
||||
return {
|
||||
blockNumber,
|
||||
transactionHash,
|
||||
commitment,
|
||||
leafIndex: Number(leafIndex),
|
||||
timestamp
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function mapWithdrawEvents() {
|
||||
fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => {
|
||||
const { nullifierHash, to, fee } = returnValues
|
||||
return {
|
||||
blockNumber,
|
||||
transactionHash,
|
||||
nullifierHash,
|
||||
to,
|
||||
fee
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function mapLatestEvents() {
|
||||
console.log("Mapping",amount,currency.toUpperCase(),type,"events, please wait")
|
||||
if (type === "deposit"){
|
||||
await mapDepositEvents();
|
||||
} else {
|
||||
await mapWithdrawEvents();
|
||||
}
|
||||
}
|
||||
await mapLatestEvents();
|
||||
|
||||
console.log("Gathering cached events + collected events from node")
|
||||
|
||||
async function concatEvents() {
|
||||
events = cachedEvents.events.concat(fetchedEvents)
|
||||
}
|
||||
await concatEvents();
|
||||
|
||||
console.log('Total events:', events.length)
|
||||
|
||||
async function updateCache() {
|
||||
async function syncEvents() {
|
||||
try {
|
||||
await fs.writeFileSync(`./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`, JSON.stringify(events, null, 2), 'utf8')
|
||||
console.log("Cache updated for Tornado",type,amount,currency,"instance successfully")
|
||||
} catch (e) {
|
||||
throw new Error('Writing cache file failed:',e)
|
||||
let targetBlock = await web3.eth.getBlockNumber();
|
||||
let chunks = 1000;
|
||||
for (let i=startBlock; i < targetBlock; i+=chunks) {
|
||||
let fetchedEvents = [];
|
||||
async function fetchLatestEvents(i) {
|
||||
await tornadoContract.getPastEvents(capitalizeFirstLetter(type), {
|
||||
fromBlock: i,
|
||||
toBlock: i+chunks-1,
|
||||
}).then(r => { fetchedEvents = fetchedEvents.concat(r); console.log("Fetched",amount,currency.toUpperCase(),type,"events to block:", i) }, err => { console.error(i + " failed fetching",type,"events from node", err); process.exit(1); }).catch(console.log);
|
||||
}
|
||||
|
||||
async function mapDepositEvents() {
|
||||
fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => {
|
||||
const { commitment, leafIndex, timestamp } = returnValues
|
||||
return {
|
||||
blockNumber,
|
||||
transactionHash,
|
||||
commitment,
|
||||
leafIndex: Number(leafIndex),
|
||||
timestamp
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function mapWithdrawEvents() {
|
||||
fetchedEvents = fetchedEvents.map(({ blockNumber, transactionHash, returnValues }) => {
|
||||
const { nullifierHash, to, fee } = returnValues
|
||||
return {
|
||||
blockNumber,
|
||||
transactionHash,
|
||||
nullifierHash,
|
||||
to,
|
||||
fee
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function mapLatestEvents() {
|
||||
if (type === "deposit"){
|
||||
await mapDepositEvents();
|
||||
} else {
|
||||
await mapWithdrawEvents();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateCache() {
|
||||
try {
|
||||
const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`
|
||||
const localEvents = await initJson(fileName);
|
||||
const events = localEvents.concat(fetchedEvents);
|
||||
await fs.writeFileSync(fileName, JSON.stringify(events, null, 2), 'utf8')
|
||||
} catch (error) {
|
||||
throw new Error('Writing cache file failed:',error)
|
||||
}
|
||||
}
|
||||
await fetchLatestEvents(i);
|
||||
await mapLatestEvents();
|
||||
await updateCache();
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error("Error while updating cache")
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
await updateCache();
|
||||
await syncEvents();
|
||||
|
||||
async function loadUpdatedEvents() {
|
||||
const fileName = `./cache/${netName.toLowerCase()}/${type}s_${currency}_${amount}.json`
|
||||
const updatedEvents = await initJson(fileName);
|
||||
const updatedBlock = updatedEvents[updatedEvents.length - 1].blockNumber
|
||||
console.log("Cache updated for Tornado",type,amount,currency,"instance to block",updatedBlock,"successfully")
|
||||
console.log('Total events:', updatedEvents.length)
|
||||
return updatedEvents;
|
||||
}
|
||||
const events = await loadUpdatedEvents();
|
||||
|
||||
return events
|
||||
}
|
||||
@ -846,7 +917,12 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100', torPort,
|
||||
MERKLE_TREE_HEIGHT = process.env.MERKLE_TREE_HEIGHT || 20
|
||||
ETH_AMOUNT = process.env.ETH_AMOUNT
|
||||
TOKEN_AMOUNT = process.env.TOKEN_AMOUNT
|
||||
PRIVATE_KEY = process.env.PRIVATE_KEY
|
||||
const privKey = process.env.PRIVATE_KEY
|
||||
if (privKey.includes("0x")) {
|
||||
PRIVATE_KEY = process.env.PRIVATE_KEY.substring(2)
|
||||
} else {
|
||||
PRIVATE_KEY = process.env.PRIVATE_KEY
|
||||
}
|
||||
if (PRIVATE_KEY) {
|
||||
const account = web3.eth.accounts.privateKeyToAccount('0x' + PRIVATE_KEY)
|
||||
web3.eth.accounts.wallet.add('0x' + PRIVATE_KEY)
|
||||
@ -893,6 +969,7 @@ async function init({ rpc, noteNetId, currency = 'dai', amount = '100', torPort,
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
netSymbol = getCurrentNetworkSymbol()
|
||||
tornado = new web3.eth.Contract(contractJson, tornadoAddress)
|
||||
tornadoContract = new web3.eth.Contract(instanceJson, tornadoInstance)
|
||||
contractAddress = tornadoAddress
|
||||
@ -958,6 +1035,13 @@ async function main() {
|
||||
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) => {
|
||||
await init({ rpc: program.rpc, torPort: program.tor, balanceCheck: true })
|
||||
await send({ address, amount, tokenAddress })
|
||||
})
|
||||
program
|
||||
.command('compliance <note>')
|
||||
.description(
|
||||
|
20
config.js
20
config.js
@ -330,5 +330,25 @@ module.exports = {
|
||||
},
|
||||
proxy: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||
},
|
||||
netId42161: {
|
||||
'eth': {
|
||||
'instanceAddress': {
|
||||
'0.1': '0x84443CFd09A48AF6eF360C6976C5392aC5023a1F',
|
||||
'1': '0xd47438C816c9E7f2E2888E060936a499Af9582b3',
|
||||
'10': '0x330bdFADE01eE9bF63C209Ee33102DD334618e0a',
|
||||
'100': '0x1E34A77868E19A6647b1f2F47B51ed72dEDE95DD'
|
||||
},
|
||||
'deployedBlockNumber': {
|
||||
'0.1': 2243707,
|
||||
'1': 2243709,
|
||||
'10': 2243735,
|
||||
'100': 2243749
|
||||
},
|
||||
'miningEnabled': false,
|
||||
'symbol': 'ETH',
|
||||
'decimals': 18
|
||||
},
|
||||
proxy: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user