6870 lines
322 KiB
HTML
6870 lines
322 KiB
HTML
<!doctype html>
|
|
<html lang="en" data-bs-theme="dark">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Tornado Withdraw</title>
|
|
<link rel="icon" type="image/png" href="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png">
|
|
|
|
<meta name="description" content="Tornado Withdraw - Open Source UI for Tornado Cash">
|
|
<meta property="og:title" content="Tornado Withdraw">
|
|
<meta property="og:description" content="Tornado Withdraw - Open Source UI for Tornado Cash">
|
|
<meta property="og:image" content="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/tw.png">
|
|
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/bootstrap.css" integrity="sha384-p8zfDSkYPu7Xu7mMd8DJHdXwh1/mZ2P/aMhahJze550GcUbzNxB841pMCrYaew9I" crossorigin="anonymous">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" integrity="sha384-XGjxtQfXaH2tnPFa9x+ruJTuLE3Aa6LhHSWRr1XeTyhezb4abCG4ccI5AkVDxqC+" crossorigin="anonymous">
|
|
|
|
<script>
|
|
window.process = {
|
|
browser: true,
|
|
env: {},
|
|
};
|
|
</script>
|
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/ethers@6.13.4/dist/ethers.umd.min.js" integrity="sha384-6Zl0Pc8zjSz8KvmNeXRvUQgY4ryFb+BwDvKCmLYcBME0joAaru491tQgi9B7zsMM" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/moment@2.30.1/moment.min.js" integrity="sha384-aQgnUSsW4D+imRFZ/dILN0wXp3MGO6RE3ccC/gZHr6BQzvhwzD+Bzon5C+kO3NHQ" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/datatables.net@2.1.8/js/dataTables.min.js" integrity="sha384-MgwUq0TVErv5Lkj/jIAgQpC+iUIqwhwXxJMfrZQVAOhr++1MR02yXH8aXdPc3fk0" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@2.1.8/js/dataTables.bootstrap5.min.js" integrity="sha384-G85lmdZCo2WkHaZ8U1ZceHekzKcg37sFrs4St2+u/r2UtfvSDQmQrkMsEx4Cgv/W" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/tornado.umd.js" integrity="sha384-hMoCdBLrTbzEjQ16gl7WhAJLmRKLCR3lBDwEpB8G/EuN1d7jpSrtDLTmj7EOUx+J" crossorigin="anonymous"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/tornadoContracts.umd.js" integrity="sha384-YXvIEhvhQZA2aG8aFjpu7zp70EIZWvVIKp2cug1bOkPUA3Ta96vUvQ1rasWqpqHq" crossorigin="anonymous"></script>
|
|
|
|
<style>
|
|
@media (min-width: 1100px) {
|
|
.container {
|
|
width: 1100px;
|
|
}
|
|
}
|
|
@media (min-width: 768px) {
|
|
.navbar-mobile {
|
|
display: none !important;
|
|
}
|
|
}
|
|
.navbar-mobile {
|
|
flex-basis: 100%;
|
|
flex-grow: 1;
|
|
align-items: center;
|
|
}
|
|
.mb-5-super {
|
|
margin-bottom: 0.5rem !important;
|
|
}
|
|
.logo {
|
|
max-width: 40px;
|
|
max-height: 40px;
|
|
}
|
|
.loader {
|
|
animation: rotation 2s infinite linear;
|
|
}
|
|
.status {
|
|
display: block;
|
|
margin-top: 20px;
|
|
margin-bottom: 10px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
width: 100px;
|
|
}
|
|
@keyframes rotation {
|
|
from {
|
|
transform: rotate(359deg);
|
|
}
|
|
to {
|
|
transform: rotate(0deg);
|
|
}
|
|
}
|
|
.bg-purple {
|
|
background-color: purple;
|
|
}
|
|
.table-column {
|
|
word-wrap: break-word;
|
|
max-width: 500px;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
/*global $, jQuery, bootstrap, ethers, Tornado, TornadoContracts, moment*/
|
|
const VERSION = '1.0.7';
|
|
|
|
const DONATION_ADDRESS = '0x40c3d1656a26C9266f4A10fed0D87EFf79F54E64';
|
|
const DEFAULT_GAS_LIMIT = 600_000;
|
|
|
|
// Used for DAO
|
|
const GOVERNANCE_NETWORK = Tornado.NetId.MAINNET;
|
|
// Used for relayers
|
|
const RELAYER_NETWORK = Tornado.NetId.MAINNET;
|
|
|
|
const JSDELIVR = 'https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7';
|
|
|
|
const IP_ECHO = 'https://tornadowithdraw.com/ip';
|
|
|
|
// Prohibits United States, United Kingdom, Netherlands, and countries sanctioned by the United Nations Security Council
|
|
// https://main.un.org/securitycouncil/en/sanctions/information
|
|
const PROHIBITED_COUNTRIES = [
|
|
{
|
|
country: 'United States',
|
|
iso: 'US',
|
|
},
|
|
{
|
|
country: 'United Kingdom',
|
|
iso: 'UK',
|
|
},
|
|
{
|
|
country: 'Netherlands',
|
|
iso: 'NL',
|
|
},
|
|
{
|
|
country: 'Angola',
|
|
iso: 'AO',
|
|
},
|
|
{
|
|
country: 'Liberia',
|
|
iso: 'LR',
|
|
},
|
|
{
|
|
country: 'Iraq',
|
|
iso: 'IQ',
|
|
},
|
|
{
|
|
country: 'Somalia',
|
|
iso: 'SO',
|
|
},
|
|
{
|
|
country: 'Congo (the Democratic Republic of the)',
|
|
iso: 'CD',
|
|
},
|
|
{
|
|
country: 'Sudan (the)',
|
|
iso: 'SD',
|
|
},
|
|
{
|
|
country: 'Lebanon',
|
|
iso: 'LB',
|
|
},
|
|
{
|
|
country: 'Korea (the Democratic People\'s Republic of)',
|
|
iso: 'KP',
|
|
},
|
|
{
|
|
country: 'Libya',
|
|
iso: 'LY',
|
|
},
|
|
{
|
|
country: 'Central African Republic (the)',
|
|
iso: 'CF',
|
|
},
|
|
{
|
|
country: 'Yemen',
|
|
iso: 'YE',
|
|
},
|
|
{
|
|
country: 'South Sudan',
|
|
iso: 'SS',
|
|
},
|
|
{
|
|
country: 'Mali',
|
|
iso: 'ML',
|
|
},
|
|
];
|
|
|
|
const hashes = {
|
|
'static/bootstrap.css': 'sha384-p8zfDSkYPu7Xu7mMd8DJHdXwh1/mZ2P/aMhahJze550GcUbzNxB841pMCrYaew9I',
|
|
'static/bootstrap.css.map': 'sha384-j26wcRYDZq3IKjEVNqnOwbDwbzbuyl21J7osOB66qzfGSQTHCDo6/7ucE8tZL6jL',
|
|
'static/bootstrap.scss': 'sha384-WGj/q75ULGvWTkymODLWPqlrh+u7FJaZ+rojlBt3zSNgrXmKR0EPTugM5dG/TZd8',
|
|
'static/failed.png': 'sha384-4RgbpGGgorMHTDOhCbsLMD8mkTAxw1UBgtClItza78QPG6vtHXNC2BMq2C25ZUBz',
|
|
'static/merkleTreeWorker.umd.js': 'sha384-Zp2NHEeBOeZC5QD4qQUplChHllTkqtzyqCiz3qRacrUYZ19xPC7XBhfzf8IzHpJx',
|
|
'static/merkleTreeWorker.umd.min.js': 'sha384-YIZuKW04rsurzd5YdJ7qKBxqJoeAfVtBjjDsQ/l5iAGyyzbMDEXszBYe+UkqU8hp',
|
|
'static/success.png': 'sha384-mydX80QpF0gj6AWSPZh9L7ETKOMdQhhOjKylm8LTFLIyhuwTJyMxT41cQKbIpIC3',
|
|
'static/torn.png': 'sha384-JHO0kpKduyooWMLR9QZ35GSU1/B0cpbQ6wLvVAg91KkVEFLQz9XBwBD9FCFJVL9R',
|
|
'static/torn2.png': 'sha384-4wuwoWrwrSCoVr3jtQLAFoj09ukL6JJ0/MJ/5qCoN5oEv6CwPe5Xu2VP5wKiHsbS',
|
|
'static/tornado.json.zip': 'sha384-XmKBnm5OYS4kGyw52NNSOmSQ4uvN7w/ZNQIgqfSSSSJ1MJw9dfCE5OLQHHDvqP0F',
|
|
'static/tornado.umd.js': 'sha384-hMoCdBLrTbzEjQ16gl7WhAJLmRKLCR3lBDwEpB8G/EuN1d7jpSrtDLTmj7EOUx+J',
|
|
'static/tornado.umd.min.js': 'sha384-OgIIEpuZ36gZnA2UcCCiIDiRX0A4dbNDzyAN6v9/RaWAaKn8A3JsjK9Esqy7SxR7',
|
|
'static/tornadoContracts.umd.js': 'sha384-YXvIEhvhQZA2aG8aFjpu7zp70EIZWvVIKp2cug1bOkPUA3Ta96vUvQ1rasWqpqHq',
|
|
'static/tornadoContracts.umd.min.js': 'sha384-1yFtL5qBZWn4ZZc9Yxt64WwSEBTd7u9FEXzJV6ef1966Wty8OpFINVv22VD98Oau',
|
|
'static/tornadoProvingKey.bin.zip': 'sha384-O+ICo/CmIvnFmeaSfvRc6CUlde24XWL2Bp2hgKRPZTO1LLlVW0GS8gHIzMtcKRa2',
|
|
'static/tw.png': 'sha384-DZz1nbDiTbg7RYMRjqvM6sD549Hzr83KzWXIH9x5S+9vEjm67MGbc9pjZoaVOWvT',
|
|
'static/tokens/avax.png': 'sha384-RJu9H2cnsMxyrwre/CWILYE5Rz2i+/4sf1igngk17yqR784178CYm2etsL7qSlkM',
|
|
'static/tokens/bnb.png': 'sha384-ZHavaJrdvQXViSliE64xeYwF1BbD3T6IHqxQOZcNNcRgzjFXezI3DeJAGkyMaDer',
|
|
'static/tokens/cdai.png': 'sha384-Z+9YQ/A/hiINLHXGGW7opwzGeZoBrxxro3jAmh45h2h/TT+4D2BPtUVYsOQZV7mJ',
|
|
'static/tokens/dai.png': 'sha384-8cCp4wB7I6YJlHcS/2slypyOdWLCpHKHLLxJTVJXuGG+o3rgUfqcennA9UaxrImg',
|
|
'static/tokens/eth.png': 'sha384-NuQCy9iHPek+MNtJAtjAII5o8kVec4pZD8wWnCeQ14ePi+6aA0RLQkp6/kUQ8/9+',
|
|
'static/tokens/matic.png': 'sha384-EyLQM+E06SdSJEg9YdNb7a5i/vI+/ARVFsNbEI0dHZdfH5lggmknhR2fj4/dwvz+',
|
|
'static/tokens/sepoliaeth.png': 'sha384-NuQCy9iHPek+MNtJAtjAII5o8kVec4pZD8wWnCeQ14ePi+6aA0RLQkp6/kUQ8/9+',
|
|
'static/tokens/torn.png': 'sha384-9tTQeaGm1FRGWjIzWp/2jxGRWkcJOWTYTowihd3MTcVXHENgOROnwjqxHvRC9vHo',
|
|
'static/tokens/usdc.png': 'sha384-C9fBtBeyuT1BEnGWjvHxGmnJ7itvXUksCUh8w2t7tRCyVQGckLnyWiZqftdlQfRY',
|
|
'static/tokens/usdt.png': 'sha384-qfdxj2ekS52MuRF4mwnOG2rz4L2LpESgguV7vEROvB97GdW/Z3REPwOa5F61FnI4',
|
|
'static/tokens/wbtc.png': 'sha384-G6cKlI++QFQbL9wuZZZFkk+T1JK8+r3Efq4VO4WY/ToZoaids7lL4syePLwQZyk2',
|
|
'static/tokens/xdai.png': 'sha384-8cCp4wB7I6YJlHcS/2slypyOdWLCpHKHLLxJTVJXuGG+o3rgUfqcennA9UaxrImg',
|
|
'static/events/deposits_100_xdai_100.json.zip': 'sha384-saavn+sR0uZd9suSw3HfnhRAsTX5VP5fUH9nI5/KHmIvDRtEBrjyPlmUAxOD/oft',
|
|
'static/events/deposits_100_xdai_1000.json.zip': 'sha384-FU2sF62dSNe3b1nONeIfYcNMIrn11CvlK0zFGkpqkBo+KUO0dnZuMXoWIX7dUPAK',
|
|
'static/events/deposits_100_xdai_10000.json.zip': 'sha384-qUHmU0fVJ3iOuUHn0QErIU3IYesD2mO/CJ4JEvjkgtoKRjp24ZEf6a0wQHm+5tPr',
|
|
'static/events/deposits_100_xdai_100000.json.zip': 'sha384-ZlQJ8V8E+zTI7+9s2JMtHXr+CvoTULZ/AS94wzluRFYZS373eYr81yIor1a5Cdr8',
|
|
'static/events/deposits_10_eth_0.1.json.zip': 'sha384-/enSn2ftk0dnsuu3nCjhDenFEXuwGSiuigsx8P4umVf7a35DEeUuv87M1D3D0K+l',
|
|
'static/events/deposits_10_eth_1.json.zip': 'sha384-rQU7KBTVX9CC2VsZQ2czIV3s2E7rlI/QZBsrpqgxWOw7bMAOa7QUQOcxcqE9aS3T',
|
|
'static/events/deposits_10_eth_10.json.zip': 'sha384-sfnY/SbvXbGkxDJkGECps0ZpzhL7rvXemz8881Aj+LuUJGv2xKBQFhcwwKsYlD6F',
|
|
'static/events/deposits_10_eth_100.json.zip': 'sha384-plnh245VknBamQ+Y4248qyj+SA9tlaXt7yCP/gkaxvq2GAgv5Ujy3wVKuq+kC8cf',
|
|
'static/events/deposits_11155111_dai_100.json.zip': 'sha384-wX658+oUHQf3FmVic79SxGoE2aiqDmd/D7N0HtPEySvC12tf3+mUpIYotBSXcbth',
|
|
'static/events/deposits_11155111_dai_1000.json.zip': 'sha384-c2K073UqEy+/u9pZszv1wyuw9c606/sTVrpcBgrhyayIwVbQca90eaqHWj6J2sI/',
|
|
'static/events/deposits_11155111_dai_10000.json.zip': 'sha384-tHtc9Fj3QFiNWXyb/bHWN4M/2GVLh9tNJ5bMwxTAA1oXJb4BFdFGHv5evgPvsiNi',
|
|
'static/events/deposits_11155111_dai_100000.json.zip': 'sha384-nM/wwfrjfuVQRVA7VJwkLlWUh0WlA9rUvLlAtZEkoCNeYtgwmpUvEiN4H6/HOqT6',
|
|
'static/events/deposits_11155111_eth_0.1.json.zip': 'sha384-GCM7lgk/vUZKp5om0/u0Kang3Q0RQjejOifMskojg/MYusOpy/3ZjA0b2ivzbgHK',
|
|
'static/events/deposits_11155111_eth_1.json.zip': 'sha384-pMQm/swVdVlL0HRTLq6tOvabv2B01YJh9jVBuIwWsHY+4d2ezPQYuIn/NmrTjJHN',
|
|
'static/events/deposits_11155111_eth_10.json.zip': 'sha384-Vz9opUQVIkQP4PLHD5W+DEPWAzLU5pOj6u8utdH87deRmBuBcJKpj7Nj/CfiZMXi',
|
|
'static/events/deposits_11155111_eth_100.json.zip': 'sha384-Pcuy0YLekrcVjgVWq9mjxqrLY9BWmuL9ndP03/+VH+alcPEao75PnrRrSybcunwX',
|
|
'static/events/deposits_137_matic_100.json.zip': 'sha384-dqOvSPzykgjA5eOMiLbmAA8EkbDFLMnImDW/Lj7VJLDRDvLJPocFIvs7m+OQSafh',
|
|
'static/events/deposits_137_matic_1000.json.zip': 'sha384-K1WUYi3JVaY6lckGSM+SjKih39F4/Zml1UnwCOK37tHusRIB03xksEidzB+HeLsf',
|
|
'static/events/deposits_137_matic_10000.json.zip': 'sha384-8QBV+n3d7KstAwPAUtAGqWcxX7n7XyJNqq6DAJHvUrqNfhGCiikufN5jDjHOflCR',
|
|
'static/events/deposits_137_matic_100000.json.zip': 'sha384-gaeHrMxsg8ACD3n/nbl5BXAR5idodfFkPXjxy9uu2iTWdU63hvqKdXuTH7xzKRd7',
|
|
'static/events/deposits_1_cdai_5000.json.zip': 'sha384-IcIQWLBuV5Nr6wFADYKq4TAzoTti0k1O8FOertwHiTY7TzCbs+/T5OAZBdhpn34G',
|
|
'static/events/deposits_1_cdai_50000.json.zip': 'sha384-Jnex5SeDkvos730ZItsbuCcWdY9sruoeI5k3/m08wXW9O+dQ1DI0EQ0RLKOWndOo',
|
|
'static/events/deposits_1_cdai_500000.json.zip': 'sha384-PXZFPcBZuBIfOr8PqKHc4ytiUCOYt3NIHBWBoSRrmKgxYqu2l8MMy/hL9Iwi/rEk',
|
|
'static/events/deposits_1_cdai_5000000.json.zip': 'sha384-xrbjndEOH+P3MO0eyBQ6NEcj11GZ8ZXu3tijxbz8jay0R4U2CYleAxxtjXSrpFfk',
|
|
'static/events/deposits_1_dai_100.json.zip': 'sha384-EPC3ag8WjCyvUJ+33AXda6TCgR8X14yDfh8TbEu/3nvaXL+SZYmebq+X4J4fhorj',
|
|
'static/events/deposits_1_dai_1000.json.zip': 'sha384-BEnV9Zy2WrQTpVA3qiuDsLCZpl2Zn+WcaPkgKCAKvZfDn5J+Jsg+7tKY24VbuGaS',
|
|
'static/events/deposits_1_dai_10000.json.zip': 'sha384-3NI4Nso1Mw3oXnCXZFjRNLGva5zD4SclLm6hFMH1fhjJLF5o96SbiQlaBEVV1XZz',
|
|
'static/events/deposits_1_dai_100000.json.zip': 'sha384-zAJMb5MbyAy2oDFqMt4j2YY4NlOgbh8KS+jN5b1sv9yfWsJkMlRnyDKxzG9Vo8Tm',
|
|
'static/events/deposits_1_eth_0.1.json.zip': 'sha384-m4OIQKD9Wb95sCPXcVT50Q4HwP5wIO0u2fdPW7a2VXzZ2R8GqakhZOJWNdNByUPO',
|
|
'static/events/deposits_1_eth_1.json.zip': 'sha384-SkvUqenciTikzSpJDFIgIBNSOLmADa2fWbq20VE/aS265LpAMaNzMMzb8zanG59P',
|
|
'static/events/deposits_1_eth_10.json.zip': 'sha384-g9AhRjKehsJwPuUIXRKTUgpdQIoKc+VlhwLANzy9p759pM646fMAdQVOaxrh3TWx',
|
|
'static/events/deposits_1_eth_100.json.zip': 'sha384-GLNU/069uZt4E5QIj0kjfHZMJZr6j//1XFx91VKdDsY0tVtWEE4ZG73uZhNuQKcv',
|
|
'static/events/deposits_1_usdc_100.json.zip': 'sha384-vKySc3XblzRo+rHv/bF0CbQl2tLl2g+/0O4ZmDyPyJUh0KSPPRT/KfPYx14RD1Ru',
|
|
'static/events/deposits_1_usdc_1000.json.zip': 'sha384-hoRofVEOBRqLH3R3+dHl1MMJd5HAIOZNcfjFFH3pYE7+s48n53z8crg1+d/VkkQT',
|
|
'static/events/deposits_1_usdt_100.json.zip': 'sha384-IqMfcdMBJRp+Y9mG1e91NPuy84kszjL4Hpc6fhayouePQvrEu/j4Lhz+P/O6/kbW',
|
|
'static/events/deposits_1_usdt_1000.json.zip': 'sha384-j7uWvF1kdfG2aaqzKgU+vT2nzF4hY1JIFQpksHPOeSo3etAfyPSg7q7SSmlv85++',
|
|
'static/events/deposits_1_wbtc_0.1.json.zip': 'sha384-j/Q/Fz/S4pjbi9gCwVLc/p0BGrCpbeo5FmfYJ1y1Pp3Gf+eZRqQa8kBVMmJaP7qw',
|
|
'static/events/deposits_1_wbtc_1.json.zip': 'sha384-e/UcCulccgADPSvd+rATql0NUtHgx6G48d1FmpO7GAfn1DYG1PnUHhcbvi5Pp/qP',
|
|
'static/events/deposits_1_wbtc_10.json.zip': 'sha384-oueBOv7Vgk3fjImUOx7YfUVU5N3oAq+hmkZZr2v2U+XxpLiV2Z/jN5ezfHX/lqrU',
|
|
'static/events/deposits_42161_eth_0.1.json.zip': 'sha384-c1zl1Otj+y4sYiglPs+DIis/j18P72iVOReFNCoWMYvocLLe8gZXRXEjYC++lsaS',
|
|
'static/events/deposits_42161_eth_1.json.zip': 'sha384-M4mJr3wVvF93/Gb6vEds4g8sPMsXY/YAYStrghBCrd9GSq4NwzosMRFbUEUNXXTl',
|
|
'static/events/deposits_42161_eth_10.json.zip': 'sha384-QACaql15NdtWF55pv80yAh6O+D9Ba1a07vumThL1KC0KuQovq5H/9MgIVS1er1ih',
|
|
'static/events/deposits_42161_eth_100.json.zip': 'sha384-rsiaiKZ2rH+qlcevksbLhYvrstCWyxur6DvLQxb804hu0dFVHGz4XufkAghBjRek',
|
|
'static/events/deposits_43114_avax_10.json.zip': 'sha384-jpn/kbpHDF/Var46GyRkZKw67hOZWUfqZlf4aaWAVXZ96sxtXJXDMX8NtX92ODiJ',
|
|
'static/events/deposits_43114_avax_100.json.zip': 'sha384-PnL0dma4Wqma/zHUbuAS/cq+WHT5Oifh7fNO1C2mEZi6tib+gTN4OECD2Eua6xce',
|
|
'static/events/deposits_43114_avax_500.json.zip': 'sha384-xszbEh3nLB/Uh25KNHM9nBzmjyO/P7aD7CeyUt+dzUA8SqDgqQ1LYc5tWySc4AVt',
|
|
'static/events/deposits_56_bnb_0.1.json.zip': 'sha384-jI3zhrADAjWGYPCyn4Oc9JpxwUaMeYbxwUGcBJbaMcYXu5Llz0d7RvzPVbsjODz9',
|
|
'static/events/deposits_56_bnb_1.json.zip': 'sha384-A3LVQptEWs/lrFnFmkTorVS/yktfLBrmD/5GybDYyXwGPMD5fwl8np+Dr0zVQfQF',
|
|
'static/events/deposits_56_bnb_10.json.zip': 'sha384-EhV7PGtYJ2bF6mW6AUbyEP99R5zdyWGE2N04gaQhb9cE3IIpyIVDOXa7toEAO44R',
|
|
'static/events/deposits_56_bnb_100.json.zip': 'sha384-ZBoGR/qavAj3jiaTPhN5hRNf4mz/9ctAJmX12wdKGuRPR/3cyoXj4hpv7sZeZM8d',
|
|
'static/events/deposits_56_btcb_0.0001.json.zip': 'sha384-5uB3/HPuhA0WaSHhn/W6JVPFuhN6MDwU8wFO6jFCjfvpWQcGcUCZ+9apVLw0t53a',
|
|
'static/events/deposits_56_btcb_0.001.json.zip': 'sha384-asIx6JLLgHSUiKNxSyImWFg1A1oRna1vcgd2R7ej8QhY41khnxsZEG/EaMTVOcFk',
|
|
'static/events/deposits_56_btcb_0.01.json.zip': 'sha384-bxztmr4vwPJzi4nkJBC5soDVxNFGA0zUi9LCIzkBybpM5hAOtKFwu23P+qj+1L9a',
|
|
'static/events/deposits_56_btcb_0.1.json.zip': 'sha384-K1moGbmn16J9C/FFXM1xy4ZW3GYL8CkNA9WOestfWyeGHmEy0xphaQsglSHOW2Ql',
|
|
'static/events/deposits_56_usdt_10.json.zip': 'sha384-nD+QIlY363JMsK3bFJjRgvI9opiL3a655qJF9/dPjovp5lRc71tA+HB0vcpwckAY',
|
|
'static/events/deposits_56_usdt_100.json.zip': 'sha384-4gxbcD0tG7BnEmlJyxsvvQ467qkAyeBKhviY9F9zKztV2sVjdlRQbGWbjOhJWblF',
|
|
'static/events/deposits_56_usdt_1000.json.zip': 'sha384-W1aWsVRAMgNlDdCWt0R2dPS4ts39Hlw/PS8/dOcmQHnSyaP/o7ZNZbH+4RbwdlGp',
|
|
'static/events/deposits_56_usdt_10000.json.zip': 'sha384-F4QCKuR/GHMafglAgMOyD/OO2JtkeDEJhIpsYcEVRc7Kb1Y4g7sLiVw6Q/LMNM4s',
|
|
'static/events/echo_1.json.zip': 'sha384-8f2mVn+RYYyEp2BATmpjI9ikHy1tqiiqlMFDEFqy3OUsIlvscePVveFaZNW4apCa',
|
|
'static/events/echo_10.json.zip': 'sha384-JpMvj21HSHJu5v5qN/gz8D1M3RFxWj74dS4Gb6zU5/RyPvqhI3Kx1+foyAfDHUxX',
|
|
'static/events/echo_100.json.zip': 'sha384-VY1XwuU+YeIveBDxPtOL7i+W3EqXGBtAaAEO1dopzf6J+0rk+BOrzSqAFO3CYjVl',
|
|
'static/events/echo_11155111.json.zip': 'sha384-aAfuGiEOIWiNpv0jBFsf19M5+xwfQc9nCFxz1dr2H6RhMGe3KeAxCO0GVg43qErD',
|
|
'static/events/echo_137.json.zip': 'sha384-dZQo5zA30Gd1KPxsAGgIAgkLmfBjYoGTTeN5kgQaTQJ5MxF0PF+MICQKsNdjGLYN',
|
|
'static/events/echo_42161.json.zip': 'sha384-cSbp3tSqjJfP6oYSUKVhkesMBdRIBeoxxhkIp+jk3tCXMqal5nETCYZw+WVw+G0L',
|
|
'static/events/echo_43114.json.zip': 'sha384-7Py5sljkmMAt0TCaBUxhoWOU++Kmnas5S8sJYKxT8f3q8h0KxeyB8DKiIQ3ym8kR',
|
|
'static/events/echo_56.json.zip': 'sha384-DNkAK5qxqtnnlm8Pbj1ZA2MYORb9mhLdgWzTfeJTcfG95eEJ5/OtxeMMyZZaoU1O',
|
|
'static/events/encrypted_notes_1.json.zip': 'sha384-jK4k63kgFvuvq1thwhkh2V7jNbWueaiJOcMbibvUxjcP3OvSljswc9MJYCDjzANN',
|
|
'static/events/encrypted_notes_10.json.zip': 'sha384-RhHxF7F2bx0a37WdLKcIB4O0PDc3TO5mnv50ayFFgWC86FTh5UXJ8nAsrXTV9hMt',
|
|
'static/events/encrypted_notes_100.json.zip': 'sha384-HNJck1bBW68p7SfyNfvogpmegi6XqaDgFDn4/cafNirDbWpCTpCrfikOFTU/ILze',
|
|
'static/events/encrypted_notes_11155111.json.zip': 'sha384-BF7nJu5NR9PIgR1OQYgzdG7iK6eJSSJjsSRG50I9TQctRKLqn3wtyUfdE8rdv96t',
|
|
'static/events/encrypted_notes_137.json.zip': 'sha384-9MBupp5P5VHpB8cMFuIfzmBoT/BE7cKbhuECk6OPm2N6KrB/4zZ59w5yfbO6lTvV',
|
|
'static/events/encrypted_notes_42161.json.zip': 'sha384-QtAZwi1JPdguLf8JUPN1fe/LHi1mTZQ+76wF0Ct6MxapUkmWkvRbV+TUk6Fi5W+Y',
|
|
'static/events/encrypted_notes_43114.json.zip': 'sha384-enjUljsf0qTe//tTV5PGW9wqlgE0ZaBzz/bDb74LAxiYzUqOI3FVAiNB0rPFPhVk',
|
|
'static/events/encrypted_notes_56.json.zip': 'sha384-umlVjJ0xe8ZCJhaTX2Slix7W1VGZdEXiMYm4f0fzRAG/HDm+KMPxQbHZF7YGLLJH',
|
|
'static/events/governance_1.json.zip': 'sha384-IJEQOOJdxsufN7Hsq59ZE5KBuyIKjjf/Ve5T9D+7/RLHi//JRmkRuI928UbWFoeN',
|
|
'static/events/governance_11155111.json.zip': 'sha384-MbeTc/EYah39LgNSnWDFAy4jns7BchGVDILO76l77a24ufJmJ6JlNivwJMiGlbMI',
|
|
'static/events/registry_1.json.zip': 'sha384-eudIMv/G9rl0X7hyNNVPiqxK6tfg6d8NjwFAKljtJsqxKMbaF8y2Q/FizTox1s08',
|
|
'static/events/registry_11155111.json.zip': 'sha384-tpc6vtBloHUyoSZ/5biG3jAvZ9mlj9SHAR9LP82up1nNwHdCnVGhzvcGjsyYRvK/',
|
|
'static/events/relayers.json': 'sha384-Mz1FuLCjLyD/8RY9nP2Cge/PkYkra4d3Zt+MB4rx09/TvJCvoB4Z7/MXuHOFROS1',
|
|
'static/events/relayers.json.zip': 'sha384-EA6DvejrS6NO1vufecok50J0VGOWXx/EiBALAXTcDmRY0IOfdCtyuCOAIGsEK1fF',
|
|
'static/events/revenue_1.json.zip': 'sha384-IFzN3M4VaaBu6T0uH7BfRm60jv/CeHM5f507N6BMKEny2a0W+Nt6oAcBf+ePwO3/',
|
|
'static/events/revenue_11155111.json.zip': 'sha384-w9/pn3h2qdLL9n7NLGiHK6xxLvWMpi7YQqOuwBo4WL4tsIptCiEMwkNPe/br1fVK',
|
|
'static/events/withdrawals_100_xdai_100.json.zip': 'sha384-HoXz34GeA97nbkluH2/L59PEPVSin++GCI/pPlExDAK2t7G0V+cNC3JbM6wM/W/6',
|
|
'static/events/withdrawals_100_xdai_1000.json.zip': 'sha384-xfrUckEy/dSUEdJxQSXcqHdYyMfN2tAxsRXkfUT6BxBkc4kdj73qYWor2bk+ju1Y',
|
|
'static/events/withdrawals_100_xdai_10000.json.zip': 'sha384-iYDsm8lPVE9DmmGkDZVygGydRF4GUfhDyVMXRd+qjo+ZxGiyi3qJK8/tC9ADItuw',
|
|
'static/events/withdrawals_100_xdai_100000.json.zip': 'sha384-etsh+M3HUyEtZUwK/+QF8//9YudFx6g+QNJbv1dvgLI+VvlsufhkMVDPbbB4a5fq',
|
|
'static/events/withdrawals_10_eth_0.1.json.zip': 'sha384-LclLA42t5MoMJph/BB7Lw2Wd15VtyjX8jUA/rVQzVH8eSJLC7h/Q1U2wecllVGPm',
|
|
'static/events/withdrawals_10_eth_1.json.zip': 'sha384-5YCH/f57fSc2p0Dz1kkdvSSJwua8ViSBy7fwd1n582+UfeFx/HVOKC8++KrshhHI',
|
|
'static/events/withdrawals_10_eth_10.json.zip': 'sha384-XVts4oweZc5qXvhCNeot9B6m/3K9EWEtqZTuOWzmQJgMNRvzW2f0/9HcH2N4en+D',
|
|
'static/events/withdrawals_10_eth_100.json.zip': 'sha384-ApTVOiZGm8mGdDfMax3ctZ0JDJ6MwWZowubJu4lQ8swxDANLkwit2ciANMEG55dT',
|
|
'static/events/withdrawals_11155111_dai_100.json.zip': 'sha384-wZXPCl/nGe9POXBv5c6jPTZESiGYVuVV9afHaT132qVOQ7ed5P/i+1wMQ3wgnxQw',
|
|
'static/events/withdrawals_11155111_dai_1000.json.zip': 'sha384-YYWCoQrLzLdAZO9qB2D46M8uZQyjHdUdkAzWXVcIv0ujZWixjgYMHYwyd1XEogl3',
|
|
'static/events/withdrawals_11155111_dai_10000.json.zip': 'sha384-rkiynDp7409fZHiWuaXzxX8hsBy0qM9aLQVYAv3L9lP7AWPp9sLsfSgDpzv5ZzSa',
|
|
'static/events/withdrawals_11155111_dai_100000.json.zip': 'sha384-KsDEGIxrF84j5GL5ZeNBbyoICIkjAz5JJ8N1HA+k21oHgrXk3fJO7N7IMWOKoZ2G',
|
|
'static/events/withdrawals_11155111_eth_0.1.json.zip': 'sha384-RBRd8cl48M4Gr+h9x5qSGsLkl1J3xfWE8nFUeInaRUTI8FuawtqJScmPUtTVbtyX',
|
|
'static/events/withdrawals_11155111_eth_1.json.zip': 'sha384-3nR+2H54CuFwl+4pX+mzyYQH3KJQRpeDjh50WBY9+j7vK4j/YrHxHr6TLbJUtFo1',
|
|
'static/events/withdrawals_11155111_eth_10.json.zip': 'sha384-R1AHoexWR3K1n2y2TkQ+lTVBGc+UI+FVlNxQwDRAU3hMV7aGeX7VkBf8R09fIjb7',
|
|
'static/events/withdrawals_11155111_eth_100.json.zip': 'sha384-mUYncP9vTxRb4k5sEKfcuMccbaG/8izTKcIVlN0gyEDJkYhGCdheC8iJ5sP26ECe',
|
|
'static/events/withdrawals_137_matic_100.json.zip': 'sha384-XtqTLEn1yScAZToaybTCm5f4zG/ThJMNBf21kPLptHR7X3ffJN+t0skI2tUOeKNA',
|
|
'static/events/withdrawals_137_matic_1000.json.zip': 'sha384-Cz9KtoBqwld0Z6wsPHxv3pBKeByL4Vi0Zncmy4xTigGqMTTNfoSpVPSveA6PAmon',
|
|
'static/events/withdrawals_137_matic_10000.json.zip': 'sha384-nJqLrhlHDitxiURYNRgdnRdB2xCvfUT67ajjcxmzEJwndBDVtVhcjyn6licaL6qt',
|
|
'static/events/withdrawals_137_matic_100000.json.zip': 'sha384-6MSAIl/TsIj4BV2YCXBPmR/7PKxotVcaYm+WP1q1Znbz2MNkKjDQjpIcjucyy5Xx',
|
|
'static/events/withdrawals_1_cdai_5000.json.zip': 'sha384-oMSzbMsWHjmJ10duwUBs2B0OlWezTiEtghQiI7gmmoOUZHmI74zv166c246jiI3N',
|
|
'static/events/withdrawals_1_cdai_50000.json.zip': 'sha384-RY2zNkSag/qtt1I2FkqRi2nmz5VAdntaRy4goI+/SkYQ2CW13cAyAODIMPQce7+U',
|
|
'static/events/withdrawals_1_cdai_500000.json.zip': 'sha384-2Z/2iSYnjrIJUV34o/Oy/I6Cd+ZLZ5f+oaVjk0QPGqjr5Yp5EcKYDoR6D2IaS647',
|
|
'static/events/withdrawals_1_cdai_5000000.json.zip': 'sha384-A5sFkXaGTK7oqc3mmUk8Zp3pR966R59Ez/ci4UwQ8TnD4LauEKNAGBYUOSVyjTBh',
|
|
'static/events/withdrawals_1_dai_100.json.zip': 'sha384-7j4T+pbMEHZqQR+lvnXGtdddysOLJx2rSTiCx3fSmlubx2+In1pUiXIctE1PEJZ5',
|
|
'static/events/withdrawals_1_dai_1000.json.zip': 'sha384-dskVGh1bEr9btiiD8us+lYZT1urGeo4v0D4gVoaeIpIyycLLaAAVsenB+pEHEtEQ',
|
|
'static/events/withdrawals_1_dai_10000.json.zip': 'sha384-MZR4eTJQr3KRySx6SzWxY9pngMuRMtb3ioYfcZJdROjw+4R4JONRsric2/FIHLAN',
|
|
'static/events/withdrawals_1_dai_100000.json.zip': 'sha384-GMe1JZ1MfTplKDyR+Ee1euj4mui6mpEB67x0MfI32/u+sUglZClADthfJorMU9bS',
|
|
'static/events/withdrawals_1_eth_0.1.json.zip': 'sha384-EUALLHA5ES/IICWcuog/nqN4NhmqjbIU7PuvjiyLHx4F9zbt7Bor/NBaxdHBv2eU',
|
|
'static/events/withdrawals_1_eth_1.json.zip': 'sha384-sz94RYCFQyJQmBxo5XYcyhDqY+88Ci5W2hVmyvbo9SC7Nu6wp8/S6/57w2Oca+eL',
|
|
'static/events/withdrawals_1_eth_10.json.zip': 'sha384-zM4yOX6Esbo4V7JnkHDct4aXLMDZv7M7TuWZ1yoKa4vZcb2ZglvbhksSQUC8bjFQ',
|
|
'static/events/withdrawals_1_eth_100.json.zip': 'sha384-2CZhP2xI8zMIU8YNl7huIV5PmbYfI8GBcw+dOqDEhDqa3EugGaobHHtHXZsWkvs6',
|
|
'static/events/withdrawals_1_usdc_100.json.zip': 'sha384-Y9mV4gW+gcDUfm0PFfcOHRxTh1dcrRpsr3s8wxU8AbfGsLhdzxwIJF4dX6ipf3KB',
|
|
'static/events/withdrawals_1_usdc_1000.json.zip': 'sha384-2OlNnWtNJcuN9l0QS/pJ3/kS7YzFpY9KPHP2gtRxL7T1Mr55Q0pK8Ft54tgGG1pG',
|
|
'static/events/withdrawals_1_usdt_100.json.zip': 'sha384-gnagdsNsMdqPoTbCG9rUZi19HMjI+E9mtrWE5SmkthtJGrloYiFUdxJWZ+OE/sMK',
|
|
'static/events/withdrawals_1_usdt_1000.json.zip': 'sha384-xiZquAIB+BKT3LKBoFYopzjw62hz78vQIQHDxp9pWvbBP12CTDIHqbcB5C5HMUpk',
|
|
'static/events/withdrawals_1_wbtc_0.1.json.zip': 'sha384-sqzhGkGZWOIOtdCQqUTCN1ZW5rFNFIOjLOy1QpXWc+eko5jKzcp3mFCf4AsWa9tH',
|
|
'static/events/withdrawals_1_wbtc_1.json.zip': 'sha384-A1//Rwa3liaMdOkI+ruW00RSgcmQgGd26z+jhHEy+bFumIYqtWyRa100uDqPHI5X',
|
|
'static/events/withdrawals_1_wbtc_10.json.zip': 'sha384-cpgNBJOQrYO8r+9WounhGiJW/stK5UduSxUFRncCSJ6qfoWWzTUiEB52J4FwC8Vv',
|
|
'static/events/withdrawals_42161_eth_0.1.json.zip': 'sha384-TVGV5jkWKEVRgzd7AWxr+9Chi8hfft+UzEzyN4DhA6ZicbcUifhmhBXKi1fc5BRn',
|
|
'static/events/withdrawals_42161_eth_1.json.zip': 'sha384-lNHohw9TW5B07O9EFhPz1OFTJmbIU2IPBkdg6KzwoPPPeNy2YcxqJ6fR2KWXxHbZ',
|
|
'static/events/withdrawals_42161_eth_10.json.zip': 'sha384-uPEH1LLp+EtNXBnI1nXaObPSErVDTcR36RrLGbvCcqKx6fjmyDcVvIfrNWquaB9q',
|
|
'static/events/withdrawals_42161_eth_100.json.zip': 'sha384-dDe5gPLIqBV8VJjuNnuG89OPDjQLEkE5m3byYpMSNAhNPpx7EkPeJMoVhJlY+yNl',
|
|
'static/events/withdrawals_43114_avax_10.json.zip': 'sha384-JO5HQirAZUhHTEXfwsgAwdqIWklIzYdXRdfGYs2BHkRiyzHpfUx4iRUs/LHeDkd6',
|
|
'static/events/withdrawals_43114_avax_100.json.zip': 'sha384-WHNGUMVxsWlKTJdfoLAJYPcv1LPkVrStMjGaVzBQfDhfX750zRZzd2hyyY6ZWCIf',
|
|
'static/events/withdrawals_43114_avax_500.json.zip': 'sha384-RnOtZx9aWrczr/32qjb9CLAlCsiYx7IazDZ91aNEKFURj+ghTG5B2O1EEl+ot2/4',
|
|
'static/events/withdrawals_56_bnb_0.1.json.zip': 'sha384-ZLE2sUH5HLYdsoWbCdGhmv7kLP/R8JmDNVFnRAkR5uefYo9JwNM8KEsrlaw1km3e',
|
|
'static/events/withdrawals_56_bnb_1.json.zip': 'sha384-l8aZ2NG2D72hnzr3tsBei1VJAMqXHDdF3EqViyKRcdneB9XRnJ/sykDbRfHOofeI',
|
|
'static/events/withdrawals_56_bnb_10.json.zip': 'sha384-LZjcjfdXVBNdhbfvcB+AUnLHrHMfFrki0MArUUT9ZYR4kfZG9CZBsKeLoWWQ1qMs',
|
|
'static/events/withdrawals_56_bnb_100.json.zip': 'sha384-yr3s7J+81mGr6rtfMRJsPJMIT1ftIFVsHZttlGDC3DMmj4BrnzBHLLPsqJKnA+eG',
|
|
'static/events/withdrawals_56_btcb_0.0001.json.zip': 'sha384-LZga+qg6O9zQzOtkU6GHg6zj9abc3WgEIbZORjvfwRIQPcDOO/80We7SQv7Xc/E3',
|
|
'static/events/withdrawals_56_btcb_0.001.json.zip': 'sha384-8TdqO6eeSqFjcTVgPLUm4vWGQR6rgG1MssgB7IWRrtvKoVnf+H0Xu1sAtFaruvrI',
|
|
'static/events/withdrawals_56_btcb_0.01.json.zip': 'sha384-hEkoASpe70UmvWQXHmXve21jIuP+Ysdqo37l8+GQMvuIVIGlBM2VkxerfcUCAkJj',
|
|
'static/events/withdrawals_56_btcb_0.1.json.zip': 'sha384-6daK+ZjVzm66YU1AGGB0gzxkWZMI2qOu3+FYynzHeBfID4INZpEgG1pPAjqzWRAZ',
|
|
'static/events/withdrawals_56_usdt_10.json.zip': 'sha384-LQDFsM/IeGfyWjwix6LbGn4idJephXAQ0YcOv1moDW3RTz963cwSYa74lYRGKTSM',
|
|
'static/events/withdrawals_56_usdt_100.json.zip': 'sha384-87oE4g7egwws0z6zAfKhYb+tXtq8dWMFhE1j4gXTTgMxc8NIrHQsp2BtgPgjz0fF',
|
|
'static/events/withdrawals_56_usdt_1000.json.zip': 'sha384-7vGElgJoUWyqDeIhPjHbFKedq5AZTRNU9vxjQ3kJoHXCm8M4Ww2qHtmlsR9ho1+a',
|
|
'static/events/withdrawals_56_usdt_10000.json.zip': 'sha384-TRVX7ZkiqkpUrACrW8WY1/mTpGNgn1U54XAyogHrXhZKD87hqNMWtYQLYkuJR2Gh'
|
|
};
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<!-- Main Container -->
|
|
<div class="container">
|
|
|
|
<!-- Header -->
|
|
<header class="py-2 mt-2 mb-3 border-bottom">
|
|
<nav class="navbar navbar-expand-md">
|
|
<a href="#" class="navbar-brand me-md-auto">
|
|
<img src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png" class="me-1 logo">
|
|
<span class="fs-4 align-middle">Tornado Withdraw</span>
|
|
</a>
|
|
|
|
<!-- Desktop Navbar -->
|
|
<ul class="nav nav-pills">
|
|
<li class="nav-item d-none d-md-block">
|
|
<a href="#" class="nav-link bar-link active" data-page="home">Home</a>
|
|
</li>
|
|
<li class="nav-item d-none d-md-block">
|
|
<a href="#voting" class="nav-link bar-link" data-page="voting">Voting</a>
|
|
</li>
|
|
<li class="nav-item d-none d-md-block">
|
|
<a href="#bulk" class="nav-link bar-link" data-page="bulk">Bulk</a>
|
|
</li>
|
|
<li class="nav-item d-none d-md-block">
|
|
<a href="#encrypt" class="nav-link bar-link" data-page="encrypt">Encrypt</a>
|
|
</li>
|
|
<li class="nav-item d-none d-md-block">
|
|
<a href="#relayer" class="nav-link bar-link" data-page="relayer">Relayer</a>
|
|
</li>
|
|
<li class="nav-item d-none d-md-block">
|
|
<a href="#wallet" class="nav-link bar-link" data-page="wallet">Wallet</a>
|
|
</li>
|
|
<li class="nav-item d-none d-md-block">
|
|
<a onclick="settings()" class="nav-link"><i class="bi bi-gear-fill"></i></a>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Mobile Navbar -->
|
|
<button
|
|
class="navbar-toggler"
|
|
type="button"
|
|
data-bs-toggle="collapse"
|
|
data-bs-target="#navbarToggleExternalContent"
|
|
aria-controls="navbarToggleExternalContent"
|
|
aria-expanded="false"
|
|
aria-label="Toggle navigation"
|
|
>
|
|
<span class="navbar-toggler-icon"></span>
|
|
</button>
|
|
<div class="navbar-mobile collapse" id="navbarToggleExternalContent">
|
|
<ul class="navbar-nav me-auto mt-2 mb-2 mb-lg-0">
|
|
<li class="nav-item d-md-none border-top mt-2 mb-2"></li>
|
|
<li class="nav-item d-md-none">
|
|
<a href="#" class="nav-link bar-link active" data-page="home">Home</a>
|
|
</li>
|
|
<li class="nav-item d-md-none">
|
|
<a href="#voting" class="nav-link bar-link" data-page="voting">Voting</a>
|
|
</li>
|
|
<li class="nav-item d-md-none">
|
|
<a href="#bulk" class="nav-link bar-link" data-page="bulk">Bulk</a>
|
|
</li>
|
|
<li class="nav-item d-md-none">
|
|
<a href="#encrypt" class="nav-link bar-link" data-page="encrypt">Encrypt</a>
|
|
</li>
|
|
<li class="nav-item d-md-none">
|
|
<a href="#relayer" class="nav-link bar-link" data-page="relayer">Relayer</a>
|
|
</li>
|
|
<li class="nav-item d-md-none">
|
|
<a href="#wallet" class="nav-link bar-link" data-page="wallet">Wallet</a>
|
|
</li>
|
|
<li class="nav-item d-md-none">
|
|
<a onclick="settings()" class="nav-link"><i class="bi bi-gear-fill"></i> Settings</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<!-- Messages -->
|
|
<div id="messages"></div>
|
|
<div class="alert alert-warning" role="alert">
|
|
This is a new open source Tornado Cash UI that enables you to withdraw faster with new relayers.
|
|
It is unaudited so use at your own risk.
|
|
</div>
|
|
|
|
<!-- Deposit Page -->
|
|
<div id="home" class="page">
|
|
<div class="col-md-12">
|
|
<h2 class="mb-5-super">Deposit \ Withdraw Notes</h2>
|
|
<p>
|
|
This UI allows you to use the new <a href="https://git.tornado.ws/tornadocontrib/tovarish-relayer" target="_blank" rel="noreferrer nofollow">Tovarish Relayer</a>
|
|
which provides historic Deposit Events at a faster speed than fetching it from normal nodes.
|
|
</p>
|
|
|
|
<!-- Cards -->
|
|
<div class="row">
|
|
<!-- Deposit Card -->
|
|
<div class="card col-md-6 mt-2 me-4 g-0" style="min-height: 410px;">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs home-card-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link active" href="#" data-card-group="home-card" data-card="deposit-card">Deposit</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="home-card" data-card="invoice-card">Invoice</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="home-card" data-card="withdraw-card">Withdraw</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="home-card" data-card="compliance-card">Compliance</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body ms-2 me-2">
|
|
<!-- Deposit -->
|
|
<div class="home-card deposit-card">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="deposit-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Currency</label>
|
|
<select id="deposit-currency" class="form-control form-select"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount</label>
|
|
<select id="deposit-amount" class="form-control form-select"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="Backup encrypted notes on-chain?" disabled>
|
|
<div class="input-group-text">
|
|
<!-- $('#deposit-onchain').is(':checked') -->
|
|
<input id="deposit-onchain" class="form-check-input mt-0" type="checkbox" value="">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Invoice -->
|
|
<div class="home-card invoice-card d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Deposit Invoice</label>
|
|
<input id="invoice-note" type="text" class="form-control" placeholder="Enter your Deposit Invoice">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Withdraw -->
|
|
<div class="home-card withdraw-card d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Deposit Note</label>
|
|
<input id="withdraw-note" type="text" class="form-control" placeholder="Enter your Deposit Note">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Recipient Address</label>
|
|
<input id="withdraw-recipient" type="text" class="form-control" placeholder="Enter your Wallet Address">
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="Purchase Gas for Token Withdrawals?" disabled="">
|
|
<div class="input-group-text">
|
|
<input id="withdraw-refund" class="form-check-input mt-0" type="checkbox" checked>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="withdraw-on-note" class="d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<input type="text" class="form-control note-network network-list" disabled>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Currency</label>
|
|
<input type="text" class="form-control note-currency" disabled>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount</label>
|
|
<input type="text" class="form-control note-amount" disabled>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Relayer (From Settings)</label>
|
|
<input type="text" class="form-control note-relayer" disabled>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Compliance -->
|
|
<div class="home-card compliance-card d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Deposit Note</label>
|
|
<input id="compliance-note" type="text" class="form-control" placeholder="Enter your Deposit Note">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer" style="border-top: none;">
|
|
<div class="home-card deposit-card">
|
|
<button type="button" class="btn btn-primary mb-2" style="width: 48%" onclick="deposit(true)">Create</button>
|
|
<button type="button" class="btn btn-primary w-50 mb-2" onclick="deposit()">Deposit</button>
|
|
</div>
|
|
<div class="home-card invoice-card d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="invoice()">Deposit Invoice</button>
|
|
</div>
|
|
<div class="home-card withdraw-card d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="withdraw()">Withdraw</button>
|
|
</div>
|
|
<div class="home-card compliance-card d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="compliance()">Compliance</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Card -->
|
|
<div class="card mt-2 col-md-5 g-0" style="min-height: 410px;">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link active">Statistics <span id="statistics-instance" class="badge bg-primary ms-1"></span></a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body ms-2 me-2">
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<p class="fst-italic mb-2">Anonymity set</p>
|
|
<span id="statistics-deposits" class="fw-bold"></span> <span>equal user deposits</span>
|
|
</div>
|
|
<div style="margin-bottom: 0.75rem;">
|
|
<p class="fst-italic mb-2">Latest Deposits</p>
|
|
</div>
|
|
<table id="statistics-table" class="table"></table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5">
|
|
<div class="form-text pb-2">Information of notes, deposits, withdrawals, or private keys is never shared among any other third parties including us, relayers, and RPC providers. You can download and audit the source code of this website by simply saving it to index.html</div>
|
|
|
|
<div class="form-text">By using this website you are confirming that you aren't from United States, United Kingdom, Netherlands, sanctioned country by the United Nations Security Council or anywhere where the usage of Tornado Cash is legally prohibited</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Voting -->
|
|
<div id="voting" class="page d-none">
|
|
<div class="col-md-12">
|
|
<div class="card mt-2 col-md-12 mb-5">
|
|
<div class="row">
|
|
<div class="col-sm ms-2 px-4 py-3">
|
|
<h6>Available balance</h6>
|
|
<span id="torn-balance">0</span> TORN
|
|
</div>
|
|
<div class="col-sm px-4 py-3">
|
|
<h6>Locked Balance</h6>
|
|
<span id="torn-locked">0</span> TORN
|
|
</div>
|
|
<div class="col-sm px-4 py-3">
|
|
<h6>Locking Reward</h6>
|
|
<span id="torn-reward">0</span> TORN
|
|
</div>
|
|
<div class="col-sm px-4 py-3">
|
|
<h6>Delegated balance</h6>
|
|
<span id="torn-delegated">0</span> TORN
|
|
</div>
|
|
<div class="col-sm px-4 py-2">
|
|
<button type="button" class="btn btn-primary position-relative top-50 translate-middle ms-4" onclick="refreshLocked()">Refresh</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Cards -->
|
|
<div class="row">
|
|
<!-- Deposit Card -->
|
|
<div class="card col-md-12 g-0" style="min-height: 410px;">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs voting-card-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link active" href="#" data-card-group="voting-card" data-card="proposals-card">Proposal</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="voting-card" data-card="create-proposal-card">Create Proposal</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="voting-card" data-card="lock-card">Lock</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="voting-card" data-card="delegate-card">Delegate</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="voting-card" data-card="delegations-card">Get Delegations</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="voting-card" data-card="apy-card" onclick="navApy()">APY</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body ms-2 me-2">
|
|
<!-- Proposals -->
|
|
<div class="voting-card proposals-card">
|
|
<table id="proposal-table" class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Title</th>
|
|
<th>Start Time</th>
|
|
<th>End Time</th>
|
|
<th>Quorum</th>
|
|
<th>For Votes</th>
|
|
<th>Against Votes</th>
|
|
<th>State</th>
|
|
<th>View</th>
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Create Proposal -->
|
|
<div class="voting-card create-proposal-card d-none">
|
|
<p class="mb-2">You can create proposal to participate for on-chain governance.</p>
|
|
<p>Locked balance should be more or equal to 1000 TORN in order to create proposal.</p>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Proposal Title</label>
|
|
<input id="create-proposal-title" type="text" class="form-control" placeholder="Title">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Proposal Address</label>
|
|
<input id="create-proposal-address" type="text" class="form-control" placeholder="Proposal Address">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Proposal Description</label>
|
|
<textarea id="create-proposal-description" type="text" class="form-control" rows="3" placeholder="Proposal Description"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lock -->
|
|
<div class="voting-card lock-card d-none">
|
|
<p class="mb-2">You can lock TORN tokens to receive yields from withdrawal fees collected by Relayers. </p>
|
|
<p>In order to participate in Governance, you must lock TORN tokens. Your voting power will be equivalent to how many tokens you lock.</p>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount to lock</label>
|
|
<div class="input-group">
|
|
<input id="lock-amount" type="number" class="form-control" placeholder="Amount">
|
|
<button type="button" class="btn btn-primary" onclick="maxLockAmount()">Max</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount to unlock</label>
|
|
<div class="input-group">
|
|
<input id="unlock-amount" type="number" class="form-control" placeholder="Amount">
|
|
<button type="button" class="btn btn-primary" onclick="maxUnlockAmount()">Max</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="voting-card delegate-card d-none">
|
|
<p>You can delegate your voting power to a delegatee. The delegatee will be able to participate in Governance on your behalf.</p>
|
|
<div class="mb-3">
|
|
<label class="form-label">Delegate</label>
|
|
<div class="input-group">
|
|
<input id="delegate-address" type="text" class="form-control" placeholder="Address or ENS name to Delegate">
|
|
<button type="button" class="btn btn-primary" onclick="getCurrentDelegate()">Get Current Delegate</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="voting-card delegations-card d-none">
|
|
<p>You can view the list of delegated accounts to the delegatee</p>
|
|
<div class="mb-3">
|
|
<label class="form-label">Delegatee</label>
|
|
<div class="input-group">
|
|
<input id="delegatee-address" type="text" class="form-control" placeholder="Address or ENS name of Delegatee">
|
|
<button type="button" class="btn btn-primary" onclick="getCurrentDelegatee()">Get List</button>
|
|
</div>
|
|
<table id="delegatee-table" class="table table-bordered mt-4"></table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- APY -->
|
|
<div id="apy-content" class="voting-card apy-card d-none"></div>
|
|
</div>
|
|
<div class="card-footer" style="border-top: none;">
|
|
<div class="voting-card proposals-card">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="loadProposals()">Refresh Proposals</button>
|
|
</div>
|
|
<div class="voting-card create-proposal-card d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="createProposal()">Create Proposal</button>
|
|
</div>
|
|
<div class="voting-card lock-card d-none">
|
|
<button type="button" class="btn btn-primary mb-2" style="width: 33%" onclick="lock()">Lock</button>
|
|
<button type="button" class="btn btn-primary mb-2" style="width: 33%" onclick="unlock()">Unlock</button>
|
|
<button type="button" class="btn btn-primary mb-2" style="width: 33%" onclick="claim()">Claim</button>
|
|
</div>
|
|
<div class="voting-card delegate-card d-none">
|
|
<button type="button" class="btn btn-primary w-50 mb-2" onclick="delegate()">Delegate</button>
|
|
<button type="button" class="btn btn-primary mb-2" style="width: 49%" onclick="undelegate()">Undelegate</button>
|
|
</div>
|
|
<div class="voting-card apy-card d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="loadApy()">Refresh APY</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5">
|
|
<div class="form-text">Information of notes, deposits, withdrawals, or private keys is never shared among any other third parties including us, relayers, and RPC providers. You can download and audit the source code of this website by simply saving it to index.html</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Deposit -->
|
|
<div id="bulk" class="page d-none">
|
|
<div class="col-md-12">
|
|
<h2 class="mb-5-super">Bulk Deposit Notes \ Invoices</h2>
|
|
<p>
|
|
Make a bulk deposit of Tornado Notes \ Invoices using <a href="https://www.multicall3.com/" target="_blank" rel="noreferrer nofollow">Multicall3</a> contract.
|
|
Only Instance with native assets (ETH, BNB, MATIC) are supported.
|
|
</p>
|
|
<!-- Cards -->
|
|
<div class="row">
|
|
<!-- Bulk Card -->
|
|
<div class="card mt-2 col-md-12 g-0" style="min-height: 410px;">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs bulk-card-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link active" href="#" data-card-group="bulk-card" data-card="deposit-card">Deposit</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body ms-2 me-2">
|
|
<!-- Deposit -->
|
|
<div class="bulk-card deposit-card">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="bulk-deposit-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes / Invoices to deposit (Splitted by line break!)</label>
|
|
<textarea id="bulk-deposit" type="text" class="form-control" rows="7"></textarea>
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="Backup encrypted notes on-chain?" disabled="">
|
|
<div class="input-group-text">
|
|
<input id="bulk-onchain" class="form-check-input mt-0" type="checkbox" value="">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer" style="border-top: none;">
|
|
<div class="bulk-card deposit-card">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="bulkDeposit()">Deposit</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5">
|
|
<div class="form-text">Information of notes, deposits, withdrawals, or private keys is never shared among any other third parties including us, relayers, and RPC providers. You can download and audit the source code of this website by simply saving it to index.html</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Encryption -->
|
|
<div id="encrypt" class="page d-none">
|
|
<div class="col-md-12">
|
|
<!-- Cards -->
|
|
<div class="row">
|
|
<!-- Encryption Card -->
|
|
<div class="card mt-2 col-md-12 g-0" style="min-height: 410px;">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs encrypt-card-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link active" href="#" data-card-group="encrypt-card" data-card="recover-card">Recover Keys & Notes</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="encrypt-card" data-card="create-card">Create Encrypt Key</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="encrypt-card" data-card="backup-card">Backup Notes</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body ms-2 me-2">
|
|
<!-- Recover -->
|
|
<div class="encrypt-card recover-card">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="recover-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create -->
|
|
<div class="encrypt-card create-card d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="create-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Backup -->
|
|
<div class="encrypt-card backup-card d-none">
|
|
<p>You can encrypt and backup any text on-chain using your encryption key, from Deposit Notes to Text.</p>
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="backup-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes / Texts to backup (Splitted by line break!)</label>
|
|
<textarea id="backup-note" type="text" class="form-control" rows="7"></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer" style="border-top: none;">
|
|
<div class="encrypt-card recover-card">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="recover()">Recover Keys & Notes</button>
|
|
</div>
|
|
<div class="encrypt-card create-card d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="createKey()">Create</button>
|
|
</div>
|
|
<div class="encrypt-card backup-card d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="backupEncrypted()">Backup</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5">
|
|
<div class="form-text">Information of notes, deposits, withdrawals, or private keys is never shared among any other third parties including us, relayers, and RPC providers. You can download and audit the source code of this website by simply saving it to index.html</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Relayer -->
|
|
<div id="relayer" class="page d-none">
|
|
<div class="col-md-12">
|
|
<!-- Cards -->
|
|
<div class="row">
|
|
<div class="card mt-2 col-md-12 g-0" style="min-height: 410px;">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs relayer-card-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link active" href="#" data-card-group="relayer-card" data-card="relayer-list">Relayers</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="relayer-card" data-card="relayer-status">Relayer Status</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="relayer-card" data-card="relayer-register">Register Relayer</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="relayer-card" data-card="register-hostname">Register Hostname</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="relayer-card" data-card="relayer-stake">Stake TORN to Relayer</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body ms-2 me-2">
|
|
<!-- Relayer List -->
|
|
<div class="relayer-card relayer-list">
|
|
<p>Get list of available relayers for the selected network</p>
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="relayer-list-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
<div id="relayer-list"></div>
|
|
</div>
|
|
|
|
<!-- Relayer Status -->
|
|
<div class="relayer-card relayer-status d-none">
|
|
<p>Get relayer status (including staked TORN amounts, relayer status, etc)</p>
|
|
<div class="mb-3">
|
|
<label class="form-label">ENS Name</label>
|
|
<input id="relayer-status-name" type="text" class="form-control" placeholder="ENS Name">
|
|
</div>
|
|
<div id="relayer-status"></div>
|
|
</div>
|
|
|
|
<!-- Register Relayer -->
|
|
<div class="relayer-card relayer-register d-none">
|
|
<p>Register new relayer on-chain ( registering relayer would cost you some amount of TORN tokens )</p>
|
|
<div class="mb-3">
|
|
<label class="form-label">ENS Name</label>
|
|
<input id="relayer-register-name" type="text" class="form-control" placeholder="ENS Name">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Register Hostname -->
|
|
<div class="relayer-card register-hostname d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="register-hostname-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">ENS Name</label>
|
|
<input id="register-hostname-name" type="text" class="form-control" placeholder="ENS Name">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Relayer Hostname</label>
|
|
<input id="register-hostname" type="text" class="form-control" placeholder="Relayer Hostname">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stake torn for Relayer -->
|
|
<div class="relayer-card relayer-stake d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">ENS Name</label>
|
|
<input id="relayer-stake-name" type="text" class="form-control" placeholder="ENS Name">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">TORN Amount</label>
|
|
<div class="input-group">
|
|
<input id="relayer-stake-amount" type="number" class="form-control" placeholder="TORN Amount">
|
|
<button type="button" class="btn btn-primary" onclick="maxRelayerStakeAmount()">Max</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer" style="border-top: none;">
|
|
<div class="relayer-card relayer-list">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="getRelayers()">Get Relayers</button>
|
|
</div>
|
|
<div class="relayer-card relayer-status d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="relayerStatus()">Get Status</button>
|
|
</div>
|
|
<div class="relayer-card relayer-register d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="registerRelayer()">Register Relayer</button>
|
|
</div>
|
|
<div class="relayer-card register-hostname d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="registerHostname()">Register Hostname</button>
|
|
</div>
|
|
<div class="relayer-card relayer-stake d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="relayerStake()">Stake to Relayer</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5">
|
|
<div class="form-text">Information of notes, deposits, withdrawals, or private keys is never shared among any other third parties including us, relayers, and RPC providers. You can download and audit the source code of this website by simply saving it to index.html</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Wallet -->
|
|
<div id="wallet" class="page d-none">
|
|
<div class="col-md-12">
|
|
<!-- Cards -->
|
|
<div class="row">
|
|
<div class="card mt-2 col-md-12 g-0" style="min-height: 410px;">
|
|
<div class="card-header">
|
|
<ul class="nav nav-tabs card-header-tabs wallet-card-tabs">
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link active" href="#" data-card-group="wallet-card" data-card="wallet-balance">Get Balance</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="wallet-card" data-card="send-coins">Send Coins</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="wallet-card" data-card="send-tokens">Send Tokens</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="wallet-card" data-card="gas-zip" onclick="displayGasZipMax()">Gas.zip</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="wallet-card" data-card="sign-transaction">Sign Transaction</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="wallet-card" data-card="broadcast-transaction">Broadcast Transaction</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link card-link" href="#" data-card-group="wallet-card" data-card="create-wallet">Create Wallet</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body ms-2 me-2">
|
|
<!-- Wallet Balance -->
|
|
<div class="wallet-card wallet-balance">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="wallet-balance-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
<div id="wallet-balance"></div>
|
|
</div>
|
|
|
|
<!-- Send Coins -->
|
|
<div class="wallet-card send-coins d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="send-coins-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Recipient Address</label>
|
|
<input id="send-coins-recipient" type="text" class="form-control" placeholder="Recipient Address">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount</label>
|
|
<div class="input-group">
|
|
<input id="send-coins-amount" type="number" class="form-control" placeholder="Amount">
|
|
<button type="button" class="btn btn-primary" onclick="maxSendAmount()">Max</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Input data (optional)</label>
|
|
<input id="send-coins-data" type="text" class="form-control" placeholder="Input data (optional)">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Send Tokens -->
|
|
<div class="wallet-card send-tokens d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network</label>
|
|
<select id="send-tokens-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Token Address</label>
|
|
<input id="send-tokens-address" type="text" class="form-control" placeholder="Token Address">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Recipient Address</label>
|
|
<input id="send-tokens-recipient" type="text" class="form-control" placeholder="Recipient Address">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Amount</label>
|
|
<div class="input-group">
|
|
<input id="send-tokens-amount" type="number" class="form-control" placeholder="Amount">
|
|
<button type="button" class="btn btn-primary" onclick="maxTokenAmount()">Max</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Gas.zip -->
|
|
<div class="wallet-card gas-zip d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Inbound Chain</label>
|
|
<select id="gas-zip-inbound" class="form-control form-select network-list"></select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Outbound Chains</label>
|
|
<div id="gas-zip-outbounds">
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="Ethereum Mainnet" disabled="">
|
|
<div class="input-group-text">
|
|
<input id="deposit-gas" class="form-check-input mt-0" type="checkbox" value="">
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="BNB Chain" disabled="">
|
|
<div class="input-group-text">
|
|
<input class="form-check-input mt-0" type="checkbox" value="">
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="Optimism" disabled="">
|
|
<div class="input-group-text">
|
|
<input class="form-check-input mt-0" type="checkbox" value="">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Recipient Address</label>
|
|
<input id="gas-zip-recipient" type="text" class="form-control" placeholder="Recipient Address">
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label">Deposit Amount ( Output will be a divided value per chain )</label>
|
|
<div class="input-group">
|
|
<input id="gas-zip-amount" type="number" class="form-control" placeholder="Amount">
|
|
<button type="button" class="btn btn-primary" onclick="maxGasZipAmount()">Max</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p>Min Input: <span id="gas-zip-min"></span></p>
|
|
<p>Max Input: <span id="gas-zip-max"></span></p>
|
|
</div>
|
|
|
|
<!-- Sign Transaction -->
|
|
<div class="wallet-card sign-transaction d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network (Will be used when tx doesn't have chainId)</label>
|
|
<select id="unsigned-tx-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Unsigned Transaction</label>
|
|
<textarea id="unsigned-tx" type="text" class="form-control" rows="3"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Broadcast Transaction -->
|
|
<div class="wallet-card broadcast-transaction d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Network (Will be used when tx doesn't have chainId)</label>
|
|
<select id="raw-tx-network" class="form-control form-select network-list"></select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Raw Transaction</label>
|
|
<textarea id="raw-tx" type="text" class="form-control" rows="3"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Wallet -->
|
|
<div class="wallet-card create-wallet d-none">
|
|
<div class="mb-3">
|
|
<label class="form-label">Mnemonic</label>
|
|
<input id="create-wallet-mnemonic" type="text" class="form-control" placeholder="New BIP39 Mnemonic" disabled>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Address</label>
|
|
<input id="create-wallet-address" type="text" class="form-control" placeholder="New Ethereum Address" disabled>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Private Key</label>
|
|
<input id="create-wallet-private-key" type="text" class="form-control" placeholder="New Private Key" disabled>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-footer" style="border-top: none;">
|
|
<div class="wallet-card wallet-balance">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="balance()">Get Balance</button>
|
|
</div>
|
|
<div class="wallet-card send-coins d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="sendCoins()">Send</button>
|
|
</div>
|
|
<div class="wallet-card send-tokens d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="sendTokens()">Send</button>
|
|
</div>
|
|
<div class="wallet-card gas-zip d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="sendGasZip()">Send</button>
|
|
</div>
|
|
<div class="wallet-card sign-transaction d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="signTransaction()">Sign</button>
|
|
</div>
|
|
<div class="wallet-card broadcast-transaction d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="broadcastTransaction()">Broadcast</button>
|
|
</div>
|
|
<div class="wallet-card create-wallet d-none">
|
|
<button type="button" class="btn btn-primary w-100 mb-2" onclick="createWallet()">Create</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-5">
|
|
<div class="form-text">Information of notes, deposits, withdrawals, or private keys is never shared among any other third parties including us, relayers, and RPC providers. You can download and audit the source code of this website by simply saving it to index.html</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<footer class="d-flex flex-wrap justify-content-between align-items-center pt-3 mt-3 mb-4 border-top">
|
|
<ul class="nav col-md-7 mb-3">
|
|
<li class="nav-item">
|
|
<a href="https://git.tornado.ws/tornadocontrib/tornado-withdraw" target="_blank" rel="noreferrer nofollow" class="nav-link px-2 text-muted"><i class="bi bi-git"></i> Source Code</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="https://element.tornadocash.social" target="_blank" rel="noreferrer nofollow" class="nav-link px-2 text-muted"><i class="bi bi-chat"></i> Matrix Chat</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a href="https://t.me/tornadoofficial" target="_blank" rel="noreferrer nofollow" class="nav-link px-2 text-muted"><i class="bi bi-telegram"></i> Telegram</a>
|
|
</li>
|
|
</ul>
|
|
|
|
<p class="ms-2 text-muted justify-content-end">Built with <a href="https://git.tornado.ws/tornadocontrib/tornado-core" target="_blank" rel="noreferrer nofollow">@tornado/core</a> | <a target="_blank" rel="noreferrer nofollow" class="donation">Donate</a></p>
|
|
</footer>
|
|
</div>
|
|
|
|
<!-- Settings modal -->
|
|
<div id="settings" class="modal fade" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 id="settings-title" class="modal-title">Settings</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body settings">
|
|
<form action="javascript:void(0);">
|
|
<div class="mb-3">
|
|
<label class="form-label">RPC endpoints</label>
|
|
<div id="rpc-endpoints"></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Relayers</label>
|
|
<select id="relayers" class="form-control form-select">
|
|
<option value="wallet" data-tovarish="true">Browser Wallet / Private Key</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">View-only Address</label>
|
|
<input id="view-only" type="text" class="form-control" placeholder="View-only wallet address (to generate unsigned transactions)">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Mnemonic ( Optional, not saved )</label>
|
|
<input id="mnemonic" type="text" class="form-control" placeholder="Mnemonic (12 / 24 words) to use for Direct Wallet Withdrawal">
|
|
<input id="mnemonic-index" type="number" class="form-control" value="0">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Private Key ( Optional, not saved )</label>
|
|
<input id="private-key" type="text" class="form-control" placeholder="Private Key to use for Direct Wallet Withdrawal">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Encryption Key ( Optional, not saved )</label>
|
|
<input id="encrypt-key" type="text" class="form-control" placeholder="Encryption Key to encrypt and decrypt on-chain notes">
|
|
</div>
|
|
<div class="mb-3">
|
|
<p>UI Version: <span class="version"></span></p>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="resetSettings()">Reset</button>
|
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="saveSettings()">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Send Modal -->
|
|
<div id="send" class="modal fade" tabindex="-2">
|
|
<div class="modal-dialog modal-xl" style="max-width: 950px;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 id="send-title" class="modal-title"></h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="confirm-screen" class="d-none">
|
|
<div id="send-table"></div>
|
|
<div id="send-confirmation"></div>
|
|
</div>
|
|
<div id="status-screen" class="d-none">
|
|
<div id="send-status" class="mt-1"></div>
|
|
<img id="send-loading" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png" class="loader status d-none">
|
|
<img id="send-error" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/failed.png" class="status d-none">
|
|
<img id="send-success" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/success.png" class="status d-none">
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button id="unsigned-button" type="button" class="btn btn-secondary d-none" onclick=""></button>
|
|
<a id="backup-button" download="" href="" class="btn btn-primary d-none">Save</a>
|
|
<button id="send-button" type="button" class="btn btn-primary d-none" onclick="confirmDeposit()"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prohibited Modal -->
|
|
<div id="prohibited" class="modal fade" tabindex="-3">
|
|
<div class="modal-dialog modal-xl" style="max-width: 950px;">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Access denied</h5>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="prohibited-context"></div>
|
|
<img src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/failed.png" class="status">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-primary " data-bs-dismiss="modal">I am not the citizen of those prohibited countries</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let settingsDB;
|
|
const AllDB = new Map();
|
|
|
|
const providers = new Map();
|
|
|
|
let viewOnly;
|
|
let mnemonic;
|
|
let mnemonicIndex = 0;
|
|
let privateKey;
|
|
let encryptKey;
|
|
let browserSigner = new Map();
|
|
|
|
const allRelayers = [];
|
|
const allTovarishRelayers = [];
|
|
const allProposals = [];
|
|
|
|
let proposalTable;
|
|
|
|
let workerChecked = false;
|
|
|
|
let job = {};
|
|
|
|
const abiCoder = ethers.AbiCoder.defaultAbiCoder();
|
|
|
|
class VoidSigner extends Tornado.TornadoVoidSigner {
|
|
async sendTransaction(tx) {
|
|
const pop = await this.populateTransaction(tx);
|
|
delete pop.from;
|
|
const txObj = ethers.Transaction.from(pop);
|
|
|
|
setTimeout(() => {
|
|
showConfirmation(
|
|
'Unsigned Transaction',
|
|
'',
|
|
`<textarea type="text" class="form-control" rows="3" disabled>${txObj.unsignedSerialized}</textarea>`
|
|
);
|
|
}, 50);
|
|
|
|
return {
|
|
hash: `Unsigned: ${txObj.unsignedSerialized}`
|
|
};
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function settings() {
|
|
new bootstrap.Modal('#settings', {}).toggle();
|
|
}
|
|
|
|
function hide(comp) {
|
|
if (!$(comp).hasClass('d-none')) {
|
|
$(comp).addClass('d-none');
|
|
}
|
|
}
|
|
|
|
function show(comp) {
|
|
if ($(comp).hasClass('d-none')) {
|
|
$(comp).removeClass('d-none');
|
|
}
|
|
}
|
|
|
|
function notifyMsg(msg) {
|
|
const msgId = `msg-${Date.now()}`;
|
|
|
|
$('#messages').prepend(`
|
|
<div id="${msgId}" class="alert alert-success" role="alert">
|
|
${msg}
|
|
</div>
|
|
`);
|
|
|
|
setTimeout(() => {
|
|
$(`#${msgId}`).remove();
|
|
}, 10000);
|
|
}
|
|
|
|
async function shortHash(msg) {
|
|
const digested = await Tornado.digest(new TextEncoder().encode(msg), 'SHA-1');
|
|
|
|
return Tornado.bytesToHex(digested);
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function closeAlert(msg) {
|
|
await settingsDB.setValue(msg, 'ignore');
|
|
}
|
|
|
|
async function alertMsg(msg, permanent) {
|
|
const hash = await shortHash(msg);
|
|
|
|
const pref = await settingsDB.getValue(hash);
|
|
|
|
if (pref === 'ignore') {
|
|
return;
|
|
}
|
|
|
|
const msgId = `msg-${Date.now()}`;
|
|
|
|
$('#messages').prepend(`
|
|
<div id="${msgId}" class="alert alert-warning alert-dismissible" role="alert">
|
|
${msg}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick="closeAlert('${hash}')"></button>
|
|
</div>
|
|
`);
|
|
|
|
if (!permanent) {
|
|
setTimeout(() => {
|
|
$(`#${msgId}`).remove();
|
|
}, 60000);
|
|
}
|
|
}
|
|
|
|
function errorMsg(msg, permanent) {
|
|
const msgId = `msg-${Date.now()}`;
|
|
|
|
$('#messages').prepend(`
|
|
<div id="${msgId}" class="alert alert-danger" role="alert">
|
|
${msg}
|
|
</div>
|
|
`);
|
|
|
|
if (!permanent) {
|
|
setTimeout(() => {
|
|
$(`#${msgId}`).remove();
|
|
}, 10000);
|
|
}
|
|
}
|
|
|
|
function showConfirmation(title, context, table) {
|
|
$('#send-title').text(title);
|
|
|
|
$('#send-confirmation').empty();
|
|
$('#send-confirmation').append(context);
|
|
$('#send-table').empty();
|
|
if (table) {
|
|
$('#send-table').append(table);
|
|
}
|
|
|
|
show('#confirm-screen');
|
|
hide('#status-screen');
|
|
|
|
hide('#backup-button');
|
|
hide('#send-button');
|
|
hide('#unsigned-button');
|
|
$('#send-button').attr('onclick', '');
|
|
$('#send-button').text('');
|
|
$('#unsigned-button').attr('onclick', '');
|
|
$('#unsigned-button').text('');
|
|
}
|
|
|
|
function showDeposit(title, context, table, createOnly = false) {
|
|
showConfirmation(title, context, table);
|
|
show('#backup-button');
|
|
|
|
if (!createOnly) {
|
|
show('#send-button');
|
|
show('#unsigned-button');
|
|
$('#send-button').attr('onclick', 'confirmDeposit()');
|
|
$('#send-button').text('Deposit');
|
|
$('#unsigned-button').attr('onclick', 'getUnsignedDeposit()');
|
|
$('#unsigned-button').text('Unsigned');
|
|
}
|
|
}
|
|
|
|
function showWithdrawal(title, context, table) {
|
|
showConfirmation(title, context, table);
|
|
show('#send-button');
|
|
$('#send-button').attr('onclick', 'confirmWithdrawal()');
|
|
$('#send-button').text('Withdraw');
|
|
}
|
|
|
|
function showProposal(title, proposalId, table) {
|
|
showConfirmation(title, '', table);
|
|
show('#send-button');
|
|
$('#send-button').attr('onclick', `vote('${proposalId}')`);
|
|
$('#send-button').text('Vote');
|
|
}
|
|
|
|
function showAccount(title, context, table) {
|
|
showConfirmation(title, context, table);
|
|
show('#backup-button');
|
|
show('#send-button');
|
|
$('#send-button').attr('onclick', 'confirmKeyBackup()');
|
|
$('#send-button').text('Backup on-chain');
|
|
}
|
|
|
|
function showEncryptBackup(title, context, table) {
|
|
showConfirmation(title, context, table);
|
|
show('#send-button');
|
|
$('#send-button').attr('onclick', 'confirmEncryptBackup()');
|
|
$('#send-button').text('Backup on-chain');
|
|
}
|
|
|
|
function showSendCoins(title, context, table) {
|
|
showConfirmation(title, context, table);
|
|
show('#send-button');
|
|
show('#unsigned-button');
|
|
$('#send-button').attr('onclick', 'confirmSendCoins()');
|
|
$('#send-button').text('Send');
|
|
$('#unsigned-button').attr('onclick', 'getUnsigned()');
|
|
$('#unsigned-button').text('Unsigned');
|
|
}
|
|
|
|
function showStatus(title, context, status) {
|
|
$('#send-title').text(title);
|
|
|
|
$('#send-status').empty();
|
|
$('#send-status').append(context);
|
|
|
|
hide('#confirm-screen');
|
|
hide('#backup-button');
|
|
hide('#send-button');
|
|
hide('#unsigned-button');
|
|
show('#status-screen');
|
|
|
|
if (!status) {
|
|
hide('#send-success');
|
|
hide('#send-error');
|
|
show('#send-loading');
|
|
return;
|
|
}
|
|
|
|
if (status === 'success') {
|
|
show('#send-success');
|
|
hide('#send-error');
|
|
hide('#send-loading');
|
|
return;
|
|
}
|
|
|
|
if (status === 'error') {
|
|
hide('#send-success');
|
|
show('#send-error');
|
|
hide('#send-loading');
|
|
return;
|
|
}
|
|
}
|
|
|
|
async function getRpcUrl(netId) {
|
|
const savedRpc = await settingsDB.getValue(`saved_rpc_${netId}`);
|
|
|
|
if (!savedRpc) {
|
|
return Object.values(Tornado.getConfig(netId).rpcUrls || {})[0];
|
|
}
|
|
|
|
return JSON.parse(savedRpc);
|
|
}
|
|
|
|
async function saveRpcUrl(netId, { name, url }) {
|
|
await settingsDB.setValue(`saved_rpc_${netId}`, JSON.stringify({ name, url }));
|
|
}
|
|
|
|
async function loadSettings() {
|
|
$('#rpc-endpoints').empty();
|
|
|
|
for (const netId of Tornado.enabledChains) {
|
|
const selectedRpc = await getRpcUrl(netId);
|
|
|
|
const { rpcUrls, networkName, relayerEnsSubdomain } = Tornado.getConfig(netId);
|
|
|
|
const shortName = relayerEnsSubdomain.split('-')[0];
|
|
|
|
$('#rpc-endpoints').append(`
|
|
<div class="input-group mb-2">
|
|
<span class="input-group-text">${shortName}</span>
|
|
<select id="${netId}-rpc-select" class="form-select rpc-select">
|
|
<option value="${selectedRpc.url}" selected>${selectedRpc.name}</option>
|
|
|
|
${Object.values(rpcUrls).filter(({ name }) => name !== selectedRpc.name).map(({ name, url }) => {
|
|
return `
|
|
<option value="${url}">${name}</option>
|
|
`;
|
|
}).join('')}
|
|
|
|
${selectedRpc.name !== 'Custom RPC' ? `
|
|
<option value="custom">Custom RPC</option>
|
|
` : ''}
|
|
|
|
</select>
|
|
</div>
|
|
|
|
${selectedRpc.name !== 'Custom RPC' ? `
|
|
<input id="${netId}-rpc-custom" type="text" class="form-control mb-2 d-none" placeholder="Custom ${networkName} RPC URL">
|
|
` : `
|
|
<input id="${netId}-rpc-custom" type="text" class="form-control mb-2" value="${selectedRpc.url}">
|
|
`}
|
|
`);
|
|
}
|
|
|
|
$('.rpc-select').on('change', function (e) {
|
|
e.preventDefault();
|
|
|
|
const netId = Number($(this).attr('id').split('-rpc-select')[0]);
|
|
|
|
if ($(this).find(':selected').val() === 'custom') {
|
|
|
|
$(`#${netId}-rpc-custom`).removeClass('d-none');
|
|
|
|
} else {
|
|
|
|
$(`#${netId}-rpc-custom`).addClass('d-none');
|
|
}
|
|
});
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function resetSettings() {
|
|
viewOnly = '';
|
|
$('#view-only').val('');
|
|
|
|
mnemonic = '';
|
|
mnemonicIndex = 0;
|
|
$('#mnemonic').val('');
|
|
$('#mnemonic-index').val('0');
|
|
|
|
privateKey = '';
|
|
$('#private-key').val('');
|
|
|
|
encryptKey = '';
|
|
$('#encrypt-key').val('');
|
|
|
|
if (settingsDB.dbExists) {
|
|
await settingsDB._removeExist();
|
|
}
|
|
|
|
await loadSettings();
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function saveSettings() {
|
|
for (const netId of Tornado.enabledChains) {
|
|
const oldRpc = await getRpcUrl(netId);
|
|
|
|
const selectedRpc = {
|
|
name: $(`#${netId}-rpc-select`).find(':selected').text(),
|
|
url: $(`#${netId}-rpc-select`).find(':selected').val(),
|
|
};
|
|
|
|
if (selectedRpc.url === 'custom') {
|
|
selectedRpc.url = $(`#${netId}-rpc-custom`).val();
|
|
}
|
|
|
|
if (selectedRpc.name !== oldRpc.name && selectedRpc.url) {
|
|
const { networkName } = Tornado.getConfig(netId);
|
|
|
|
try {
|
|
const provider = await Tornado.getProvider(selectedRpc.url, {
|
|
netId,
|
|
});
|
|
|
|
providers.set(netId, provider);
|
|
|
|
await saveRpcUrl(netId, selectedRpc);
|
|
|
|
notifyMsg(`Successfully changed RPC for ${networkName} to ${selectedRpc.name} (${selectedRpc.url})`);
|
|
|
|
} catch (err) {
|
|
errorMsg(`Ignoring changes for RPC ${selectedRpc.url} ${networkName} due to error: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
}
|
|
|
|
const newViewOnly = $('#view-only').val();
|
|
|
|
if (ethers.isAddress(newViewOnly)) {
|
|
viewOnly = ethers.getAddress(newViewOnly);
|
|
|
|
notifyMsg(`Will use ${viewOnly} for view-only wallet (not saved, will be cleared once refresh)`);
|
|
|
|
} else {
|
|
viewOnly = '';
|
|
|
|
$('#view-only').val('');
|
|
}
|
|
|
|
const newMnemonic = $('#mnemonic').val();
|
|
|
|
if (newMnemonic) {
|
|
try {
|
|
const newIndex = parseInt($('#mnemonic-index').val());
|
|
const path = `m/44'/60'/0'/0/${newIndex}`;
|
|
const { address } = ethers.HDNodeWallet.fromPhrase(newMnemonic, undefined, path);
|
|
|
|
mnemonic = newMnemonic;
|
|
mnemonicIndex = newIndex;
|
|
|
|
notifyMsg(`Will use ${address} for wallet withdrawals (not saved, will be cleared once refresh)`);
|
|
|
|
} catch {
|
|
mnemonic = '';
|
|
mnemonicIndex = 0;
|
|
|
|
$('#mnemonic').val('');
|
|
$('#mnemonic-index').val('0');
|
|
|
|
errorMsg(`Failed to derive wallet from mnemonic ${newMnemonic}, will clear the value`);
|
|
|
|
}
|
|
} else {
|
|
mnemonic = '';
|
|
mnemonicIndex = 0;
|
|
|
|
$('#mnemonic').val('');
|
|
$('#mnemonic-index').val('0');
|
|
}
|
|
|
|
const newKey = /(^|\b)(0x)?[0-9a-fA-F]{64}(\b|$)/.exec($('#private-key').val())?.[0];
|
|
|
|
if (newKey) {
|
|
try {
|
|
const address = ethers.computeAddress(newKey);
|
|
|
|
privateKey = newKey;
|
|
|
|
notifyMsg(`Will use ${address} for wallet withdrawals (not saved, will be cleared once refresh)`);
|
|
} catch {
|
|
privateKey = '';
|
|
|
|
$('#private-key').val('');
|
|
|
|
errorMsg(`Failed to derive wallet from private key ${newKey}, will clear the value`);
|
|
}
|
|
} else {
|
|
privateKey = '';
|
|
|
|
$('#private-key').val('');
|
|
}
|
|
|
|
// Encryption key uses the same format as private key (without 0x prefix)
|
|
const newEncryptKey = /(^|\b)[0-9a-fA-F]{64}(\b|$)/.exec($('#encrypt-key').val())?.[0];
|
|
|
|
if (newEncryptKey) {
|
|
try {
|
|
ethers.computeAddress(`0x${newEncryptKey}`);
|
|
|
|
encryptKey = newEncryptKey;
|
|
|
|
notifyMsg(`Will use ${encryptKey} for note encryption (not saved, will be cleared once refresh)`);
|
|
} catch {
|
|
encryptKey = '';
|
|
|
|
$('#encrypt-key').val('');
|
|
|
|
errorMsg(`Failed to verify encrypt key ${newEncryptKey}, will clear the value`);
|
|
}
|
|
} else {
|
|
encryptKey = '';
|
|
|
|
$('#encrypt-key').val('');
|
|
}
|
|
|
|
await loadSettings();
|
|
}
|
|
|
|
async function checkIP() {
|
|
try {
|
|
// Only check IP on public instances
|
|
if (new URL(window.location.href).protocol !== 'https:') {
|
|
return;
|
|
}
|
|
|
|
const { ip, iso, tor } = await Tornado.fetchIp(IP_ECHO);
|
|
|
|
const prohibited = Boolean(PROHIBITED_COUNTRIES.find(c => c.iso === iso)) && !tor;
|
|
|
|
console.log(`Your IP: ${ip} (${iso}) ${tor ? '(TOR)' : ''} ${prohibited ? '(Prohibited)' : ''}`);
|
|
|
|
// Show prohibited prompt if it is not TOR and the IP is from one of those countries
|
|
if (!prohibited) {
|
|
return;
|
|
}
|
|
|
|
$('#prohibited-context').empty();
|
|
$('#prohibited-context').append(`
|
|
<p>Following countries are denied access to the UI:</p>
|
|
|
|
<p>${PROHIBITED_COUNTRIES.map(({ country }) => country).join(', ')}</p>
|
|
|
|
<p>By confirming with the button below you are confirming that you aren't citizen of the countries above</p>
|
|
`);
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#prohibited', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
|
|
} catch (err) {
|
|
console.log('Failed to check IP');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
function getStaticRoot() {
|
|
const url = new URL(window.location.href);
|
|
|
|
if (url.protocol === 'file:' || url.origin.includes('tornadowithdraw.com') || url.origin.includes('dweb.link')) {
|
|
return {
|
|
staticRoot: JSDELIVR,
|
|
origin: false,
|
|
};
|
|
}
|
|
|
|
return {
|
|
staticRoot: './static',
|
|
origin: true,
|
|
};
|
|
}
|
|
|
|
async function getCachedFile(zipName, zipDigest) {
|
|
// If archived file is text like json or js
|
|
const isString = !zipName.endsWith('.bin');
|
|
|
|
const { staticRoot: staticUrl } = getStaticRoot();
|
|
|
|
if (settingsDB.dbExists) {
|
|
const cachedFile = await settingsDB.getValue(zipName);
|
|
|
|
if (cachedFile) {
|
|
return isString ? cachedFile : Tornado.base64ToBytes(cachedFile);
|
|
}
|
|
}
|
|
|
|
const fileBytes = await Tornado.downloadZip({
|
|
staticUrl,
|
|
zipName,
|
|
zipDigest,
|
|
parseJson: false,
|
|
});
|
|
|
|
let fileString = isString
|
|
? new TextDecoder().decode(fileBytes)
|
|
: Tornado.bytesToBase64(fileBytes);
|
|
|
|
if (settingsDB.dbExists) {
|
|
await settingsDB.setValue(zipName, fileString);
|
|
}
|
|
|
|
return isString ? fileString : fileBytes;
|
|
}
|
|
|
|
async function getCircuit() {
|
|
const circuit = await getCachedFile('tornado.json', hashes['static/tornado.json.zip']);
|
|
|
|
return JSON.parse(circuit);
|
|
}
|
|
|
|
async function getProvingKey() {
|
|
const provingKey = await getCachedFile('tornadoProvingKey.bin', hashes['static/tornadoProvingKey.bin.zip']);
|
|
|
|
return provingKey.buffer;
|
|
}
|
|
|
|
async function checkWorker() {
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const fileName = 'merkleTreeWorker.umd.js';
|
|
const remote_url = `${staticRoot}/${fileName}`;
|
|
|
|
if (workerChecked) {
|
|
return remote_url;
|
|
}
|
|
|
|
const resp = await fetch(remote_url, {
|
|
method: 'GET',
|
|
});
|
|
|
|
if (!resp.ok) {
|
|
const errMsg = `Failed to fetch worker from ${remote_url}: ${await resp.text()}`;
|
|
throw new Error(errMsg);
|
|
}
|
|
|
|
const hash = 'sha384-' + Tornado.bytesToBase64(await Tornado.digest(await resp.arrayBuffer()));
|
|
|
|
if (hash !== hashes[`static/${fileName}`]) {
|
|
const errMsg = `Worker hash mismatch from ${remote_url}, wants ${hashes[`static/${fileName}`]} got ${hash}`;
|
|
throw new Error(errMsg);
|
|
}
|
|
|
|
workerChecked = true;
|
|
|
|
return remote_url;
|
|
}
|
|
|
|
/**
|
|
* Using importScripts to evade cross-origin limitation (Also we verify worker with hash)
|
|
* https://stackoverflow.com/questions/21913673/execute-web-worker-from-different-origin
|
|
*/
|
|
async function getWorkerUrl() {
|
|
// Do not use WebWorkers on Firefox and Tor Browser because the speed is terrible
|
|
if (navigator.userAgent.toLowerCase().includes('firefox')) {
|
|
return;
|
|
}
|
|
|
|
const { origin } = getStaticRoot();
|
|
|
|
const remote_url = await checkWorker();
|
|
|
|
if (origin) {
|
|
return remote_url;
|
|
}
|
|
|
|
const worker_url = URL.createObjectURL(
|
|
new Blob(
|
|
[`importScripts('${remote_url}')`],
|
|
{ type: 'text/javascript' }
|
|
)
|
|
);
|
|
|
|
return worker_url;
|
|
}
|
|
|
|
async function getNetworkParams(netId) {
|
|
const config = Tornado.getConfig(netId);
|
|
const { url } = await getRpcUrl(netId);
|
|
|
|
return [{
|
|
chainId: `0x${Number(netId).toString(16)}`,
|
|
chainName: config.networkName,
|
|
nativeCurrency: {
|
|
name: config.networkName,
|
|
symbol: config.nativeCurrency.toUpperCase(),
|
|
decimals: 18,
|
|
},
|
|
rpcUrls: [url],
|
|
blockExplorerUrls: [config.explorerUrl],
|
|
}];
|
|
}
|
|
|
|
async function getProvider(netId) {
|
|
if (providers.has(netId)) {
|
|
return providers.get(netId);
|
|
}
|
|
|
|
const { url } = await getRpcUrl(netId);
|
|
|
|
try {
|
|
const provider = await Tornado.getProvider(url, {
|
|
netId,
|
|
});
|
|
|
|
providers.set(netId, provider);
|
|
|
|
return provider;
|
|
} catch (err) {
|
|
errorMsg(`Failed to connect with RPC, make sure to change with working one on settings: ${err.message}`);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async function getSigner(netId) {
|
|
if (viewOnly) {
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = new VoidSigner(viewOnly, provider);
|
|
|
|
notifyMsg(`View-only signer (${signer.address}) connected`);
|
|
|
|
return signer;
|
|
}
|
|
|
|
if (mnemonic) {
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = Tornado.TornadoWallet.fromMnemonic(mnemonic, provider, mnemonicIndex || 0);
|
|
|
|
notifyMsg(`Mnemonic signer (${signer.address}) connected`);
|
|
|
|
return signer;
|
|
}
|
|
|
|
if (privateKey) {
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = new Tornado.TornadoWallet(privateKey, provider);
|
|
|
|
notifyMsg(`Private key signer (${signer.address}) connected`);
|
|
|
|
return signer;
|
|
}
|
|
|
|
if (browserSigner.has(netId)) {
|
|
return browserSigner.get(netId);
|
|
}
|
|
|
|
if (!window.ethereum) {
|
|
throw new Error('Browser Wallet not found, make sure you have installed one or try with private key on settings');
|
|
}
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const handleChain = async (netId) => {
|
|
try {
|
|
await window.ethereum.request({
|
|
method: 'wallet_switchEthereumChain',
|
|
params: [{ chainId: `0x${Number(netId).toString(16)}` }]
|
|
});
|
|
} catch (switchError) {
|
|
if (switchError.code === 4902) {
|
|
await window.ethereum.request({
|
|
method: 'wallet_addEthereumChain',
|
|
params: await getNetworkParams(netId),
|
|
});
|
|
} else {
|
|
throw switchError;
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleWalletFunc = () => {
|
|
if (globalThis.walletTimeout) {
|
|
return;
|
|
}
|
|
|
|
globalThis.walletTimeout = setTimeout(() => {
|
|
browserSigner = new Map();
|
|
|
|
notifyMsg('Browser Wallet signer disconnected');
|
|
|
|
globalThis.walletTimeout = null;
|
|
}, 500);
|
|
};
|
|
|
|
// Prevent immediate disconnection
|
|
globalThis.walletTimeout = true;
|
|
|
|
const browserProvider = new Tornado.TornadoBrowserProvider(window.ethereum, await provider.getNetwork(), {
|
|
netId,
|
|
connectWallet: handleChain,
|
|
handleNetworkChanges: handleWalletFunc,
|
|
handleAccountChanges: handleWalletFunc,
|
|
handleAccountDisconnect: handleWalletFunc,
|
|
});
|
|
|
|
const signer = await browserProvider.getSigner(0);
|
|
|
|
browserSigner.set(netId, signer);
|
|
|
|
// Prevent immediate disconnection
|
|
setTimeout(() => {
|
|
globalThis.walletTimeout = null;
|
|
}, 5000);
|
|
|
|
notifyMsg(`Browser Wallet signer (${signer.address}) connected`);
|
|
|
|
return signer;
|
|
}
|
|
|
|
/**
|
|
* Deposit
|
|
*/
|
|
function displayNetworks() {
|
|
$('.network-list').empty();
|
|
|
|
for (const netId of Tornado.enabledChains) {
|
|
const { networkName } = Tornado.getConfig(netId);
|
|
|
|
$('.network-list').append(`
|
|
<option value="${netId}">${networkName}</option>
|
|
`);
|
|
}
|
|
|
|
$('#gas-zip-outbounds').empty();
|
|
|
|
$('#gas-zip-outbounds').append(`
|
|
${Tornado.enabledChains.map(netId => {
|
|
const { networkName } = Tornado.getConfig(netId);
|
|
|
|
return `
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" value="${networkName}" disabled="">
|
|
<div class="input-group-text">
|
|
<input id="${netId}-gas-zip" class="form-check-input mt-0" type="checkbox" value="">
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('')}
|
|
`);
|
|
}
|
|
|
|
function displayCurrency(currency) {
|
|
const netId = parseInt($('#deposit-network').find(':selected').val());
|
|
|
|
if (!Tornado.enabledChains.includes(netId)) {
|
|
return;
|
|
}
|
|
|
|
$('#deposit-currency').empty();
|
|
|
|
const tokens = Tornado.getActiveTokens(Tornado.getConfig(netId));
|
|
|
|
for (const token of tokens) {
|
|
$('#deposit-currency').append(`
|
|
<option value="${token}">${token.toUpperCase()}</option>
|
|
`);
|
|
}
|
|
|
|
if (currency) {
|
|
$('#deposit-currency').val(currency);
|
|
}
|
|
}
|
|
|
|
function displayAmount(selectedAmount) {
|
|
const netId = parseInt($('#deposit-network').find(':selected').val());
|
|
const currency = $('#deposit-currency').find(':selected').val();
|
|
|
|
if (!Tornado.enabledChains.includes(netId)) {
|
|
return;
|
|
}
|
|
|
|
const { tokens } = Tornado.getConfig(netId);
|
|
|
|
if (!tokens[currency]) {
|
|
return;
|
|
}
|
|
|
|
const amounts = Object.keys(tokens[currency].instanceAddress).sort((a, b) => Number(a) - Number(b));
|
|
|
|
$('#deposit-amount').empty();
|
|
|
|
for (const amount of amounts) {
|
|
$('#deposit-amount').append(`
|
|
<option value="${amount}">${amount}</option>
|
|
`);
|
|
}
|
|
|
|
if (selectedAmount) {
|
|
$('#deposit-amount').val(selectedAmount);
|
|
}
|
|
}
|
|
|
|
let loadingStatistics = false;
|
|
let statisticsInstance = '';
|
|
|
|
function displayStatistics(netId, currency, amount) {
|
|
if (loadingStatistics) {
|
|
return;
|
|
}
|
|
|
|
if (!netId) {
|
|
netId = parseInt($('#deposit-network').find(':selected').val());
|
|
}
|
|
|
|
if (!currency) {
|
|
currency = $('#deposit-currency').find(':selected').val();
|
|
}
|
|
|
|
if (!amount) {
|
|
amount = $('#deposit-amount').find(':selected').val();
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
if (!Tornado.enabledChains.includes(netId)) {
|
|
return;
|
|
}
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const instanceAddress = config.tokens[currency]?.instanceAddress?.[amount];
|
|
|
|
if (!instanceAddress) {
|
|
return;
|
|
}
|
|
|
|
const instance = `${config.networkName} ${amount} ${currency.toUpperCase()}`;
|
|
|
|
if (statisticsInstance === instance) {
|
|
return;
|
|
}
|
|
|
|
loadingStatistics = true;
|
|
statisticsInstance = instance;
|
|
|
|
try {
|
|
const provider = await getProvider(netId);
|
|
|
|
const Tornado = TornadoContracts.Tornado__factory.connect(
|
|
instanceAddress,
|
|
provider,
|
|
);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
if (!tovarishClient.selectedRelayer) {
|
|
return;
|
|
}
|
|
|
|
const [nextIndex, { events }] = await Promise.all([
|
|
Tornado.nextIndex(),
|
|
tovarishClient.getEvents({
|
|
type: 'deposit',
|
|
currency,
|
|
amount,
|
|
fromBlock: 0,
|
|
recent: true,
|
|
}),
|
|
]);
|
|
|
|
const firstRow = events.slice(0, 5);
|
|
const secondRow = events.slice(5);
|
|
|
|
let statsTable = '';
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
const firstElement = firstRow[i];
|
|
const secondElement = secondRow[i];
|
|
|
|
statsTable += `
|
|
<tr>
|
|
<td class="col-md-5">${firstElement ? `${firstElement.leafIndex + 1}. ${moment.unix(firstElement.timestamp).fromNow()}` : '.'}</td>
|
|
<td class="col-md-5">${secondElement ? `${secondElement.leafIndex + 1}. ${moment.unix(secondElement.timestamp).fromNow()}` : '.'}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
$('#statistics-table').empty();
|
|
$('#statistics-table').append(`
|
|
<tbody>
|
|
${statsTable}
|
|
</tbody>
|
|
`);
|
|
$('#statistics-instance').text(statisticsInstance);
|
|
$('#statistics-deposits').text(nextIndex);
|
|
|
|
loadingStatistics = false;
|
|
} catch (err) {
|
|
errorMsg(`Failed to fetch statistics: ${err.message}`);
|
|
console.log(err);
|
|
loadingStatistics = false;
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
function changeDisplay({ netId, currency, amount }) {
|
|
$('#deposit-network').val(netId);
|
|
displayCurrency(currency);
|
|
displayAmount(amount);
|
|
displayStatistics(netId, currency, amount);
|
|
}
|
|
|
|
async function prepareDeposit({ netId, currency, amount, commitmentHex, encryptNote, createOnly }) {
|
|
const {
|
|
networkName,
|
|
explorerUrl,
|
|
nativeCurrency,
|
|
routerContract,
|
|
multicallContract,
|
|
offchainOracleContract,
|
|
tokens: {
|
|
[currency]: {
|
|
decimals,
|
|
tokenAddress,
|
|
instanceAddress: {
|
|
[amount]: instanceAddress,
|
|
},
|
|
instanceApproval,
|
|
},
|
|
},
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const isEth = nativeCurrency === currency;
|
|
const denomination = ethers.parseUnits(amount, decimals);
|
|
|
|
try {
|
|
job = {};
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Connecting Wallet', 'Connecting Wallet and RPC');
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = !createOnly ? await getSigner(netId) : undefined;
|
|
|
|
const TornadoProxy = TornadoContracts.TornadoRouter__factory.connect(routerContract, provider);
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
const Token = tokenAddress ? TornadoContracts.ERC20__factory.connect(tokenAddress, provider) : undefined;
|
|
|
|
const tornadoFeeOracle = new Tornado.TornadoFeeOracle(provider);
|
|
|
|
const tokenPriceOracle = new Tornado.TokenPriceOracle(
|
|
provider,
|
|
Multicall,
|
|
offchainOracleContract
|
|
? TornadoContracts.OffchainOracle__factory.connect(offchainOracleContract, provider)
|
|
: undefined,
|
|
);
|
|
|
|
showStatus('Preparing Deposit', 'Creating Deposit Note');
|
|
|
|
const [gasPrice, [ethBalance, tokenPrice, tokenBalance, tokenApprovals]] = await Promise.all([
|
|
tornadoFeeOracle.gasPrice(),
|
|
Tornado.multicall(Multicall, [
|
|
{
|
|
contract: Multicall,
|
|
name: 'getEthBalance',
|
|
params: [signer?.address || ethers.ZeroAddress],
|
|
},
|
|
...(!isEth
|
|
? [
|
|
...(tokenPriceOracle.buildCalls([{ tokenAddress, decimals }])),
|
|
{
|
|
contract: Token,
|
|
name: 'balanceOf',
|
|
params: [signer?.address || ethers.ZeroAddress],
|
|
},
|
|
{
|
|
contract: Token,
|
|
name: 'allowance',
|
|
params: [signer?.address || ethers.ZeroAddress, routerContract],
|
|
},
|
|
]
|
|
: []
|
|
),
|
|
]),
|
|
]);
|
|
|
|
const tokenPriceInWei = tokenPrice
|
|
? tokenPrice * BigInt(10 ** decimals) / BigInt(10 ** 18)
|
|
: BigInt(0);
|
|
|
|
if (!createOnly) {
|
|
if (isEth && denomination > ethBalance) {
|
|
const errMsg = `Invalid ${currency.toUpperCase()} balance, wants ${amount} ${currency.toUpperCase()} have ${ethers.formatUnits(ethBalance, decimals)} ${currency.toUpperCase()}`;
|
|
throw new Error(errMsg);
|
|
} else if (!isEth && denomination > tokenBalance) {
|
|
const errMsg = `Invalid ${currency.toUpperCase()} balance, wants ${amount} ${currency.toUpperCase()} have ${ethers.formatUnits(tokenBalance, decimals)} ${currency.toUpperCase()}`;
|
|
throw new Error(errMsg);
|
|
}
|
|
}
|
|
|
|
const requiresApproval = (!isEth && denomination > tokenApprovals) ? true : false;
|
|
const approvalData = requiresApproval
|
|
? (await Token.approve.populateTransaction(instanceApproval ? instanceAddress : routerContract, ethers.MaxUint256)).data
|
|
: undefined;
|
|
|
|
let note;
|
|
let invoice;
|
|
let encryptedNote = '0x';
|
|
|
|
// Create new note
|
|
if (!commitmentHex) {
|
|
const deposit = await Tornado.Deposit.createNote({ currency, amount, netId });
|
|
|
|
({ note, invoice, commitmentHex } = deposit);
|
|
|
|
const { noteHex } = deposit;
|
|
|
|
// Store encrypted note on Router contract as event
|
|
if (encryptNote && encryptKey) {
|
|
const noteAccount = new Tornado.NoteAccount({
|
|
recoveryKey: encryptKey,
|
|
});
|
|
|
|
encryptedNote = noteAccount.encryptNote({
|
|
address: instanceAddress,
|
|
noteHex,
|
|
});
|
|
}
|
|
|
|
const backupFile = `backup-tornado-${currency}-${amount}-${netId}-${noteHex.slice(0, 10)}.txt`;
|
|
|
|
$('#backup-button').attr('download', backupFile);
|
|
$('#backup-button').attr('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(note));
|
|
document.getElementById('backup-button').click();
|
|
|
|
} else {
|
|
invoice = `tornadoInvoice-${currency}-${amount}-${netId}-${commitmentHex}`;
|
|
const backupFile = `backup-tornadoInvoice-${currency}-${amount}-${netId}-${commitmentHex.slice(0, 10)}.txt`;
|
|
|
|
$('#backup-button').attr('download', backupFile);
|
|
$('#backup-button').attr('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(invoice));
|
|
}
|
|
|
|
const value = isEth ? denomination : BigInt(0);
|
|
|
|
const { data } = await TornadoProxy.deposit.populateTransaction(
|
|
instanceAddress,
|
|
commitmentHex,
|
|
encryptedNote,
|
|
{
|
|
value,
|
|
}
|
|
);
|
|
|
|
let gasLimit = BigInt(1_000_000);
|
|
|
|
// Ignore gas limit error as we are simulating not sending anything
|
|
try {
|
|
gasLimit = await TornadoProxy.deposit.estimateGas(
|
|
instanceAddress,
|
|
commitmentHex,
|
|
encryptedNote,
|
|
{
|
|
value,
|
|
from: signer?.address || undefined,
|
|
}
|
|
);
|
|
// eslint-disable-next-line no-empty
|
|
} catch {}
|
|
|
|
const txFee = gasPrice * gasLimit;
|
|
const txFeeInToken = !isEth
|
|
? tornadoFeeOracle.calculateTokenAmount(txFee, tokenPriceInWei, decimals)
|
|
: BigInt(0);
|
|
const txFeePercent = !isEth
|
|
? `( ${Number(ethers.formatUnits(txFeeInToken, decimals)).toFixed(5)} ${currency.toUpperCase()} worth ) ` +
|
|
`( ${((Number(ethers.formatUnits(txFeeInToken, decimals)) / Number(amount)) * 100).toFixed(5)}% )`
|
|
: `( ${((Number(ethers.formatUnits(txFee, decimals)) / Number(amount)) * 100).toFixed(5)}% )`;
|
|
|
|
showDeposit(
|
|
'Confirm Deposit',
|
|
'',
|
|
`
|
|
<table class="table table-bordered mb-2">
|
|
<tbody>
|
|
<tr>
|
|
<td>Deposit</td>
|
|
<td>${networkName} ${amount} ${currency.toUpperCase()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Commitment</td>
|
|
<td>${commitmentHex}</td>
|
|
</tr>
|
|
${note ? `
|
|
<tr>
|
|
<td>Note</td>
|
|
<td class="table-column">${note}</td>
|
|
</tr>
|
|
` : ''}
|
|
<tr>
|
|
<td>Invoice</td>
|
|
<td class="table-column">${invoice}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Tornado Proxy ( to )</td>
|
|
<td><a href="${explorerUrl}/address/${routerContract}" target="_blank" rel="noreferrer nofollow">${routerContract}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Tornado Instance ( ${amount} ${currency.toUpperCase()} )</td>
|
|
<td><a href="${explorerUrl}/address/${instanceAddress}" target="_blank" rel="noreferrer nofollow">${instanceAddress}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>${nativeCurrency.toUpperCase()} to spend ( value )</td>
|
|
<td>${ethers.formatEther(value)} ${nativeCurrency.toUpperCase()}</td>
|
|
</tr>
|
|
${!isEth ? `
|
|
<tr>
|
|
<td>Token Address</td>
|
|
<td><a href="${explorerUrl}/address/${tokenAddress}" target="_blank" rel="noreferrer nofollow">${tokenAddress}</a></td>
|
|
</tr>
|
|
` : ''}
|
|
<tr>
|
|
<td>Gas Price</td>
|
|
<td>${ethers.formatUnits(gasPrice, 'gwei')} gwei</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Limit</td>
|
|
<td>${gasLimit}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Transaction Fee</td>
|
|
<td>${Number(ethers.formatEther(txFee)).toFixed(8)} ${nativeCurrency.toUpperCase()} ${txFeePercent}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
${encryptedNote !== '0x' ? `
|
|
<p class="mt-2 mb-2">Encrypted Note</p>
|
|
<textarea type="text" class="form-control" rows="3" disabled>${encryptedNote}</textarea>
|
|
` : ''}
|
|
|
|
<p class="mt-2 mb-2">Input Data</p>
|
|
<textarea type="text" class="form-control" rows="3" disabled>${data}</textarea>
|
|
|
|
${requiresApproval ? `
|
|
<p class="mt-2 mb-2">Token Approval Input</p>
|
|
<textarea type="text" class="form-control" rows="2" disabled>${approvalData}</textarea>
|
|
` : ''}
|
|
`,
|
|
createOnly
|
|
);
|
|
|
|
if (!createOnly) {
|
|
|
|
job = {
|
|
netId,
|
|
currency,
|
|
amount,
|
|
networkName,
|
|
explorerUrl,
|
|
instanceAddress,
|
|
instanceApproval,
|
|
commitmentHex,
|
|
encryptedNote,
|
|
value,
|
|
requiresApproval,
|
|
TornadoProxy: TornadoProxy.connect(signer),
|
|
Token: Token ? Token.connect(signer) : undefined,
|
|
signer,
|
|
};
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
showStatus('Error from Deposit', `Error from ${networkName} ${amount} ${currency.toUpperCase()} deposit: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function confirmDeposit() {
|
|
const {
|
|
currency,
|
|
amount,
|
|
networkName,
|
|
explorerUrl,
|
|
instanceAddress,
|
|
instanceApproval,
|
|
commitmentHex,
|
|
encryptedNote,
|
|
value,
|
|
requiresApproval,
|
|
TornadoProxy,
|
|
Token,
|
|
} = job;
|
|
|
|
try {
|
|
if (requiresApproval) {
|
|
showStatus('Approving Token', 'Approving Token Spending by Tornado Router Contract');
|
|
|
|
const resp = await Token.approve(instanceApproval ? instanceAddress : TornadoProxy.target, ethers.MaxUint256);
|
|
|
|
showStatus(
|
|
'Approving Token',
|
|
`Waiting for approval tx <a href="${explorerUrl}/tx/${resp.hash}" target="_blank" rel="noreferrer nofollow">${resp.hash}</a> to confirm.`
|
|
);
|
|
|
|
await resp.wait();
|
|
}
|
|
|
|
showStatus('Sending Deposit', 'Sending Deposit to Tornado Router Contract');
|
|
|
|
const { hash } = await TornadoProxy.deposit(
|
|
instanceAddress,
|
|
commitmentHex,
|
|
encryptedNote,
|
|
{
|
|
value
|
|
}
|
|
);
|
|
|
|
showStatus(
|
|
'Sent Deposit',
|
|
`Sent ${networkName} ${amount} ${currency.toUpperCase()} Deposit transaction <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
} catch (err) {
|
|
showStatus('Error from Sending Deposit', `Error from ${networkName} ${amount} ${currency.toUpperCase()} deposit: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
|
|
job = {};
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function getUnsignedDeposit() {
|
|
const {
|
|
currency,
|
|
amount,
|
|
networkName,
|
|
instanceAddress,
|
|
commitmentHex,
|
|
encryptedNote,
|
|
value,
|
|
requiresApproval,
|
|
TornadoProxy,
|
|
Token,
|
|
signer,
|
|
} = job;
|
|
|
|
try {
|
|
let approvalSerialized;
|
|
|
|
if (requiresApproval) {
|
|
const populated = await signer.populateTransaction(
|
|
await Token.approve.populateTransaction(TornadoProxy.target, ethers.MaxUint256)
|
|
);
|
|
delete populated.from;
|
|
|
|
approvalSerialized = ethers.Transaction.from(populated).unsignedSerialized;
|
|
}
|
|
|
|
const populated = await signer.populateTransaction(
|
|
await TornadoProxy.deposit.populateTransaction(
|
|
instanceAddress,
|
|
commitmentHex,
|
|
encryptedNote,
|
|
{
|
|
value
|
|
}
|
|
)
|
|
);
|
|
delete populated.from;
|
|
|
|
const serialized = ethers.Transaction.from(populated).unsignedSerialized;
|
|
|
|
showConfirmation(
|
|
'Unsigned Transaction',
|
|
'',
|
|
`
|
|
${approvalSerialized ? `
|
|
<p class="mb-2">Approval TX</p>
|
|
|
|
<textarea type="text" class="form-control" rows="2" disabled>${approvalSerialized}</textarea>
|
|
` : ''}
|
|
|
|
<p class="mt-2 mb-2">Deposit TX</p>
|
|
|
|
<textarea type="text" class="form-control" rows="3" disabled>${serialized}</textarea>
|
|
`
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from populating Deposit transaction', `Error from ${networkName} ${amount} ${currency.toUpperCase()} deposit: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
|
|
job = {};
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function deposit(createOnly = false) {
|
|
const netId = parseInt($('#deposit-network').find(':selected').val());
|
|
const currency = $('#deposit-currency').find(':selected').val();
|
|
const amount = $('#deposit-amount').find(':selected').val();
|
|
const encryptNote = $('#deposit-onchain').is(':checked');
|
|
|
|
if (!netId || !currency || !amount || !Tornado.enabledChains.includes(netId)) {
|
|
errorMsg('Invalid deposit input');
|
|
return;
|
|
}
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
if (!config.tokens[currency].instanceAddress[amount]) {
|
|
errorMsg(`Instance ${netId} ${currency} ${amount} not found`);
|
|
return;
|
|
}
|
|
|
|
if (encryptNote && !encryptKey) {
|
|
errorMsg('Encrypt note option is enabled however no encryption key is found, make sure you have supplied the correct key on settings');
|
|
return;
|
|
}
|
|
|
|
prepareDeposit({ netId, currency, amount, encryptNote, createOnly });
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function invoice() {
|
|
try {
|
|
const {
|
|
netId,
|
|
currency,
|
|
amount,
|
|
commitmentHex,
|
|
} = new Tornado.Invoice($('#invoice-note').val());
|
|
|
|
prepareDeposit({ netId, currency, amount, commitmentHex });
|
|
|
|
} catch {
|
|
errorMsg('Invalid Tornado Deposit Invoice, make sure you check the input again');
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Withdrawal & Compliance
|
|
*/
|
|
async function getIndexedDB(netId) {
|
|
if (AllDB.has(netId)) {
|
|
return AllDB.get(netId);
|
|
}
|
|
|
|
const idb = await Tornado.getIndexedDB(netId);
|
|
AllDB.set(netId, idb);
|
|
return idb;
|
|
}
|
|
|
|
function getEventHash(instanceName) {
|
|
return hashes[`static/events/${instanceName}.json.zip`];
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function getEvents({ netId, amount, currency, optionalTree }) {
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
deployedBlock,
|
|
networkName,
|
|
tokens: {
|
|
[currency]: {
|
|
instanceAddress: {
|
|
[amount]: instanceAddress,
|
|
}
|
|
}
|
|
}
|
|
} = config;
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const Instance = TornadoContracts.Tornado__factory.connect(instanceAddress, provider);
|
|
|
|
const TornadoServiceConstructor = {
|
|
netId,
|
|
provider,
|
|
Tornado: Instance,
|
|
amount,
|
|
currency,
|
|
deployedBlock,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
};
|
|
|
|
const depositsService = new Tornado.DBTornadoService({
|
|
...TornadoServiceConstructor,
|
|
type: 'Deposit',
|
|
merkleTreeService: new Tornado.MerkleTreeService({
|
|
netId,
|
|
amount,
|
|
currency,
|
|
Tornado: Instance,
|
|
merkleWorkerPath: await getWorkerUrl(),
|
|
}),
|
|
optionalTree,
|
|
});
|
|
|
|
depositsService.zipDigest = getEventHash(depositsService.getInstanceName());
|
|
|
|
const withdrawalService = new Tornado.DBTornadoService({
|
|
...TornadoServiceConstructor,
|
|
type: 'Withdrawal',
|
|
});
|
|
|
|
withdrawalService.zipDigest = getEventHash(withdrawalService.getInstanceName());
|
|
|
|
showStatus('Fetching Events', `Fetching Deposit Events & Tree for ${networkName} ${amount} ${currency.toUpperCase()} Instance`);
|
|
|
|
const { events: depositEvents, validateResult: tree } = await depositsService.updateEvents();
|
|
|
|
showStatus('Fetching Events', `Fetching Withdrawal Events for ${networkName} ${amount} ${currency.toUpperCase()} Instance`);
|
|
|
|
const withdrawalEvents = (await withdrawalService.updateEvents()).events;
|
|
|
|
return {
|
|
depositEvents,
|
|
withdrawalEvents,
|
|
tree,
|
|
};
|
|
}
|
|
|
|
async function updateRelayers(forceUpdate = false) {
|
|
const netId = RELAYER_NETWORK;
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
registryContract,
|
|
aggregatorContract,
|
|
constants: { REGISTRY_BLOCK }
|
|
} = config;
|
|
|
|
const registryService = new Tornado.DBRegistryService({
|
|
netId,
|
|
provider,
|
|
RelayerRegistry: TornadoContracts.RelayerRegistry__factory.connect(registryContract, provider),
|
|
Aggregator: TornadoContracts.Aggregator__factory.connect(aggregatorContract, provider),
|
|
relayerEnsSubdomains: Tornado.getRelayerEnsSubdomains(),
|
|
deployedBlock: REGISTRY_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
registryService.zipDigest = getEventHash(registryService.getInstanceName());
|
|
|
|
registryService.relayerJsonDigest = hashes['static/events/relayers.json'];
|
|
|
|
if (forceUpdate) {
|
|
await idb.clearStore({ storeName: `relayers_${netId}` });
|
|
}
|
|
|
|
const { relayers } = await registryService.updateRelayers();
|
|
|
|
// If we have new relayers
|
|
if (allRelayers.length !== relayers.length) {
|
|
allRelayers.length = 0;
|
|
allRelayers.push(...relayers);
|
|
|
|
console.log(`Updated ${allRelayers.length} relayers`);
|
|
|
|
const tovarishRelayers = allRelayers.filter((r) => r.tovarishHost && r.tovarishNetworks?.length);
|
|
|
|
// If we have new tovarish relayers
|
|
if (tovarishRelayers.length !== [...(new Set(allTovarishRelayers.map(r => r.ensName)))].length) {
|
|
const { validRelayers } = await new Tornado.TovarishClient({}).getTovarishRelayers(relayers);
|
|
|
|
allTovarishRelayers.length = 0;
|
|
allTovarishRelayers.push(...validRelayers);
|
|
|
|
console.log(`Updated ${allTovarishRelayers.length} tovarish relayers`);
|
|
}
|
|
}
|
|
|
|
return {
|
|
registryService,
|
|
relayers,
|
|
};
|
|
}
|
|
|
|
async function getAllRelayers() {
|
|
const netId = RELAYER_NETWORK;
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const registryService = new Tornado.DBRegistryService({
|
|
netId,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
registryService.relayerJsonDigest = hashes['static/events/relayers.json'];
|
|
|
|
const cachedRelayers = await registryService.getSavedRelayers();
|
|
|
|
await registryService.saveRelayers(cachedRelayers);
|
|
|
|
if (!cachedRelayers.relayers.length) {
|
|
errorMsg(`Failed to load list of relayers from ${staticRoot}/events/relayers.json the functionality might be restricted`, true);
|
|
return;
|
|
}
|
|
|
|
allRelayers.push(...cachedRelayers.relayers);
|
|
|
|
// List all tovarish relayers
|
|
const { validRelayers } = await new Tornado.TovarishClient({}).getTovarishRelayers(cachedRelayers.relayers);
|
|
|
|
allTovarishRelayers.push(...validRelayers);
|
|
|
|
displayRelayers();
|
|
updateRelayers();
|
|
}
|
|
|
|
function displayRelayers() {
|
|
$('#relayers').empty();
|
|
|
|
for (const relayer of allRelayers) {
|
|
const isTovarish = Boolean(relayer.tovarishHost);
|
|
|
|
$('#relayers').append(`
|
|
<option value="${relayer.ensName}" data-tovarish="${isTovarish ? 'true' : 'false'}">${relayer.ensName} ( ${isTovarish ? 'Tovarish' : 'Classic'} )</option>
|
|
`);
|
|
}
|
|
|
|
$('#relayers').append('<option value="wallet" data-tovarish="true">Browser Wallet / Private Key</option>');
|
|
}
|
|
|
|
function getSelectedRelayer() {
|
|
return allRelayers.find(r => r.ensName === $('#relayers').find(':selected').val());
|
|
}
|
|
|
|
/**
|
|
* Get Selected Tovarish Relayer Client used for fetching events
|
|
* ( Will Select the first working relayer client for fallbacks )
|
|
*/
|
|
function getTovarishClient(netId) {
|
|
const config = Tornado.getConfig(netId);
|
|
const ensName = $('#relayers').find(':selected').val();
|
|
|
|
const tovarishClient = new Tornado.TovarishClient({
|
|
netId,
|
|
config,
|
|
});
|
|
|
|
const selectedRelayer = allTovarishRelayers.find(r => r.netId === netId && r.ensName === ensName);
|
|
|
|
if (selectedRelayer) {
|
|
tovarishClient.selectedRelayer = selectedRelayer;
|
|
} else {
|
|
tovarishClient.selectedRelayer = allTovarishRelayers.find(r => r.netId === netId);
|
|
}
|
|
|
|
return tovarishClient;
|
|
}
|
|
|
|
async function getRelayerClient(netId) {
|
|
const relayer = getSelectedRelayer();
|
|
|
|
const isTovarish = $('#relayers').find(':selected').data('tovarish');
|
|
const isWallet = $('#relayers').find(':selected').val() === 'wallet';
|
|
|
|
if (isWallet) {
|
|
return;
|
|
}
|
|
|
|
if (!relayer) {
|
|
throw new Error('Please select the valid withdraw method, if you have refreshed the page just input the note again');
|
|
}
|
|
|
|
if (!relayer.hostnames?.[netId] && !relayer.tovarishNetworks?.includes(netId)) {
|
|
throw new Error('Selected Relayer doesn\'t support the network');
|
|
}
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
if (tovarishClient.selectedRelayer?.ensName === relayer.ensName) {
|
|
return tovarishClient;
|
|
}
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const relayerClient = isTovarish
|
|
? new Tornado.TovarishClient({
|
|
netId,
|
|
config,
|
|
})
|
|
: new Tornado.RelayerClient({
|
|
netId,
|
|
config,
|
|
});
|
|
|
|
const { validRelayers } = await relayerClient.getValidRelayers([relayer]);
|
|
|
|
if (!validRelayers.length) {
|
|
const errMsg = `Selected Relayer (${relayer?.ensName}) is invalid, select other relayer`;
|
|
throw new Error(errMsg);
|
|
}
|
|
|
|
relayerClient.selectedRelayer = validRelayers[0];
|
|
|
|
return relayerClient;
|
|
}
|
|
|
|
function getWithdrawRecipient() {
|
|
const address = $('#withdraw-recipient').val();
|
|
|
|
if (!ethers.isAddress(address)) {
|
|
return;
|
|
}
|
|
|
|
return ethers.getAddress(address);
|
|
}
|
|
|
|
function prepareCompliance(deposit, depositEvent, withdrawalEvent) {
|
|
const { netId, amount, currency } = deposit;
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
networkName,
|
|
explorerUrl,
|
|
tokens: {
|
|
[currency]: {
|
|
decimals,
|
|
}
|
|
}
|
|
} = config;
|
|
|
|
const depositDate = new Date(depositEvent.timestamp * 1000);
|
|
const withdrawalDate = withdrawalEvent ? new Date(withdrawalEvent.timestamp * 1000) : undefined;
|
|
|
|
showConfirmation(
|
|
'Compliance',
|
|
`Following is a compliance info for your deposit and withdrawal from ${networkName} ${amount} ${currency.toUpperCase()} deposit`,
|
|
`
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td>Deposit Date</td>
|
|
<td>${depositDate.toLocaleDateString()} ${depositDate.toLocaleTimeString()} (${moment.unix(depositEvent.timestamp).fromNow()})</td>
|
|
</tr>
|
|
<tr>
|
|
<td>From</td>
|
|
<td><a href="${explorerUrl}/address/${depositEvent.from}" target="_blank" rel="noreferrer nofollow">${depositEvent.from}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Deposit Transaction</td>
|
|
<td><a href="${explorerUrl}/tx/${depositEvent.transactionHash}" target="_blank" rel="noreferrer nofollow">${depositEvent.transactionHash}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Commitment</td>
|
|
<td>${depositEvent.commitment}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Spent</td>
|
|
<td>${Boolean(withdrawalEvent)}</td>
|
|
</tr>
|
|
${!withdrawalEvent ? '' : `
|
|
<tr>
|
|
<td>Withdrawal Date</td>
|
|
<td>${withdrawalDate.toLocaleDateString()} ${withdrawalDate.toLocaleTimeString()} (${moment.unix(withdrawalEvent.timestamp).fromNow()})</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Relayer Fee</td>
|
|
<td>${ethers.formatUnits(withdrawalEvent.fee, decimals)} ${currency.toUpperCase()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>To</td>
|
|
<td><a href="${explorerUrl}/address/${withdrawalEvent.to}" target="_blank" rel="noreferrer nofollow">${withdrawalEvent.to}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Withdrawal Transaction</td>
|
|
<td><a href="${explorerUrl}/tx/${withdrawalEvent.transactionHash}" target="_blank" rel="noreferrer nofollow">${withdrawalEvent.transactionHash}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Nullifier</td>
|
|
<td>${withdrawalEvent.nullifierHash}</td>
|
|
</tr>
|
|
`}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
}
|
|
|
|
async function createProof(deposit, depositEvent, tree, relayerClient) {
|
|
if (typeof WebAssembly === 'undefined') {
|
|
throw new Error('Please turn on WebAssembly in your browser settings.<br /> If you are using Tor browser, enable javascript.options.wasm in about:config');
|
|
}
|
|
|
|
job = {};
|
|
|
|
const {
|
|
netId,
|
|
currency,
|
|
amount,
|
|
nullifierHex,
|
|
nullifier,
|
|
secret,
|
|
} = deposit;
|
|
|
|
// Use donation address if the recipient is unspecified
|
|
const recipient = getWithdrawRecipient();
|
|
const isWallet = $('#relayers').find(':selected').val() === 'wallet';
|
|
|
|
const {
|
|
pathElements,
|
|
pathIndices,
|
|
} = tree.path(depositEvent.leafIndex);
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
explorerUrl,
|
|
networkName,
|
|
nativeCurrency,
|
|
routerContract,
|
|
ovmGasPriceOracleContract,
|
|
offchainOracleContract,
|
|
multicallContract,
|
|
tokens: {
|
|
[currency]: {
|
|
decimals,
|
|
tokenAddress,
|
|
gasLimit: instanceGasLimit,
|
|
tokenGasLimit,
|
|
instanceAddress: {
|
|
[amount]: instanceAddress,
|
|
},
|
|
},
|
|
},
|
|
} = config;
|
|
|
|
const isEth = currency === nativeCurrency;
|
|
const denomination = ethers.parseUnits(amount, decimals);
|
|
const firstAmount = Object.keys(config.tokens[currency].instanceAddress).sort((a, b) => Number(a) - Number(b))[0];
|
|
const isFirstAmount = Number(amount) === Number(firstAmount);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = isWallet ? await getSigner(netId) : undefined;
|
|
|
|
const TornadoProxy = TornadoContracts.TornadoRouter__factory.connect(
|
|
routerContract,
|
|
!isWallet ? provider : signer,
|
|
);
|
|
|
|
const tornadoFeeOracle = new Tornado.TornadoFeeOracle(
|
|
provider,
|
|
ovmGasPriceOracleContract
|
|
? TornadoContracts.OvmGasPriceOracle__factory.connect(ovmGasPriceOracleContract, provider)
|
|
: undefined,
|
|
);
|
|
|
|
const tokenPriceOracle = new Tornado.TokenPriceOracle(
|
|
provider,
|
|
TornadoContracts.Multicall__factory.connect(multicallContract, provider),
|
|
offchainOracleContract
|
|
? TornadoContracts.OffchainOracle__factory.connect(offchainOracleContract, provider)
|
|
: undefined
|
|
);
|
|
|
|
showStatus('Fetching Keys', 'Fetching Circuits and Proving Keys');
|
|
|
|
const [circuit, provingKey, netGasPrice, l1Fee, tokenPriceInWei] = await Promise.all([
|
|
getCircuit(),
|
|
getProvingKey(),
|
|
tornadoFeeOracle.gasPrice(),
|
|
tornadoFeeOracle.fetchL1OptimismFee(),
|
|
!isEth ? tokenPriceOracle.fetchPrice(tokenAddress, decimals) : BigInt(0),
|
|
]);
|
|
|
|
let gasPrice = netGasPrice;
|
|
|
|
if (!isWallet && !relayerClient?.tovarish && netId === Tornado.NetId.BSC) {
|
|
gasPrice = ethers.parseUnits('3.3', 'gwei');
|
|
}
|
|
|
|
// If the config overrides default gas limit we override
|
|
const defaultGasLimit = instanceGasLimit ? BigInt(instanceGasLimit) : BigInt(DEFAULT_GAS_LIMIT);
|
|
let gasLimit = defaultGasLimit;
|
|
|
|
// If the denomination is small only refund small amount otherwise use the default value
|
|
const refundGasLimit = isFirstAmount && tokenGasLimit ? BigInt(tokenGasLimit) : undefined;
|
|
|
|
async function getProof() {
|
|
let relayer = ethers.ZeroAddress;
|
|
let fee = BigInt(0);
|
|
let refund = BigInt(0);
|
|
|
|
if (!isWallet) {
|
|
if (!isEth) {
|
|
refund = $('#withdraw-refund').is(':checked')
|
|
? tornadoFeeOracle.defaultEthRefund(gasPrice, refundGasLimit)
|
|
: BigInt(0);
|
|
}
|
|
|
|
const {
|
|
rewardAccount,
|
|
tornadoServiceFee: relayerFeePercent,
|
|
} = relayerClient?.selectedRelayer || {};
|
|
|
|
fee = tornadoFeeOracle.calculateRelayerFee({
|
|
gasPrice,
|
|
gasLimit,
|
|
l1Fee,
|
|
denomination,
|
|
ethRefund: refund,
|
|
tokenPriceInWei,
|
|
tokenDecimals: decimals,
|
|
relayerFeePercent,
|
|
isEth,
|
|
});
|
|
|
|
relayer = rewardAccount;
|
|
|
|
if (fee > denomination) {
|
|
const errMsg = `Relayer fee ${ethers.formatUnits(fee, decimals)} ${currency.toUpperCase()} `
|
|
+ `exceeds the deposit amount ${amount} ${currency.toUpperCase()}. `
|
|
+ 'Try with other relayer or use wallet withdrawal';
|
|
|
|
throw new Error(errMsg);
|
|
}
|
|
}
|
|
|
|
const { proof, args } = await Tornado.calculateSnarkProof(
|
|
{
|
|
root: tree.root,
|
|
nullifierHex,
|
|
recipient,
|
|
relayer,
|
|
fee,
|
|
refund,
|
|
nullifier,
|
|
secret,
|
|
pathElements,
|
|
pathIndices,
|
|
},
|
|
circuit,
|
|
provingKey,
|
|
);
|
|
|
|
return {
|
|
fee,
|
|
refund,
|
|
proof,
|
|
args,
|
|
};
|
|
}
|
|
|
|
showStatus('Creating Proof', 'Calculating Withdrawal Proof');
|
|
|
|
let { fee, refund, proof, args } = await getProof();
|
|
|
|
const withdrawOverrides = {
|
|
//from: !isWallet ? relayerClient?.selectedRelayer?.rewardAccount : signer?.address,
|
|
value: args[5] || 0,
|
|
};
|
|
|
|
gasLimit = await TornadoProxy.withdraw.estimateGas(instanceAddress, proof, ...args, withdrawOverrides);
|
|
|
|
if (fee) {
|
|
// Recalculate fees and snarkProof based on gasLimit
|
|
showStatus('Creating Proof', 'Calculating Optimal Withdrawal Fees and Proof');
|
|
|
|
({ fee, refund, proof, args } = await getProof());
|
|
|
|
// Verify if our recalculated proof can be withdrawn
|
|
await TornadoProxy.withdraw.estimateGas(instanceAddress, proof, ...args, withdrawOverrides);
|
|
}
|
|
|
|
let withdrawTable = '';
|
|
|
|
if (!isWallet) {
|
|
const relayerFeePercent = Number(relayerClient?.selectedRelayer?.tornadoServiceFee) || 0;
|
|
const relayerFee = (BigInt(denomination) * BigInt(Math.floor(10000 * relayerFeePercent))) / BigInt(10000 * 100);
|
|
|
|
withdrawTable = `
|
|
<tr>
|
|
<td>Relayer</td>
|
|
<td>${relayerClient?.selectedRelayer?.url}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Relayer Fee</td>
|
|
<td>${ethers.formatUnits(relayerFee, decimals)} ${currency.toUpperCase()} ( ${relayerFeePercent}% )</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Total Fee</td>
|
|
<td>${ethers.formatUnits(fee, decimals)} ${currency.toUpperCase()} ( ${((Number(fee) / Number(denomination)) * 100).toFixed(5)}% )</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Amount to receive</td>
|
|
<td>${Number(ethers.formatUnits(denomination - fee, decimals)).toFixed(5)} ${currency.toUpperCase()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>${nativeCurrency.toUpperCase()} purchase</td>
|
|
<td>${ethers.formatEther(refund)} ${nativeCurrency.toUpperCase()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>To</td>
|
|
<td><a href="${explorerUrl}/address/${recipient}" target="_blank" rel="noreferrer nofollow">${recipient === DONATION_ADDRESS ? 'Donation' : recipient}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Nullifier</td>
|
|
<td>${nullifierHex}</td>
|
|
</tr>
|
|
`;
|
|
} else {
|
|
withdrawTable = `
|
|
<tr>
|
|
<td>Signer</td>
|
|
<td><a href="${explorerUrl}/address/${signer?.address}" target="_blank" rel="noreferrer nofollow">${signer?.address}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Amount to receive</td>
|
|
<td>${amount} ${currency.toUpperCase()}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>To</td>
|
|
<td><a href="${explorerUrl}/address/${recipient}" target="_blank" rel="noreferrer nofollow">${recipient === DONATION_ADDRESS ? 'Donation' : recipient}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Nullifier</td>
|
|
<td>${nullifierHex}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
const depositDate = new Date(depositEvent.timestamp * 1000);
|
|
|
|
const txFee = gasPrice * gasLimit;
|
|
const txFeeInToken = !isEth
|
|
? tornadoFeeOracle.calculateTokenAmount(txFee, tokenPriceInWei, decimals)
|
|
: BigInt(0);
|
|
const txFeeString = !isEth
|
|
? `( ${Number(ethers.formatUnits(txFeeInToken, decimals)).toFixed(5)} ${currency.toUpperCase()} worth ) ` +
|
|
`( ${((Number(ethers.formatUnits(txFeeInToken, decimals)) / Number(amount)) * 100).toFixed(5)}% )`
|
|
: `( ${((Number(ethers.formatUnits(txFee, decimals)) / Number(amount)) * 100).toFixed(5)}% )`;
|
|
|
|
showWithdrawal(
|
|
'Confirm Withdrawal',
|
|
`Review your withdrawal information for ${networkName} ${amount} ${currency.toUpperCase()} deposit`,
|
|
`
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td>Deposit Date</td>
|
|
<td>${depositDate.toLocaleDateString()} ${depositDate.toLocaleTimeString()} (${moment.unix(depositEvent.timestamp).fromNow()})</td>
|
|
</tr>
|
|
<tr>
|
|
<td>From</td>
|
|
<td><a href="${explorerUrl}/address/${depositEvent.from}" target="_blank" rel="noreferrer nofollow">${depositEvent.from}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Deposit Transaction</td>
|
|
<td><a href="${explorerUrl}/tx/${depositEvent.transactionHash}" target="_blank" rel="noreferrer nofollow">${depositEvent.transactionHash}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Commitment</td>
|
|
<td>${depositEvent.commitment}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Price</td>
|
|
<td>${ethers.formatUnits(gasPrice, 'gwei')} gwei</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Limit</td>
|
|
<td>${gasLimit}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Transaction Fee</td>
|
|
<td>${Number(ethers.formatEther(txFee)).toFixed(5)} ${nativeCurrency.toUpperCase()} ${txFeeString}</td>
|
|
</tr>
|
|
${withdrawTable}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
|
|
job = {
|
|
netId,
|
|
isWallet,
|
|
explorerUrl,
|
|
TornadoProxy,
|
|
relayerClient,
|
|
contract: instanceAddress,
|
|
proof,
|
|
args,
|
|
};
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function confirmWithdrawal() {
|
|
try {
|
|
showStatus('Withdrawing Deposit', 'Withdrawing Deposit');
|
|
|
|
if (!job.contract) {
|
|
throw new Error('Job not found');
|
|
}
|
|
|
|
const { isWallet, explorerUrl, TornadoProxy, relayerClient, contract, proof, args } = job;
|
|
|
|
if (!isWallet) {
|
|
let relayerStatus;
|
|
|
|
await relayerClient.tornadoWithdraw(
|
|
{
|
|
contract,
|
|
proof,
|
|
args,
|
|
},
|
|
(resp) => {
|
|
const { id, status, txHash, confirmations } = resp;
|
|
|
|
if (relayerStatus !== status) {
|
|
if (typeof status === 'undefined' || status === 'SENT') {
|
|
showStatus(
|
|
'Job sent',
|
|
`Relayer has sent withdrawal job ${id} <a href="${explorerUrl}/tx/${txHash}" target="_blank" rel="noreferrer nofollow">${txHash}</a>`
|
|
);
|
|
|
|
} else if (status === 'MINED') {
|
|
showStatus(
|
|
'Job mined',
|
|
`Withdrawal tx <a href="${explorerUrl}/tx/${txHash}" target="_blank" rel="noreferrer nofollow">${txHash}</a> has been mined (confirmations: ${confirmations})`
|
|
);
|
|
|
|
} else if (status === 'CONFIRMED') {
|
|
showStatus(
|
|
'Job completed',
|
|
`Withdrawal tx <a href="${explorerUrl}/tx/${txHash}" target="_blank" rel="noreferrer nofollow">${txHash}</a> has been confirmed (confirmations: ${confirmations})`,
|
|
'success',
|
|
);
|
|
|
|
}
|
|
|
|
relayerStatus = status;
|
|
}
|
|
}
|
|
);
|
|
|
|
} else {
|
|
const { hash } = await TornadoProxy.withdraw(contract, proof, ...args);
|
|
|
|
showStatus(
|
|
'TX Sent',
|
|
`Sent withdrawal tx <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
}
|
|
|
|
} catch (err) {
|
|
showStatus('Withdrawal Failed', `Error while processing withdrawal: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
|
|
job = {};
|
|
}
|
|
|
|
function checkNote(noteString) {
|
|
const parsedNote = Tornado.parseNote(noteString);
|
|
|
|
if (!parsedNote) {
|
|
return;
|
|
}
|
|
|
|
const { netId, currency, amount } = parsedNote;
|
|
|
|
if (!Tornado.enabledChains.includes(netId)) {
|
|
return;
|
|
}
|
|
|
|
const { networkName, tokens } = Tornado.getConfig(netId);
|
|
|
|
const instanceAddress = tokens[currency]?.instanceAddress?.[amount];
|
|
|
|
if (!instanceAddress) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
...parsedNote,
|
|
networkName,
|
|
};
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function compliance() {
|
|
if (!checkNote($('#compliance-note').val())) {
|
|
errorMsg('Invalid note format, make sure your deposit note is correct');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
const deposit = await Tornado.Deposit.parseNote($('#compliance-note').val());
|
|
|
|
const {
|
|
netId,
|
|
currency,
|
|
amount,
|
|
commitmentHex,
|
|
nullifierHex,
|
|
} = deposit;
|
|
|
|
showStatus('Getting Relayers', 'Getting Relayer Clients');
|
|
|
|
const { depositEvents, withdrawalEvents } = await getEvents({ netId, currency, amount, optionalTree: true });
|
|
|
|
const depositEvent = depositEvents.find(({ commitment }) => commitment === commitmentHex);
|
|
const withdrawalEvent = withdrawalEvents.find(({ nullifierHash }) => nullifierHash === nullifierHex);
|
|
|
|
if (!depositEvent) {
|
|
showStatus(
|
|
'Deposit not found',
|
|
'Can not find a deposit for your note, try again in a few minutes if you have recently made the deposit',
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
prepareCompliance(deposit, depositEvent, withdrawalEvent);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from Compliance', `Error while loading compliance: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function withdraw() {
|
|
if (!checkNote($('#withdraw-note').val())) {
|
|
errorMsg('Invalid note format, make sure your deposit note is correct');
|
|
return;
|
|
}
|
|
|
|
if (!getWithdrawRecipient()) {
|
|
errorMsg('Please provide valid recipient address');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
Tornado.initGroth16();
|
|
|
|
const deposit = await Tornado.Deposit.parseNote($('#withdraw-note').val());
|
|
|
|
const {
|
|
netId,
|
|
currency,
|
|
amount,
|
|
commitmentHex,
|
|
nullifierHex,
|
|
} = deposit;
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Getting Relayers', 'Getting Relayer Clients');
|
|
|
|
const relayerClient = await getRelayerClient(netId);
|
|
|
|
const { depositEvents, withdrawalEvents, tree } = await getEvents({ netId, currency, amount });
|
|
|
|
const depositEvent = depositEvents.find(({ commitment }) => commitment === commitmentHex);
|
|
const withdrawalEvent = withdrawalEvents.find(({ nullifierHash }) => nullifierHash === nullifierHex);
|
|
|
|
if (!depositEvent) {
|
|
showStatus('Deposit not found', 'Can not find a deposit for your note, try again in a few minutes if you have recently made the deposit', 'error');
|
|
return;
|
|
}
|
|
|
|
if (withdrawalEvent) {
|
|
showStatus('Note has been spent', 'Note has already been spent, check the withdrawal info on compliance tab', 'error');
|
|
return;
|
|
}
|
|
|
|
await createProof(deposit, depositEvent, tree, relayerClient);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from Withdrawal', `Error while processing withdrawal: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function bulkDeposit() {
|
|
try {
|
|
job = {};
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Preparing Bulk Deposit', 'Preparing Bulk Deposit');
|
|
|
|
const netId = parseInt($('#bulk-deposit-network').val());
|
|
|
|
const encryptNote = $('#bulk-onchain').is(':checked');
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
explorerUrl,
|
|
currencyName,
|
|
nativeCurrency,
|
|
routerContract,
|
|
multicallContract,
|
|
} = config;
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
|
|
const TornadoProxy = TornadoContracts.TornadoRouter__factory.connect(routerContract, provider);
|
|
|
|
const tornadoFeeOracle = new Tornado.TornadoFeeOracle(provider);
|
|
|
|
const noteAccount = new Tornado.NoteAccount({
|
|
recoveryKey: encryptKey,
|
|
});
|
|
|
|
const gasPrice = await tornadoFeeOracle.gasPrice();
|
|
|
|
const inputTexts = $('#bulk-deposit').val().match(/[^\r\n]+/g)?.filter((r) => r);
|
|
|
|
const deposits = (await Promise.all(inputTexts.map(async (text) => {
|
|
try {
|
|
text = text.replace(/ /g,'');
|
|
|
|
// Parse Note
|
|
if (text.includes('tornado-')) {
|
|
const deposit = await Tornado.Deposit.parseNote(text);
|
|
|
|
if (deposit.netId !== netId || deposit.currency !== nativeCurrency) {
|
|
return;
|
|
}
|
|
|
|
deposit.instanceAddress = config.tokens[deposit.currency]?.instanceAddress?.[deposit.amount];
|
|
|
|
if (!deposit.instanceAddress) {
|
|
return;
|
|
}
|
|
|
|
if (encryptNote && encryptKey) {
|
|
deposit.encryptedNote = noteAccount.encryptNote({
|
|
address: deposit.instanceAddress,
|
|
noteHex: deposit.noteHex,
|
|
});
|
|
}
|
|
|
|
return deposit;
|
|
|
|
// Parse Invoice
|
|
} else {
|
|
const deposit = new Tornado.Invoice(text);
|
|
|
|
if (deposit.netId !== netId || deposit.currency !== nativeCurrency) {
|
|
return;
|
|
}
|
|
|
|
deposit.instanceAddress = config.tokens[deposit.currency]?.instanceAddress?.[deposit.amount];
|
|
|
|
if (!deposit.instanceAddress) {
|
|
return;
|
|
}
|
|
|
|
return deposit;
|
|
}
|
|
} catch {
|
|
return;
|
|
}
|
|
}))).filter(r => r);
|
|
|
|
if (!deposits.length) {
|
|
showStatus(
|
|
'No valid notes / invoices found',
|
|
'No valid notes / invoices found, make sure to check your input again',
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const calls = deposits.map(({ instanceAddress, amount, commitmentHex, encryptedNote }) => {
|
|
return {
|
|
target: TornadoProxy.target,
|
|
allowFailure: false,
|
|
callData: TornadoProxy.interface.encodeFunctionData(
|
|
'deposit',
|
|
[instanceAddress, commitmentHex, encryptedNote || '0x']
|
|
),
|
|
value: ethers.parseEther(amount),
|
|
};
|
|
});
|
|
|
|
const tx = {
|
|
from: signer.address,
|
|
to: Multicall.target,
|
|
chainId: (await provider.getNetwork()).chainId,
|
|
value: calls.reduce((acc, c) => c.value + acc, 0n),
|
|
data: (await Multicall.aggregate3Value.populateTransaction(calls)).data,
|
|
};
|
|
|
|
tx.gasLimit = (await provider.estimateGas(tx)) * 11n / 10n;
|
|
|
|
const txFee = gasPrice * tx.gasLimit;
|
|
const txFeePercent = `( ${((Number(txFee) / Number(tx.value)) * 100).toFixed(5)}% )`;
|
|
|
|
showSendCoins(
|
|
`Bulk Deposit ${ethers.formatEther(tx.value)} ${currencyName}?`,
|
|
`Confirm Depositing ${ethers.formatEther(tx.value)} ${currencyName} to <a href="${explorerUrl}/address/${tx.to}" target="_blank" rel="noreferrer nofollow">${tx.to}</a> `
|
|
+ `(Should also spend ${ethers.formatEther(txFee)} ${currencyName} for tx fee).`,
|
|
`
|
|
<h5 class="mb-3">Notes / Invoices</h5>
|
|
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
${deposits.map(({ note, invoice }) => `
|
|
<tr>
|
|
<td class="table-column">${note || invoice}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h5 class="mb-3">Transaction Request</h5>
|
|
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td>ChainID</td>
|
|
<td>${tx.chainId}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>From (Signer)</td>
|
|
<td><a href="${explorerUrl}/address/${tx.from}" target="_blank" rel="noreferrer nofollow">${tx.from}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>To</td>
|
|
<td><a href="${explorerUrl}/address/${tx.to}" target="_blank" rel="noreferrer nofollow">${tx.to}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Price</td>
|
|
<td>${ethers.formatUnits(gasPrice, 'gwei')} gwei</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Limit</td>
|
|
<td>${tx.gasLimit}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Transaction Fee</td>
|
|
<td>${ethers.formatEther(txFee)} ${currencyName} ${txFeePercent}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Value</td>
|
|
<td>${ethers.formatEther(tx.value)} ${currencyName}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Input Data</td>
|
|
<td class="table-column">${tx.data}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
|
|
job = {
|
|
explorerUrl,
|
|
tx,
|
|
signer,
|
|
};
|
|
|
|
} catch (err) {
|
|
showStatus('Error while preparing bulk deposit', `Error while preparing bulk deposit: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TORN Governance
|
|
*/
|
|
let loadingProposals = false;
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function navVoting() {
|
|
if (!allProposals.length) {
|
|
loadProposals();
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function loadProposals() {
|
|
if (loadingProposals) {
|
|
return;
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
loadingProposals = true;
|
|
|
|
try {
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
governanceContract,
|
|
aggregatorContract,
|
|
reverseRecordsContract,
|
|
constants: { GOVERNANCE_BLOCK },
|
|
} = config;
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const governanceService = new Tornado.DBGovernanceService({
|
|
netId,
|
|
provider,
|
|
Governance: TornadoContracts.Governance__factory.connect(governanceContract, provider),
|
|
Aggregator: TornadoContracts.Aggregator__factory.connect(aggregatorContract, provider),
|
|
ReverseRecords: TornadoContracts.ReverseRecords__factory.connect(reverseRecordsContract, provider),
|
|
deployedBlock: GOVERNANCE_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
governanceService.zipDigest = getEventHash(governanceService.getInstanceName());
|
|
|
|
const proposals = await governanceService.getAllProposals();
|
|
|
|
allProposals.push(...proposals);
|
|
|
|
const proposalTableColumn = proposals.reverse().map(
|
|
({ id, title, startTime, endTime, quorum, forVotes, againstVotes, state }) => {
|
|
return {
|
|
id,
|
|
title,
|
|
startTime: moment.unix(startTime).format('DD/MM/YYYY'),
|
|
endTime: moment.unix(endTime).format('DD/MM/YYYY'),
|
|
quorum,
|
|
forVotes: Tornado.numberFormatter(ethers.formatEther(forVotes)) + ' TORN',
|
|
againstVotes: Tornado.numberFormatter(ethers.formatEther(againstVotes)) + ' TORN',
|
|
state,
|
|
view: id,
|
|
};
|
|
}
|
|
);
|
|
|
|
proposalTable.clear();
|
|
proposalTable.rows.add(proposalTableColumn);
|
|
proposalTable.draw();
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load governance proposals: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
|
|
loadingProposals = false;
|
|
}, 500);
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function viewProposal(id) {
|
|
id = Number(id);
|
|
|
|
const proposal = allProposals.find(p => p.id === id);
|
|
|
|
if (!proposal) {
|
|
errorMsg(`Proposal ${id} not found`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading proposal', `Loading ${id} proposal`);
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
explorerUrl,
|
|
governanceContract,
|
|
aggregatorContract,
|
|
reverseRecordsContract,
|
|
constants: { GOVERNANCE_BLOCK },
|
|
} = config;
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const governanceService = new Tornado.DBGovernanceService({
|
|
netId,
|
|
provider,
|
|
Governance: TornadoContracts.Governance__factory.connect(governanceContract, provider),
|
|
Aggregator: TornadoContracts.Aggregator__factory.connect(aggregatorContract, provider),
|
|
ReverseRecords: TornadoContracts.ReverseRecords__factory.connect(reverseRecordsContract, provider),
|
|
deployedBlock: GOVERNANCE_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
const votes = await governanceService.getVotes(id);
|
|
|
|
const votersSet = new Set();
|
|
const uniqueVotes = votes
|
|
.reverse()
|
|
.filter((v) => {
|
|
if (!votersSet.has(v.voter)) {
|
|
votersSet.add(v.voter);
|
|
return true;
|
|
}
|
|
return false;
|
|
})
|
|
.sort((a, b) => Number(b.votes) - Number(a.votes));
|
|
|
|
const {
|
|
title,
|
|
description,
|
|
proposer,
|
|
proposerName,
|
|
target,
|
|
transactionHash,
|
|
startTime,
|
|
endTime,
|
|
forVotes,
|
|
againstVotes,
|
|
quorum,
|
|
state,
|
|
extended,
|
|
executed,
|
|
} = proposal;
|
|
|
|
const allVotes = forVotes + againstVotes;
|
|
|
|
showProposal(
|
|
`Proposal ${id}`,
|
|
id,
|
|
`
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td>Title</td>
|
|
<td>${title}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Description</td>
|
|
<td>${description}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Proposer</td>
|
|
<td><a href="${explorerUrl}/address/${proposer}" target="_blank" rel="noreferrer nofollow">${proposerName || proposer}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Proposal Address</td>
|
|
<td><a href="${explorerUrl}/address/${target}#code" target="_blank" rel="noreferrer nofollow">${target}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Proposal TX</td>
|
|
<td><a href="${explorerUrl}/address/${transactionHash}#code" target="_blank" rel="noreferrer nofollow">${transactionHash}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Start Time</td>
|
|
<td>${String(moment.unix(startTime).toDate())}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>End Time</td>
|
|
<td>${String(moment.unix(endTime).toDate())}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>For</td>
|
|
<td>${Tornado.numberFormatter(ethers.formatEther(forVotes))} TORN</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Against</td>
|
|
<td>${Tornado.numberFormatter(ethers.formatEther(againstVotes))} TORN</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Quorum</td>
|
|
<td>${quorum}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>State</td>
|
|
<td>${state}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Extended</td>
|
|
<td>${extended}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Executed</td>
|
|
<td>${executed}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h5 class="mb-3">Votes</h5>
|
|
|
|
<table id="proposal-votes" class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Support</th>
|
|
<th>Votes</th>
|
|
<th>Voter</th>
|
|
<th>Message</th>
|
|
<th>Delegate</th>
|
|
<th>Percent</th>
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
|
|
<h5 class="mb-3">Vote</h5>
|
|
|
|
<select id="vote-select" class="form-control form-select mb-3">
|
|
<option value="for">For</option>
|
|
<option value="against">Against</option>
|
|
</select>
|
|
|
|
<h5 class="mb-3">Contact (optional)</h5>
|
|
|
|
<input id="vote-contact" type="text" class="form-control mb-3" placeholder="Contact to show for other voters (optional)">
|
|
|
|
<h5 class="mb-3">Comment (optional)</h5>
|
|
|
|
<textarea id="vote-comment" type="text" class="form-control" rows="2" placeholder="Type your vote comment here (optional)"></textarea>
|
|
`
|
|
);
|
|
|
|
const proposalVotes = $('#proposal-votes').DataTable({
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: 'support' },
|
|
{ data: 'votes' },
|
|
{ data: 'voter' },
|
|
{ data: 'message' },
|
|
{ data: 'delegate' },
|
|
{ data: 'percent' },
|
|
],
|
|
columnDefs: [
|
|
{ targets: 5, type: 'percent' },
|
|
],
|
|
order: [[5, 'desc']],
|
|
});
|
|
|
|
proposalVotes.clear();
|
|
proposalVotes.rows.add(
|
|
uniqueVotes.map(({ support, votes, voter, voterName, contact, message, from, fromName }) => {
|
|
return {
|
|
support: `<span class="badge ${support ? 'bg-primary' : 'bg-danger'}">${support ? 'For' : 'Against'}</span>`,
|
|
votes: Tornado.numberFormatter(ethers.formatEther(votes)) + ' TORN',
|
|
voter: `<a href="${explorerUrl}/address/${voter}" target="_blank" rel="noreferrer nofollow">${contact || voterName || voter.slice(0, 10)}</a>`,
|
|
message,
|
|
delegate: voter !== from ? fromName || from.slice(0, 20) : '',
|
|
percent: ((Number(votes) / Number(allVotes)) * 100).toFixed(2) + '%',
|
|
};
|
|
})
|
|
);
|
|
proposalVotes.draw();
|
|
|
|
} catch (err) {
|
|
showStatus('Error from viewProposal', `Error while loading proposal: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
async function getDelegates(address) {
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
governanceContract,
|
|
aggregatorContract,
|
|
reverseRecordsContract,
|
|
constants: { GOVERNANCE_BLOCK },
|
|
} = config;
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const governanceService = new Tornado.DBGovernanceService({
|
|
netId,
|
|
provider,
|
|
Governance: TornadoContracts.Governance__factory.connect(governanceContract, provider),
|
|
Aggregator: TornadoContracts.Aggregator__factory.connect(aggregatorContract, provider),
|
|
ReverseRecords: TornadoContracts.ReverseRecords__factory.connect(reverseRecordsContract, provider),
|
|
deployedBlock: GOVERNANCE_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
return await governanceService.getDelegatedBalance(address);
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function vote(id) {
|
|
id = Number(id);
|
|
|
|
const supportStr = $('#vote-select').find(':selected').text();
|
|
const support = $('#vote-select').find(':selected').val() === 'for';
|
|
const contact = $('#vote-contact').val();
|
|
const comment = $('#vote-comment').val();
|
|
|
|
const proposal = allProposals.find(p => p.id === id);
|
|
|
|
try {
|
|
if (proposal.state !== 'Active') {
|
|
showStatus('Can not vote', `Voting is not active on proposal ${id}`, 'error');
|
|
return;
|
|
}
|
|
|
|
showStatus('Voting Proposal', `Casting ${supportStr} of propoal ${id}`);
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { explorerUrl, governanceContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Governance = TornadoContracts.Governance__factory.connect(governanceContract, provider);
|
|
|
|
const [lockedBalance, { uniq, balances, balance }] = await Promise.all([
|
|
Governance.lockedBalance(signer.address),
|
|
getDelegates(signer.address),
|
|
]);
|
|
|
|
if (!lockedBalance) {
|
|
showStatus('Can not vote', `Can not vote when the locked TORN balance is zero on ${signer.address}`, 'error');
|
|
return;
|
|
}
|
|
|
|
const uniqWithBalances = uniq.filter((_, index) => balances[index]);
|
|
|
|
let data = Governance.interface.encodeFunctionData('castDelegatedVote', [uniqWithBalances, id, support]);
|
|
|
|
if (contact || comment) {
|
|
const value = JSON.stringify([contact, comment]);
|
|
const tail = abiCoder.encode(['string'], [value]);
|
|
data = ethers.concat([data, tail]);
|
|
}
|
|
|
|
const { hash } = await signer.sendTransaction({
|
|
to: Governance.target,
|
|
data,
|
|
});
|
|
|
|
showStatus(
|
|
'Voted Proposal',
|
|
`Casted ${supportStr} of propoal ${id} (TORN Balance: ${ethers.formatEther(lockedBalance)} TORN, Delegated: ${ethers.formatEther(balance)} TORN) <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from vote', `Error while casting vote for proposal ${id}: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function createProposal() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
const title = $('#create-proposal-title').val();
|
|
const description = $('#create-proposal-description').val();
|
|
const proposalAddress = ethers.getAddress($('#create-proposal-address').val());
|
|
|
|
showStatus('Creating proposal', `Creating ${title} proposal...`);
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { explorerUrl, governanceContract, multicallContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Governance = TornadoContracts.Governance__factory.connect(governanceContract, provider);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
|
|
const [lockedBalance, thresold] = await Tornado.multicall(
|
|
Multicall,
|
|
[
|
|
{
|
|
contract: Governance,
|
|
name: 'lockedBalance',
|
|
params: [signer.address]
|
|
},
|
|
{
|
|
contract: Governance,
|
|
name: 'PROPOSAL_THRESHOLD'
|
|
},
|
|
]
|
|
);
|
|
|
|
if (lockedBalance < thresold) {
|
|
showStatus(
|
|
'Insufficient balance',
|
|
`Insufficient locked TORN balance to create proposal, wants ${ethers.formatEther(thresold)} TORN have ${ethers.formatEther(lockedBalance)} TORN`,
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const { hash } = await Governance.connect(signer).propose(proposalAddress, JSON.stringify({ title, description }));
|
|
|
|
showStatus('Proposal submitted', `Proposal submitted on tx <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>, refresh proposal list after few minutes`, 'success');
|
|
|
|
} catch (err) {
|
|
showStatus('Error from createProposal', `Error while creating proposal: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function lock() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading account', 'Loading TORN account');
|
|
|
|
const lockAmountStr = $('#lock-amount').val();
|
|
|
|
const lockAmount = ethers.parseEther(lockAmountStr);
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { explorerUrl, tornContract, governanceContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const TORN = TornadoContracts.TORN__factory.connect(tornContract, provider);
|
|
|
|
const Governance = TornadoContracts.Governance__factory.connect(governanceContract, provider);
|
|
|
|
const tornBalance = await TORN.balanceOf(signer.address);
|
|
|
|
if (tornBalance < lockAmount) {
|
|
showStatus(
|
|
'Insufficient balance',
|
|
`Insufficient TORN balance, wants ${lockAmountStr} TORN have ${ethers.formatEther(tornBalance)} TORN`,
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
showStatus('Signing TORN Spending', `Allow ${lockAmountStr} TORN spending from Wallet to lock them`);
|
|
|
|
// 30 minutes deadline
|
|
const deadline = Math.floor(Date.now() / 1000) + 1800;
|
|
|
|
const { v, r, s } = await Tornado.getPermitSignature({
|
|
Token: TORN,
|
|
signer,
|
|
spender: Governance.target,
|
|
value: lockAmount,
|
|
deadline,
|
|
});
|
|
|
|
showStatus('Locking TORN', 'Locking TORN to governance contract');
|
|
|
|
const { hash } = await Governance.connect(signer).lock(signer.address, lockAmount, deadline, v, r, s);
|
|
|
|
showStatus(
|
|
'Locked TORN',
|
|
`Locked ${lockAmountStr} TORN <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from locking TORN', `Error while locking TORN: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function unlock() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading account', 'Loading TORN account');
|
|
|
|
const unlockAmountStr = $('#unlock-amount').val();
|
|
|
|
const unlockAmount = ethers.parseEther(unlockAmountStr);
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { explorerUrl, governanceContract, multicallContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Governance = TornadoContracts.Governance__factory.connect(governanceContract, provider);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
|
|
const [lockedBalance, canWithdrawAfter] = await Tornado.multicall(
|
|
Multicall,
|
|
[
|
|
{
|
|
contract: Governance,
|
|
name: 'lockedBalance',
|
|
params: [signer.address],
|
|
},
|
|
{
|
|
contract: Governance,
|
|
name: 'canWithdrawAfter',
|
|
params: [signer.address],
|
|
}
|
|
]
|
|
);
|
|
|
|
if (lockedBalance < unlockAmount) {
|
|
showStatus(
|
|
'Insufficient balance',
|
|
`Insufficient locked TORN balance, wants ${unlockAmountStr} have ${ethers.formatEther(lockedBalance)} Locked TORN`,
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (Math.floor(Date.now() / 1000) <= canWithdrawAfter) {
|
|
showStatus('TORN is locked', `Your TORN is locked until ${moment.unix(canWithdrawAfter).toDate()}`, 'error');
|
|
return;
|
|
}
|
|
|
|
showStatus('Unlocking TORN', `Unlocking ${unlockAmountStr} TORN`);
|
|
|
|
const { hash } = await Governance.connect(signer).unlock(unlockAmount);
|
|
|
|
showStatus(
|
|
'Unlocked TORN',
|
|
`Unlocked ${unlockAmountStr} TORN <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from unlocking TORN', `Error while unlocking TORN: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function claim() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading account', 'Loading TORN account');
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { explorerUrl, stakingRewardsContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const StakingRewards = TornadoContracts.TornadoStakingRewards__factory.connect(stakingRewardsContract, provider);
|
|
|
|
const estimatedRewards = await StakingRewards.checkReward(signer.address);
|
|
|
|
showStatus('Claiming TORN', `Claiming ${ethers.formatEther(estimatedRewards)} TORN Rewards`);
|
|
|
|
const { hash } = await StakingRewards.connect(signer).getReward();
|
|
|
|
showStatus(
|
|
'Claimed TORN',
|
|
`Claimed ${ethers.formatEther(estimatedRewards)} TORN Rewards, check it on the explorer <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from claiming TORN', `Error while claiming TORN: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function maxLockAmount() {
|
|
try {
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { tornContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const TORN = TornadoContracts.TORN__factory.connect(tornContract, provider);
|
|
|
|
const tornBalance = ethers.formatEther(await TORN.balanceOf(signer.address));
|
|
|
|
$('#lock-amount').val(tornBalance);
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load TORN balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function maxUnlockAmount() {
|
|
try {
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { governanceContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Governance = TornadoContracts.Governance__factory.connect(governanceContract, provider);
|
|
|
|
const lockedTorn = ethers.formatEther(await Governance.lockedBalance(signer.address));
|
|
|
|
$('#unlock-amount').val(lockedTorn);
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load TORN balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function refreshLocked() {
|
|
try {
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const {
|
|
tornContract,
|
|
governanceContract,
|
|
stakingRewardsContract,
|
|
multicallContract,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const TORN = TornadoContracts.TORN__factory.connect(tornContract, provider);
|
|
|
|
const Governance = TornadoContracts.Governance__factory.connect(governanceContract, provider);
|
|
|
|
const StakingRewards = TornadoContracts.TornadoStakingRewards__factory.connect(stakingRewardsContract, provider);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
|
|
const [{ balance: delegatedBalance }, [tornBalance, lockedBalance, rewardBalance]] = await Promise.all([
|
|
getDelegates(signer.address),
|
|
Tornado.multicall(
|
|
Multicall,
|
|
[
|
|
{
|
|
contract: TORN,
|
|
name: 'balanceOf',
|
|
params: [signer.address],
|
|
},
|
|
{
|
|
contract: Governance,
|
|
name: 'lockedBalance',
|
|
params: [signer.address],
|
|
},
|
|
{
|
|
contract: StakingRewards,
|
|
name: 'checkReward',
|
|
params: [signer.address],
|
|
},
|
|
]
|
|
),
|
|
]);
|
|
|
|
$('#torn-balance').text(Number(ethers.formatEther(tornBalance)).toFixed(3));
|
|
$('#torn-locked').text(Number(ethers.formatEther(lockedBalance)).toFixed(3));
|
|
$('#torn-reward').text(Number(ethers.formatEther(rewardBalance)).toFixed(3));
|
|
$('#torn-delegated').text(Number(ethers.formatEther(delegatedBalance)).toFixed(3));
|
|
|
|
notifyMsg('Refreshed TORN Balances');
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load TORN balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function delegate() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading account', 'Loading TORN account');
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { explorerUrl, governanceContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Delegation = TornadoContracts.Delegation__factory.connect(governanceContract, provider);
|
|
|
|
let delegateAddress;
|
|
|
|
try {
|
|
const delegateTo = $('#delegate-address').val();
|
|
|
|
delegateAddress = delegateTo.endsWith('.eth') ? await provider._getAddress(delegateTo) : ethers.getAddress(delegateTo);
|
|
|
|
} catch (err) {
|
|
showStatus('Invalid delegate address', `Invalid delegate address: ${err.message}`, 'error');
|
|
return;
|
|
}
|
|
|
|
showStatus('Delegating Account', `Delegating Account to ${delegateAddress}`);
|
|
|
|
const { hash } = await Delegation.connect(signer).delegate(delegateAddress);
|
|
|
|
showStatus(
|
|
'Delegated Address',
|
|
`Delegated Address to ${delegateAddress} <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from setting delegation', `Error while setting delegation: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function undelegate() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading account', 'Loading TORN account');
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { explorerUrl, governanceContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Delegation = TornadoContracts.Delegation__factory.connect(governanceContract, provider);
|
|
|
|
const delegateAddress = await Delegation.delegatedTo(signer.address);
|
|
|
|
showStatus('Removing Delegation Account', `Removing Delegation Account from ${delegateAddress}`);
|
|
|
|
const { hash } = await Delegation.connect(signer).undelegate();
|
|
|
|
showStatus(
|
|
'Undelegated',
|
|
`Undelegated from ${delegateAddress} <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from setting delegation', `Error while setting delegation: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function getCurrentDelegate() {
|
|
try {
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const { governanceContract, reverseRecordsContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Delegation = TornadoContracts.Delegation__factory.connect(governanceContract, provider);
|
|
|
|
const ReverseRecords = TornadoContracts.ReverseRecords__factory.connect(reverseRecordsContract, provider);
|
|
|
|
const delegateAddress = await Delegation.delegatedTo(signer.address);
|
|
|
|
if (delegateAddress !== ethers.ZeroAddress) {
|
|
const delegateName = (await ReverseRecords.getNames([delegateAddress]))[0];
|
|
|
|
$('#delegate-address').val(delegateName || delegateAddress);
|
|
}
|
|
} catch (err) {
|
|
errorMsg(`Failed to load delegation address: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function getCurrentDelegatee() {
|
|
try {
|
|
$('#delegatee-table').empty();
|
|
$('#delegatee-table').append(`
|
|
<p>Loading TORN delegatees please wait</p>
|
|
|
|
<img id="send-loading" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png" class="loader status">
|
|
`);
|
|
|
|
let delegateeAddress;
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const {
|
|
explorerUrl,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
try {
|
|
const delegatee = $('#delegatee-address').val();
|
|
|
|
if (!delegatee) {
|
|
delegateeAddress = (await getSigner(netId)).address;
|
|
|
|
$('#delegatee-address').val(delegateeAddress);
|
|
|
|
} else {
|
|
delegateeAddress = delegatee.endsWith('.eth') ? await provider._getAddress(delegatee) : ethers.getAddress(delegatee);
|
|
}
|
|
} catch (err) {
|
|
errorMsg(`Invalid Delegatee Address: ${err.message}`);
|
|
return;
|
|
}
|
|
|
|
const { uniq, uniqNames, balances } = await getDelegates(delegateeAddress);
|
|
|
|
const uniqSorted = uniq
|
|
.map((u, i) => {
|
|
return {
|
|
address: u,
|
|
name: uniqNames[u],
|
|
balance: balances[i],
|
|
};
|
|
})
|
|
.sort((a, b) => Number(b.balance) - Number(a.balance));
|
|
|
|
const uniqTable = uniqSorted.map(({ address, name, balance }) => {
|
|
return `
|
|
<tr>
|
|
<td><a href="${explorerUrl}/address/${address}" target="_blank" rel="noreferrer nofollow">${name || address}</a></td>
|
|
<td>${ethers.formatEther(balance)} TORN</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
$('#delegatee-table').empty();
|
|
$('#delegatee-table').append(`
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Locked Balance</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${uniqTable}
|
|
</tbody>
|
|
`);
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load delegation list: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
let loadingApy = false;
|
|
let loadedApy = false;
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function navApy() {
|
|
if (!loadedApy) {
|
|
loadApy();
|
|
}
|
|
}
|
|
|
|
function loadApy() {
|
|
if (loadingApy) {
|
|
return;
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
loadingApy = true;
|
|
|
|
try {
|
|
$('#apy-content').empty();
|
|
$('#apy-content').append(`
|
|
<p class="mt-2">Loading recent TORN APY</p>
|
|
|
|
<img id="send-loading" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png" class="loader status">
|
|
`);
|
|
|
|
const netId = GOVERNANCE_NETWORK;
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
explorerUrl,
|
|
governanceContract,
|
|
registryContract,
|
|
tornContract,
|
|
constants: { REGISTRY_BLOCK },
|
|
} = config;
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const Governance = TornadoContracts.GovernanceVaultUpgrade__factory.connect(governanceContract, provider);
|
|
|
|
const TORN = TornadoContracts.TORN__factory.connect(tornContract, provider);
|
|
|
|
const revenueService = new Tornado.DBRevenueService({
|
|
netId,
|
|
provider,
|
|
RelayerRegistry: TornadoContracts.RelayerRegistry__factory.connect(registryContract, provider),
|
|
deployedBlock: REGISTRY_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
revenueService.zipDigest = getEventHash(revenueService.getInstanceName());
|
|
|
|
const [stakedBalance, { events }] = await Promise.all([
|
|
Governance.userVault().then(vault => TORN.balanceOf(vault)),
|
|
revenueService.updateEvents(),
|
|
]);
|
|
|
|
// Recent 30 days events
|
|
const recentEvents = events
|
|
.filter(({ timestamp }) => timestamp > (Math.floor(Date.now() / 1000) - 30 * 86400))
|
|
.reverse();
|
|
|
|
const weeklyRevenue = recentEvents
|
|
.filter(({ timestamp }) => timestamp > (Math.floor(Date.now() / 1000) - 7 * 86400))
|
|
.reduce((acc, { amountBurned }) => acc + BigInt(amountBurned), 0n);
|
|
|
|
const monthlyRevenue = recentEvents.reduce((acc, { amountBurned }) => acc + BigInt(amountBurned), 0n);
|
|
|
|
const weeklyYield = (Number(weeklyRevenue * 52n) / Number(stakedBalance)) * 100;
|
|
|
|
const monthlyYield = (Number(monthlyRevenue * 12n) / Number(stakedBalance)) * 100;
|
|
|
|
const revenueTableColumn = recentEvents.map(({ blockNumber, transactionHash, relayerAddress, amountBurned, instanceAddress, relayerFee, timestamp }) => {
|
|
const { amount, currency } = Tornado.getInstanceByAddress(config, instanceAddress) || {};
|
|
|
|
const { decimals } = config.tokens[currency];
|
|
|
|
return {
|
|
blockNumber,
|
|
instance: `${amount} ${currency.toUpperCase()}`,
|
|
relayer: `<a href="${explorerUrl}/address/${relayerAddress}" target="_blank" rel="noreferrer nofollow">`
|
|
+ allRelayers.find(r => r.relayerAddress === relayerAddress)?.ensName || relayerAddress.substring(0, 10) + '</a>',
|
|
relayerFees: `${Number(ethers.formatUnits(relayerFee, decimals)).toFixed(5)} ${currency.toUpperCase()}`,
|
|
amountBurned: `${Number(ethers.formatEther(amountBurned)).toFixed(3)} TORN`,
|
|
timestamp: `<a href="${explorerUrl}/tx/${transactionHash}" target="_blank" rel="noreferrer nofollow">${moment.unix(timestamp).fromNow()}</a>`,
|
|
};
|
|
});
|
|
|
|
$('#apy-content').empty();
|
|
$('#apy-content').append(`
|
|
<p class="mt-2">Yield (from 7d income): ${weeklyYield.toFixed(3)}% (${Number(ethers.formatEther(weeklyRevenue)).toFixed(3)} TORN)</p>
|
|
|
|
<p>Yield (from 30d income): ${monthlyYield.toFixed(3)}% (${Number(ethers.formatEther(monthlyRevenue)).toFixed(3)} TORN)</p>
|
|
|
|
<p>Total Staked: ${Number(ethers.formatEther(stakedBalance)).toFixed(3)} TORN</p>
|
|
|
|
<table id="revenue-table" class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Block</th>
|
|
<th>Instance</th>
|
|
<th>Relayer</th>
|
|
<th>Relayer Fees</th>
|
|
<th>TORN Distributed</th>
|
|
<th>Timestamp</th>
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
`);
|
|
|
|
const revenueTable = $('#revenue-table').DataTable({
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: 'blockNumber' },
|
|
{ data: 'instance' },
|
|
{ data: 'relayer' },
|
|
{ data: 'relayerFees' },
|
|
{ data: 'amountBurned' },
|
|
{ data: 'timestamp' },
|
|
],
|
|
order: [[0, 'desc']],
|
|
});
|
|
|
|
revenueTable.clear();
|
|
revenueTable.rows.add(revenueTableColumn);
|
|
revenueTable.draw();
|
|
|
|
loadedApy = true;
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load apy: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
|
|
loadingApy = false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Note Encryption
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function recover() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading Encryption Keys', 'Loading Encryption Keys');
|
|
|
|
const netId = parseInt($('#recover-network').val());
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
networkName,
|
|
echoContract,
|
|
routerContract,
|
|
constants: { NOTE_ACCOUNT_BLOCK, ENCRYPTED_NOTES_BLOCK }
|
|
} = config;
|
|
|
|
const echoService = new Tornado.DBEchoService({
|
|
netId,
|
|
provider,
|
|
Echoer: TornadoContracts.Echoer__factory.connect(echoContract, provider),
|
|
deployedBlock: NOTE_ACCOUNT_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
echoService.zipDigest = getEventHash(echoService.getInstanceName());
|
|
|
|
const encryptedNotesService = new Tornado.DBEncryptedNotesService({
|
|
netId,
|
|
provider,
|
|
Router: TornadoContracts.TornadoRouter__factory.connect(routerContract, provider),
|
|
deployedBlock: ENCRYPTED_NOTES_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
encryptedNotesService.zipDigest = getEventHash(encryptedNotesService.getInstanceName());
|
|
|
|
const accounts = [];
|
|
|
|
// Recover encryption keys possibly encrypted by a signer
|
|
try {
|
|
showStatus('Loading Encryption Keys', 'Connecting Wallet to recover encryption keys (can cancel if you want to load from manually entered encryption key)');
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
showStatus('Loading Encryption Keys', 'Fetching All Encryption Keys from on-chain backup');
|
|
|
|
const echoEvents = (await echoService.updateEvents()).events;
|
|
|
|
showStatus('Loading Encryption Keys', 'Decrypting All Encryption Keys from on-chain backup (approve decryption request on wallet if have any)');
|
|
|
|
accounts.push(...(await Tornado.NoteAccount.decryptSignerNoteAccounts(signer, echoEvents)));
|
|
|
|
// eslint-disable-next-line no-empty
|
|
} catch {}
|
|
|
|
if (encryptKey && !accounts.find(({ recoveryKey }) => recoveryKey === encryptKey)) {
|
|
accounts.push(new Tornado.NoteAccount({ recoveryKey: encryptKey }));
|
|
}
|
|
|
|
if (!accounts.length) {
|
|
showStatus('No Encryption Keys Found', 'No encryption keys found from provided key & connected wallet, make sure you create one or try again at few minutes', 'error');
|
|
return;
|
|
}
|
|
|
|
const encryptedNoteEvents = (await encryptedNotesService.updateEvents()).events;
|
|
|
|
const decryptedNotes = (accounts.map(noteAccount => noteAccount.decryptNotes(encryptedNoteEvents)).flat()).map(({ address, noteHex }) => {
|
|
const { amount, currency } = Tornado.getInstanceByAddress(config, address) || {};
|
|
|
|
if (amount) {
|
|
return `tornado-${currency}-${amount}-${netId}-${noteHex}`;
|
|
}
|
|
|
|
return noteHex;
|
|
});
|
|
|
|
if (!encryptKey) {
|
|
encryptKey = accounts.slice(-1)[0].recoveryKey;
|
|
|
|
$('#encrypt-key').val(encryptKey);
|
|
|
|
notifyMsg(`Will use ${encryptKey} for note encryption (not saved, will be cleared once refresh)`);
|
|
}
|
|
|
|
showConfirmation(
|
|
`${networkName} Encrypt Keys & Encrypted Notes`,
|
|
'',
|
|
`
|
|
<h5 class="mb-3">Encryption Keys</h5>
|
|
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Keys</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${accounts.map(({ recoveryKey }) => `
|
|
<tr>
|
|
<td class="table-column">${recoveryKey}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<h5 class="mb-3">Deposit Notes</h5>
|
|
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Notes</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${decryptedNotes.map((note) => `
|
|
<tr>
|
|
<td class="table-column">${note}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error from loading encrypted notes and accounts', `Error while loading encrypted notes and accounts: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function createKey() {
|
|
try {
|
|
job = {};
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading Encryption Keys', 'Checking if you have any existing encryption keys');
|
|
|
|
const accounts = [];
|
|
|
|
const netId = parseInt($('#create-network').val());
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tovarishClient = getTovarishClient(netId);
|
|
|
|
const { staticRoot } = getStaticRoot();
|
|
|
|
const idb = await getIndexedDB(netId);
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
networkName,
|
|
explorerUrl,
|
|
echoContract,
|
|
constants: { NOTE_ACCOUNT_BLOCK }
|
|
} = config;
|
|
|
|
const Echoer = TornadoContracts.Echoer__factory.connect(echoContract, provider);
|
|
|
|
const echoService = new Tornado.DBEchoService({
|
|
netId,
|
|
provider,
|
|
Echoer,
|
|
deployedBlock: NOTE_ACCOUNT_BLOCK,
|
|
tovarishClient,
|
|
staticUrl: `${staticRoot}/events`,
|
|
idb,
|
|
});
|
|
|
|
echoService.zipDigest = getEventHash(echoService.getInstanceName());
|
|
|
|
// Recover encryption keys possibly encrypted by a signer
|
|
showStatus('Loading account', 'Connecting Wallet to recover encryption accounts (can cancel if you want to load from manually entered encryption key)');
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
showStatus('Loading Encryption Keys', 'Fetching All Encryption Keys from on-chain backup');
|
|
|
|
const echoEvents = (await echoService.updateEvents()).events;
|
|
|
|
showStatus('Loading Encryption Keys', 'Decrypting All Encryption Keys from on-chain backup (approve decryption request on wallet if have any)');
|
|
|
|
accounts.push(...(await Tornado.NoteAccount.decryptSignerNoteAccounts(signer, echoEvents)));
|
|
|
|
if (accounts.length) {
|
|
if (!encryptKey) {
|
|
encryptKey = accounts.slice(-1)[0].recoveryKey;
|
|
|
|
$('#encrypt-key').val(encryptKey);
|
|
|
|
notifyMsg(`Will use ${encryptKey} for note encryption (not saved, will be cleared once refresh)`);
|
|
}
|
|
|
|
showConfirmation(
|
|
`${networkName} Encrypt Keys`,
|
|
'',
|
|
`
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Keys</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${accounts.map(({ recoveryKey }) => `
|
|
<tr>
|
|
<td class="table-column">${recoveryKey}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
return;
|
|
}
|
|
|
|
showStatus('Creating New Encryption Key', 'Creating New Encryption Key, approve Encryption Request from your wallet');
|
|
|
|
const publicKey = await Tornado.NoteAccount.getSignerPublicKey(signer);
|
|
|
|
const newAccount = new Tornado.NoteAccount({});
|
|
|
|
const { data } = newAccount.getEncryptedAccount(publicKey);
|
|
|
|
$('#backup-button').attr('download', `backup-note-account-key-0x${newAccount.recoveryKey.slice(0, 8)}.txt`);
|
|
$('#backup-button').attr('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(newAccount.recoveryKey));
|
|
document.getElementById('backup-button').click();
|
|
|
|
showAccount(
|
|
`${networkName} New Encryption Key`,
|
|
'',
|
|
`
|
|
<p class="mt-2 mb-2">Encrypted With</p>
|
|
<textarea type="text" class="form-control" rows="1" disabled>${signer.address}</textarea>
|
|
|
|
<p class="mt-2 mb-2">Key</p>
|
|
<textarea type="text" class="form-control" rows="1" disabled>${newAccount.recoveryKey}</textarea>
|
|
|
|
<p class="mt-2 mb-2">To (Echoer contract)</p>
|
|
<textarea type="text" class="form-control" rows="1" disabled>${Echoer.target}</textarea>
|
|
|
|
<p class="mt-2 mb-2">Encrypted Data</p>
|
|
<textarea type="text" class="form-control" rows="3" disabled>${data}</textarea>
|
|
`
|
|
);
|
|
|
|
job = {
|
|
explorerUrl,
|
|
Echoer: Echoer.connect(signer),
|
|
data,
|
|
};
|
|
|
|
} catch (err) {
|
|
showStatus('Error while creating Encryption Keys', `Error while creating Encryption Keys: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function confirmKeyBackup() {
|
|
try {
|
|
showStatus('Saving Encryption Key on-chain', 'Saving Encryption Key on-chain');
|
|
|
|
const { explorerUrl, Echoer, data } = job;
|
|
|
|
const { hash } = await Echoer.echo(data);
|
|
|
|
showStatus('Saved Encryption Key', `Saved Encrypted Encryption Key on-chain <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`, 'success');
|
|
|
|
} catch (err) {
|
|
showStatus('Error while saving Encryption Keys', `Error while saving Encryption Keys: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
|
|
job = {};
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function backupEncrypted() {
|
|
try {
|
|
job = {};
|
|
|
|
if (!encryptKey) {
|
|
errorMsg('Encryption Key not found, make sure you load it from Recover Keys & Notes tab or save it manually from settings');
|
|
return;
|
|
}
|
|
|
|
const inputText = $('#backup-note').val();
|
|
|
|
if (!inputText) {
|
|
errorMsg('Invalid text input, check your text input again');
|
|
return;
|
|
}
|
|
|
|
const inputTexts = inputText.match(/[^\r\n]+/g)?.filter((r) => r);
|
|
|
|
if (!inputTexts.length) {
|
|
errorMsg('Invalid text input, check your text input again');
|
|
return;
|
|
}
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Encrypting Text', 'Encrypting Text Input and preparing on-chain backup');
|
|
|
|
const netId = parseInt($('#backup-network').val());
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
networkName,
|
|
explorerUrl,
|
|
routerContract,
|
|
} = config;
|
|
|
|
const Router = TornadoContracts.TornadoRouter__factory.connect(routerContract, provider);
|
|
|
|
const noteAccount = new Tornado.NoteAccount({ recoveryKey: encryptKey });
|
|
|
|
const data = inputTexts.map(text => {
|
|
const isNote = checkNote(text);
|
|
|
|
const address = isNote
|
|
? (config.tokens[isNote.currency]?.instanceAddress?.[isNote.amount] || ethers.ZeroAddress)
|
|
: ethers.ZeroAddress;
|
|
|
|
const noteHex = isNote ? isNote.noteHex : text;
|
|
|
|
return noteAccount.encryptNote({ address, noteHex });
|
|
});
|
|
|
|
showEncryptBackup(
|
|
`${networkName} Encrypted Text`,
|
|
'',
|
|
`
|
|
<p class="mt-2 mb-2">Encrypt Key</p>
|
|
<textarea type="text" class="form-control" rows="1" disabled>${encryptKey}</textarea>
|
|
|
|
<p class="mt-2 mb-2">To (Router contract)</p>
|
|
<textarea type="text" class="form-control" rows="1" disabled>${Router.target}</textarea>
|
|
|
|
<p class="mt-2 mb-2">Input Texts</p>
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
${inputTexts.map((text) => `
|
|
<tr>
|
|
<td class="table-column">${text}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
|
|
<p class="mt-2 mb-2">Encrypted Texts</p>
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
${data.map((encryptedText) => `
|
|
<tr>
|
|
<td class="table-column">${encryptedText}</td>
|
|
</tr>
|
|
`).join('')}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
|
|
job = {
|
|
explorerUrl,
|
|
Router: Router.connect(signer),
|
|
data,
|
|
};
|
|
|
|
} catch (err) {
|
|
showStatus('Error while encrypting text', `Error while encrypting text: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function confirmEncryptBackup() {
|
|
try {
|
|
showStatus('Saving Encrypted Text on-chain', 'Saving Encryption Text on-chain');
|
|
|
|
const { explorerUrl, Router, data } = job;
|
|
|
|
const { hash } = await Router.backupNotes(data);
|
|
|
|
showStatus('Saved Encryption Text', `Saved Encrypted Text on-chain <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`, 'success');
|
|
|
|
} catch (err) {
|
|
showStatus('Error while saving Encryption Text', `Error while saving Encryption Text: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
|
|
job = {};
|
|
}
|
|
|
|
/**
|
|
* Relayers
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function getRelayers() {
|
|
try {
|
|
const netId = parseInt($('#relayer-list-network').val());
|
|
|
|
const config = Tornado.getConfig(netId);
|
|
|
|
const {
|
|
networkName,
|
|
explorerUrl,
|
|
} = config;
|
|
|
|
const relayerClient = new Tornado.RelayerClient({
|
|
netId,
|
|
config,
|
|
});
|
|
|
|
const tovarishClient = new Tornado.TovarishClient({
|
|
netId,
|
|
config,
|
|
});
|
|
|
|
$('#relayer-list').empty();
|
|
$('#relayer-list').append(`
|
|
<p>Loading ${networkName} relayers please wait</p>
|
|
|
|
<img id="send-loading" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png" class="loader status">
|
|
`);
|
|
|
|
const { relayers } = await updateRelayers(true);
|
|
|
|
const tovarishRelayers = relayers.filter((r) => r.tovarishHost && r.tovarishNetworks?.length);
|
|
const nonTovarishRelayers = relayers.filter(r => !(r.tovarishHost && r.tovarishNetworks?.length));
|
|
|
|
const [{ validRelayers }, { validRelayers: validTovarishRelayers }] = await Promise.all([
|
|
relayerClient.getValidRelayers(nonTovarishRelayers),
|
|
tovarishClient.getValidRelayers(tovarishRelayers),
|
|
]);
|
|
|
|
$('#relayer-list').empty();
|
|
$('#relayer-list').append(`
|
|
<table id="relayer-list-table" class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>URL</th>
|
|
<th>ENS Name</th>
|
|
<th>Stake Balance</th>
|
|
<th>Current Queue</th>
|
|
<th>Service Fee</th>
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
`);
|
|
|
|
const relayerList = $('#relayer-list-table').DataTable({
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: 'id' },
|
|
{ data: 'url' },
|
|
{ data: 'ensName' },
|
|
{ data: 'stakeBalance' },
|
|
{ data: 'currentQueue' },
|
|
{ data: 'serviceFee' },
|
|
],
|
|
});
|
|
|
|
relayerList.clear();
|
|
relayerList.rows.add(
|
|
[...validTovarishRelayers, ...validRelayers].map(({
|
|
url,
|
|
ensName,
|
|
stakeBalance,
|
|
relayerAddress,
|
|
currentQueue,
|
|
tornadoServiceFee,
|
|
}, index) => {
|
|
return {
|
|
id: index,
|
|
url: `<a href="${url}" target="_blank" rel="noreferrer nofollow">${url}</a>`,
|
|
ensName: `<a href="${explorerUrl}/address/${relayerAddress}" target="_blank" rel="noreferrer nofollow">${ensName}</a>`,
|
|
stakeBalance: `${Number(stakeBalance || 0).toFixed(3)} TORN`,
|
|
currentQueue,
|
|
serviceFee: `${tornadoServiceFee}%`,
|
|
};
|
|
})
|
|
);
|
|
relayerList.draw();
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to fetch relayers: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function relayerStatus() {
|
|
try {
|
|
const ensName = $('#relayer-status-name').val();
|
|
|
|
$('#relayer-status').empty();
|
|
$('#relayer-status').append(`
|
|
<p>Loading ${ensName} relayer please wait</p>
|
|
|
|
<img id="send-loading" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png" class="loader status">
|
|
`);
|
|
|
|
const { relayers } = await updateRelayers(true);
|
|
|
|
const selectedRelayer = relayers.find(r => r.ensName === ensName);
|
|
|
|
if (!selectedRelayer) {
|
|
$('#relayer-status').empty();
|
|
$('#relayer-status').append(`
|
|
<p>Selected relayer ${ensName} relayer is not registered</p>
|
|
|
|
<img id="send-loading" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/failed.png" class="status">
|
|
`);
|
|
return;
|
|
}
|
|
|
|
// Status of registered relayer
|
|
const registeredStatus = [];
|
|
|
|
// Status of working relayer
|
|
const relayerStatus = [];
|
|
|
|
if (selectedRelayer.tovarishHost && selectedRelayer.tovarishNetworks.length) {
|
|
|
|
registeredStatus.push(...(selectedRelayer.tovarishNetworks.map(netId => ({ netId, hostname: selectedRelayer.tovarishHost }))));
|
|
|
|
relayerStatus.push(...((await new Tornado.TovarishClient({}).getTovarishRelayers([selectedRelayer])).validRelayers));
|
|
|
|
} else {
|
|
|
|
registeredStatus.push(...(Object.entries(selectedRelayer.hostnames).map(([netId, hostname]) => ({ netId: Number(netId), hostname }))));
|
|
|
|
const allStatus = (await Promise.all(Object.keys(selectedRelayer.hostnames).map(async (netId) => {
|
|
netId = Number(netId);
|
|
|
|
const relayerClient = new Tornado.RelayerClient({
|
|
netId,
|
|
config: Tornado.getConfig(netId),
|
|
});
|
|
|
|
return (await relayerClient.getValidRelayers([selectedRelayer])).validRelayers[0];
|
|
}))).filter(r => r);
|
|
|
|
relayerStatus.push(...allStatus);
|
|
|
|
}
|
|
|
|
$('#relayer-status').empty();
|
|
$('#relayer-status').append(`
|
|
<p>Staked TORN (to relayer)</p>
|
|
|
|
<input type="text" class="form-control mb-3" value="${Number(selectedRelayer.stakeBalance || 0).toFixed(3)} TORN" disabled></input>
|
|
|
|
<p>Relayers</p>
|
|
|
|
<table class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Network</th>
|
|
<th>URL</th>
|
|
<th>Current Queue</th>
|
|
<th>Service Fee</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${registeredStatus.map(({ netId, hostname }) => {
|
|
|
|
const { networkName } = Tornado.getConfig(netId);
|
|
|
|
const { url, currentQueue, tornadoServiceFee } = relayerStatus.find(r => r.netId === netId) || {};
|
|
|
|
const relayerUrl = url || `https://${hostname}`;
|
|
|
|
return `
|
|
<tr>
|
|
<td>${networkName}</td>
|
|
<td><a href="${relayerUrl}" target="_blank" rel="noreferrer nofollow">${relayerUrl}</a></td>
|
|
<td>${currentQueue || 0}</td>
|
|
<td>${tornadoServiceFee || 0}%</td>
|
|
<td>${url ? '✅' : '❌'}</td>
|
|
</tr>
|
|
`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>
|
|
`);
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to fetch relayers: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function registerRelayer() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading account', 'Loading TORN account');
|
|
|
|
const ensName = $('#relayer-register-name').val();
|
|
|
|
const netId = RELAYER_NETWORK;
|
|
|
|
const {
|
|
explorerUrl,
|
|
tornContract,
|
|
registryContract,
|
|
multicallContract,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const ENSUtils = new Tornado.ENSUtils(provider);
|
|
|
|
await ENSUtils.getContracts();
|
|
|
|
const TORN = TornadoContracts.TORN__factory.connect(tornContract, provider);
|
|
|
|
const RelayerRegistry = TornadoContracts.RelayerRegistry__factory.connect(registryContract, provider);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
|
|
const [workerAddress, tornBalance, minStakeAmount, ensOwner, ensWrappedOwner] = await Tornado.multicall(
|
|
Multicall,
|
|
[
|
|
{
|
|
contract: RelayerRegistry,
|
|
name: 'workers',
|
|
params: [signer.address],
|
|
},
|
|
{
|
|
contract: TORN,
|
|
name: 'balanceOf',
|
|
params: [signer.address],
|
|
},
|
|
{
|
|
contract: RelayerRegistry,
|
|
name: 'minStakeAmount',
|
|
},
|
|
{
|
|
contract: ENSUtils.ENSRegistry,
|
|
name: 'owner',
|
|
params: [ethers.namehash(ensName)],
|
|
},
|
|
{
|
|
contract: ENSUtils.ENSNameWrapper,
|
|
name: 'ownerOf',
|
|
params: [BigInt(ethers.namehash(ensName))]
|
|
}
|
|
]
|
|
);
|
|
|
|
if (workerAddress !== ethers.ZeroAddress) {
|
|
showStatus('Relayer already registered', 'You have already registered the relayer!', 'error');
|
|
return;
|
|
}
|
|
|
|
if (
|
|
ensOwner !== ENSUtils.ENSNameWrapper.target && ensOwner !== signer.address
|
|
|| ensOwner === ENSUtils.ENSNameWrapper.target && ensWrappedOwner !== signer.address
|
|
) {
|
|
showStatus('Not ENS name owner', `You do not own ${ensName}, can not register the selected ens name`, 'error');
|
|
return;
|
|
}
|
|
|
|
if (tornBalance < minStakeAmount) {
|
|
showStatus(
|
|
'Insufficient balance',
|
|
`Insufficient TORN balance, wants ${ethers.formatEther(minStakeAmount)} TORN have ${ethers.formatEther(tornBalance)} TORN`,
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (ensOwner === ENSUtils.ENSNameWrapper.target) {
|
|
showStatus('Unwrapping ENS Name', `Unwrapping your ENS name ${ensName} to register`);
|
|
|
|
const resp = await ENSUtils.unwrap(signer, ensName);
|
|
|
|
showStatus(
|
|
'Unwrapping ENS Name',
|
|
`Waiting for unwrapping tx <a href="${explorerUrl}/tx/${resp.hash}" target="_blank" rel="noreferrer nofollow">${resp.hash}</a> to confirm.`
|
|
);
|
|
|
|
await resp.wait();
|
|
}
|
|
|
|
showStatus('Signing TORN Spending', `Allow ${ethers.formatEther(minStakeAmount)} TORN spending from Wallet to pay registeration fee`);
|
|
|
|
// 30 minutes deadline
|
|
const deadline = Math.floor(Date.now() / 1000) + 1800;
|
|
|
|
const { v, r, s } = await Tornado.getPermitSignature({
|
|
Token: TORN,
|
|
signer,
|
|
spender: RelayerRegistry.target,
|
|
value: minStakeAmount,
|
|
deadline,
|
|
});
|
|
|
|
showStatus('Registering Relayer', `Registering ${ensName} relayer to registry contract`);
|
|
|
|
const { hash } = await RelayerRegistry.connect(signer).registerPermit(ensName, minStakeAmount, [], signer.address, deadline, v, r, s);
|
|
|
|
showStatus(
|
|
'Registered Relayer',
|
|
`Registered Relayer ${ensName} <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error while registering relayer', `Error while registering relayer: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function registerHostname() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Registering Hostname', 'Registering Hostname');
|
|
|
|
const ensName = $('#register-hostname-name').val();
|
|
|
|
const { relayerEnsSubdomain } = Tornado.getConfig(parseInt($('#register-hostname-network').val()));
|
|
|
|
const { hostname } = new URL($('#register-hostname').val());
|
|
|
|
if (hostname.includes('http')) {
|
|
showStatus('Invalid Hostname', 'Invalid hostname, hostname should not be a URL! (Remove https:// if you have)', 'error');
|
|
return;
|
|
}
|
|
|
|
const netId = RELAYER_NETWORK;
|
|
|
|
const {
|
|
explorerUrl,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const ENSUtils = new Tornado.ENSUtils(provider);
|
|
|
|
await ENSUtils.getContracts();
|
|
|
|
const subdomain = `${relayerEnsSubdomain}.${ensName}`;
|
|
|
|
const [owner, subdomainUrl] = await Promise.all([
|
|
ENSUtils.getOwner(ensName),
|
|
ENSUtils.getText(subdomain, 'url'),
|
|
]);
|
|
|
|
if (owner !== signer.address) {
|
|
showStatus('Not ENS name owner', `You do not own ${ensName}, can not register the selected ens name`, 'error');
|
|
return;
|
|
}
|
|
|
|
// Create subdomain first if not exists
|
|
if (subdomainUrl === null) {
|
|
showStatus('Creating ENS Subdomain', `Creating ENS subdomain record ${subdomain} for relayer`);
|
|
|
|
const resp = await ENSUtils.setSubnodeRecord(subdomain);
|
|
|
|
showStatus('Creating ENS Subdomain', `Waiting for subdomain record ${subdomain} to be created <a href="${explorerUrl}/tx/${resp.hash}" target="_blank" rel="noreferrer nofollow">${resp.hash}</a>`);
|
|
|
|
await resp.wait();
|
|
}
|
|
|
|
showStatus('Setting ENS Subdomain record', `Setting ENS Subdomain ${subdomain} url text record to ${hostname}`);
|
|
|
|
const { hash } = await ENSUtils.setText(signer, subdomain, 'url', hostname);
|
|
|
|
showStatus(
|
|
'Set ENS Subdomain record',
|
|
`Set ENS Subdomain ${subdomain} url record to ${hostname} <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error while registering hostname', `Error while registering hostname: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function relayerStake() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Loading TORN account', 'Loading TORN account');
|
|
|
|
const ensName = $('#relayer-stake-name').val();
|
|
|
|
const stakeAmount = ethers.parseEther($('#relayer-stake-amount').val());
|
|
|
|
const selectedRelayer = allRelayers.find(r => r.ensName === ensName);
|
|
|
|
if (!selectedRelayer) {
|
|
showStatus('Relayer not found', `Could not find registered relayer ${ensName}, reload page if you have just registered relayer`, 'error');
|
|
return;
|
|
}
|
|
|
|
const netId = RELAYER_NETWORK;
|
|
|
|
const {
|
|
explorerUrl,
|
|
tornContract,
|
|
registryContract,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const TORN = TornadoContracts.TORN__factory.connect(tornContract, provider);
|
|
|
|
const RelayerRegistry = TornadoContracts.RelayerRegistry__factory.connect(registryContract, provider);
|
|
|
|
const tornBalance = await TORN.balanceOf(signer.address);
|
|
|
|
if (tornBalance < stakeAmount) {
|
|
showStatus(
|
|
'Insufficient balance',
|
|
`Insufficient TORN balance, wants ${ethers.formatEther(stakeAmount)} TORN have ${ethers.formatEther(tornBalance)} TORN`,
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
showStatus('Signing TORN Spending', `Allow ${ethers.formatEther(stakeAmount)} TORN spending from Wallet to pay registeration fee`);
|
|
|
|
// 30 minutes deadline
|
|
const deadline = Math.floor(Date.now() / 1000) + 1800;
|
|
|
|
const { v, r, s } = await Tornado.getPermitSignature({
|
|
Token: TORN,
|
|
signer,
|
|
spender: RelayerRegistry.target,
|
|
value: stakeAmount,
|
|
deadline,
|
|
});
|
|
|
|
showStatus('Staking TORN to relayer', `Staking ${ethers.formatEther(stakeAmount)} TORN to ${ensName} relayer`);
|
|
|
|
const { hash } = await RelayerRegistry.connect(signer).stakeToRelayerPermit(
|
|
selectedRelayer.relayerAddress,
|
|
stakeAmount,
|
|
signer.address,
|
|
deadline,
|
|
v,
|
|
r,
|
|
s,
|
|
);
|
|
|
|
showStatus(
|
|
'Staked TORN to relayer',
|
|
`Staked ${ethers.formatEther(stakeAmount)} TORN to ${ensName} relayer <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error while staking TORN to relayer', `Error while staking TORN to relayer: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function maxRelayerStakeAmount() {
|
|
try {
|
|
const netId = RELAYER_NETWORK;
|
|
|
|
const { tornContract } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const TORN = TornadoContracts.TORN__factory.connect(tornContract, provider);
|
|
|
|
const tornBalance = ethers.formatEther(await TORN.balanceOf(signer.address));
|
|
|
|
$('#relayer-stake-amount').val(tornBalance);
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load TORN balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wallet
|
|
*/
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function balance() {
|
|
try {
|
|
const netId = parseInt($('#wallet-balance-network').val());
|
|
|
|
const {
|
|
explorerUrl,
|
|
currencyName,
|
|
multicallContract,
|
|
tornContract,
|
|
tokens,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
$('#wallet-balance').empty();
|
|
$('#wallet-balance').append(`
|
|
<p>Loading wallet balances</p>
|
|
|
|
<img id="send-loading" src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/torn2.png" class="loader status">
|
|
`);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
|
|
const tokenAddresses = Object.values(tokens).map(({ tokenAddress }) => tokenAddress).filter((t) => t);
|
|
|
|
if (tornContract) {
|
|
tokenAddresses.push(tornContract);
|
|
}
|
|
|
|
const tokenBalances = await Tornado.getTokenBalances({
|
|
provider,
|
|
Multicall,
|
|
currencyName,
|
|
userAddress: signer.address,
|
|
tokenAddresses,
|
|
});
|
|
|
|
$('#wallet-balance').empty();
|
|
$('#wallet-balance').append(`
|
|
<table id="wallet-balance-table" class="table table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Contract Address</th>
|
|
<th>Balance</th>
|
|
</tr>
|
|
</thead>
|
|
</table>
|
|
`);
|
|
|
|
const walletBalanceList = $('#wallet-balance-table').DataTable({
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: 'name' },
|
|
{ data: 'contractAddress' },
|
|
{ data: 'balance' },
|
|
],
|
|
columnDefs: [
|
|
{ targets: 2, type: 'balance' },
|
|
],
|
|
order: [[2, 'desc']],
|
|
});
|
|
|
|
walletBalanceList.clear();
|
|
walletBalanceList.rows.add(
|
|
tokenBalances.map(({ address, name, symbol, decimals, balance }) => {
|
|
const contractAddress = address === ethers.ZeroAddress
|
|
? ''
|
|
: `<a href="${explorerUrl}/address/${address}" target="_blank" rel="noreferrer nofollow">${address}</a>`;
|
|
|
|
return {
|
|
name: `<img src="https://cdn.jsdelivr.net/npm/tornado-cdn@1.0.7/tokens/${symbol.toLowerCase()}.png" height="24"> ${name} (${symbol})`,
|
|
contractAddress,
|
|
balance: `${ethers.formatUnits(balance, decimals)} ${symbol}`,
|
|
};
|
|
}),
|
|
);
|
|
walletBalanceList.draw();
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load wallet balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function sendCoins() {
|
|
try {
|
|
if (!$('#send-coins-data').val() && !ethers.isAddress($('#send-coins-recipient').val())) {
|
|
errorMsg(`${$('#send-coins-recipient').val()} is not a valid Ethereum address`);
|
|
return;
|
|
}
|
|
|
|
job = {};
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Preparing Send Transaction', 'Preparing Send Transaction');
|
|
|
|
const netId = parseInt($('#send-coins-network').val());
|
|
|
|
const to = ethers.getAddress($('#send-coins-recipient').val() || ethers.ZeroAddress);
|
|
|
|
const value = ethers.parseEther($('#send-coins-amount').val() || '0');
|
|
|
|
const data = $('#send-coins-data').val() || '0x';
|
|
|
|
const {
|
|
explorerUrl,
|
|
currencyName,
|
|
ovmGasPriceOracleContract,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const tornadoFeeOracle = new Tornado.TornadoFeeOracle(
|
|
provider,
|
|
ovmGasPriceOracleContract
|
|
? TornadoContracts.OvmGasPriceOracle__factory.connect(ovmGasPriceOracleContract, provider)
|
|
: undefined,
|
|
);
|
|
|
|
const tx = {
|
|
chainId: netId,
|
|
from: signer.address,
|
|
to,
|
|
value,
|
|
data,
|
|
};
|
|
|
|
const [balance, nonce, feeData, l1Fee] = await Promise.all([
|
|
provider.getBalance(signer.address),
|
|
provider.getTransactionCount(signer.address, 'pending'),
|
|
provider.getFeeData(),
|
|
tornadoFeeOracle.fetchL1OptimismFee({
|
|
...tx,
|
|
from: undefined,
|
|
}),
|
|
]);
|
|
|
|
tx.nonce = nonce;
|
|
|
|
let gasPrice = BigInt(0);
|
|
|
|
if (feeData.maxFeePerGas) {
|
|
gasPrice = feeData.maxFeePerGas + feeData.maxPriorityFeePerGas;
|
|
|
|
tx.type = 2;
|
|
tx.maxFeePerGas = feeData.maxFeePerGas;
|
|
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
|
|
} else {
|
|
gasPrice = feeData.gasPrice;
|
|
|
|
tx.type = 0;
|
|
tx.gasPrice = feeData.gasPrice;
|
|
}
|
|
|
|
if (balance <= value) {
|
|
tx.value = balance - ((gasPrice * 500000n) + l1Fee);
|
|
|
|
const gasLimit = await provider.estimateGas(tx);
|
|
|
|
tx.value = balance - ((gasPrice * (gasLimit !== 21000n ? gasLimit * 12n / 10n : gasLimit)) + l1Fee);
|
|
|
|
}
|
|
|
|
const gasLimit = await provider.estimateGas(tx);
|
|
|
|
tx.gasLimit = gasLimit !== 21000n ? gasLimit * 11n / 10n : gasLimit;
|
|
|
|
const txFee = (gasPrice * tx.gasLimit) + l1Fee;
|
|
|
|
showSendCoins(
|
|
`Send ${currencyName}?`,
|
|
`Confirm Sending ${ethers.formatEther(tx.value)} ${currencyName} to <a href="${explorerUrl}/address/${tx.to}" target="_blank" rel="noreferrer nofollow">${tx.to}</a> `
|
|
+ `(Should also spend ${ethers.formatEther(txFee)} ${currencyName} for tx fee).`,
|
|
`
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td>ChainID</td>
|
|
<td>${tx.chainId}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>From (Signer)</td>
|
|
<td><a href="${explorerUrl}/address/${tx.from}" target="_blank" rel="noreferrer nofollow">${tx.from}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>To</td>
|
|
<td><a href="${explorerUrl}/address/${tx.to}" target="_blank" rel="noreferrer nofollow">${tx.to}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Price</td>
|
|
<td>${ethers.formatUnits(gasPrice, 'gwei')} gwei</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Limit</td>
|
|
<td>${tx.gasLimit}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Transaction Fee</td>
|
|
<td>${ethers.formatEther(txFee)} ${currencyName}</td>
|
|
</tr>
|
|
${l1Fee ? `
|
|
<tr>
|
|
<td>L1 Fee</td>
|
|
<td>${ethers.formatEther(l1Fee)} ${currencyName}</td>
|
|
</tr>
|
|
` : ''}
|
|
<tr>
|
|
<td>Value</td>
|
|
<td>${ethers.formatEther(tx.value)} ${currencyName}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Input Data</td>
|
|
<td class="table-column">${tx.data}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
|
|
job = {
|
|
explorerUrl,
|
|
tx,
|
|
signer,
|
|
};
|
|
|
|
} catch (err) {
|
|
showStatus('Error while sending transaction', `Error while sending transaction: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function confirmSendCoins() {
|
|
try {
|
|
showStatus('Sending transaction', 'Sending transaction');
|
|
|
|
const { explorerUrl, tx, signer } = job;
|
|
|
|
const { hash } = await signer.sendTransaction(tx);
|
|
|
|
showStatus(
|
|
'Sent transaction',
|
|
`Sent transaction <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`,
|
|
'success'
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error while sending transaction', `Error while sending transaction: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
|
|
job = {};
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function getUnsigned() {
|
|
try {
|
|
const tx = {
|
|
...job.tx,
|
|
from: undefined,
|
|
};
|
|
|
|
const serialized = ethers.Transaction.from(tx).unsignedSerialized;
|
|
|
|
showConfirmation(
|
|
'Unsigned Transaction',
|
|
'',
|
|
`<textarea type="text" class="form-control" rows="3" disabled>${serialized}</textarea>`
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error while generating unsigned transaction', `Error while generating unsigned transaction: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
|
|
job = {};
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function maxSendAmount() {
|
|
try {
|
|
const netId = parseInt($('#send-coins-network').val());
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const balance = await provider.getBalance(signer.address);
|
|
|
|
$('#send-coins-amount').val(ethers.formatEther(balance));
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load wallet balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function sendTokens() {
|
|
try {
|
|
if (!ethers.isAddress($('#send-tokens-recipient').val())) {
|
|
errorMsg(`${$('#send-tokens-recipient').val()} is not a valid Ethereum address`);
|
|
return;
|
|
}
|
|
|
|
if (!ethers.isAddress($('#send-tokens-address').val())) {
|
|
errorMsg(`${$('#send-tokens-address').val()} is not a valid token address`);
|
|
return;
|
|
}
|
|
|
|
job = {};
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Preparing Send Transaction', 'Preparing Send Transaction');
|
|
|
|
const netId = parseInt($('#send-tokens-network').val());
|
|
|
|
const to = ethers.getAddress($('#send-tokens-recipient').val());
|
|
|
|
const {
|
|
explorerUrl,
|
|
currencyName,
|
|
multicallContract,
|
|
ovmGasPriceOracleContract,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const tornadoFeeOracle = new Tornado.TornadoFeeOracle(
|
|
provider,
|
|
ovmGasPriceOracleContract
|
|
? TornadoContracts.OvmGasPriceOracle__factory.connect(ovmGasPriceOracleContract, provider)
|
|
: undefined,
|
|
);
|
|
|
|
const Token = TornadoContracts.ERC20__factory.connect($('#send-tokens-address').val(), provider);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(multicallContract, provider);
|
|
|
|
const tx = {
|
|
chainId: netId,
|
|
from: signer.address,
|
|
to: Token.target,
|
|
value: BigInt(0),
|
|
};
|
|
|
|
const [nonce, feeData, [balance, decimals, symbol]] = await Promise.all([
|
|
provider.getTransactionCount(signer.address, 'pending'),
|
|
provider.getFeeData(),
|
|
Tornado.multicall(
|
|
Multicall,
|
|
[
|
|
{
|
|
contract: Token,
|
|
name: 'balanceOf',
|
|
params: [signer.address],
|
|
},
|
|
{
|
|
contract: Token,
|
|
name: 'decimals',
|
|
},
|
|
{
|
|
contract: Token,
|
|
name: 'symbol',
|
|
},
|
|
]
|
|
),
|
|
]);
|
|
|
|
const value = ethers.parseUnits($('#send-tokens-amount').val() || '0', decimals);
|
|
|
|
tx.data = Token.interface.encodeFunctionData('transfer', [to, value]);
|
|
|
|
tx.nonce = nonce;
|
|
|
|
let gasPrice = BigInt(0);
|
|
|
|
if (feeData.maxFeePerGas) {
|
|
gasPrice = feeData.maxFeePerGas + feeData.maxPriorityFeePerGas;
|
|
|
|
tx.type = 2;
|
|
tx.maxFeePerGas = feeData.maxFeePerGas;
|
|
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
|
|
} else {
|
|
gasPrice = feeData.gasPrice;
|
|
|
|
tx.type = 0;
|
|
tx.gasPrice = feeData.gasPrice;
|
|
}
|
|
|
|
if (balance < value) {
|
|
showStatus(
|
|
'Insufficient balance',
|
|
`Insufficient Token balance, wants ${ethers.formatUnits(value, decimals)} ${symbol} have ${ethers.formatEther(value, decimals)} ${symbol}`,
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
const [gasLimit, l1Fee] = await Promise.all([
|
|
provider.estimateGas(tx),
|
|
tornadoFeeOracle.fetchL1OptimismFee({
|
|
...tx,
|
|
from: undefined,
|
|
gasLimit: BigInt(300000),
|
|
}),
|
|
]);
|
|
|
|
tx.gasLimit = gasLimit * 11n / 10n;
|
|
|
|
const txFee = gasPrice * tx.gasLimit + l1Fee;
|
|
|
|
showSendCoins(
|
|
`Send ${symbol}?`,
|
|
`Confirm Sending Token ${ethers.formatUnits(value, decimals)} ${symbol} to <a href="${explorerUrl}/address/${to}" target="_blank" rel="noreferrer nofollow">${to}</a> `
|
|
+ `(Should also spend ${ethers.formatEther(txFee)} ${currencyName} for tx fee).`,
|
|
`
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td>ChainID</td>
|
|
<td>${tx.chainId}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>From (Signer)</td>
|
|
<td><a href="${explorerUrl}/address/${tx.from}" target="_blank" rel="noreferrer nofollow">${tx.from}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>To</td>
|
|
<td><a href="${explorerUrl}/address/${to}" target="_blank" rel="noreferrer nofollow">${to}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Token Contract</td>
|
|
<td><a href="${explorerUrl}/address/${tx.to}" target="_blank" rel="noreferrer nofollow">${tx.to}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Price</td>
|
|
<td>${ethers.formatUnits(gasPrice, 'gwei')} gwei</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Limit</td>
|
|
<td>${tx.gasLimit}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Transaction Fee</td>
|
|
<td>${ethers.formatEther(txFee)} ${currencyName}</td>
|
|
</tr>
|
|
${l1Fee ? `
|
|
<tr>
|
|
<td>L1 Fee</td>
|
|
<td>${ethers.formatEther(l1Fee)} ${currencyName}</td>
|
|
</tr>
|
|
` : ''}
|
|
<tr>
|
|
<td>Value</td>
|
|
<td>${ethers.formatUnits(value, decimals)} ${symbol} ( 0 ${currencyName} )</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Input Data</td>
|
|
<td class="table-column">${tx.data}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
|
|
job = {
|
|
explorerUrl,
|
|
tx,
|
|
signer,
|
|
};
|
|
|
|
} catch (err) {
|
|
showStatus('Error while sending tokens', `Error while sending tokens: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function maxTokenAmount() {
|
|
try {
|
|
if (!ethers.isAddress($('#send-tokens-address').val())) {
|
|
return;
|
|
}
|
|
|
|
const netId = parseInt($('#send-tokens-network').val());
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const Token = TornadoContracts.ERC20__factory.connect(ethers.getAddress($('#send-tokens-address').val()), provider);
|
|
|
|
const Multicall = TornadoContracts.Multicall__factory.connect(Tornado.getConfig(netId).multicallContract, provider);
|
|
|
|
const [balance, decimals] = await Tornado.multicall(
|
|
Multicall,
|
|
[
|
|
{
|
|
contract: Token,
|
|
name: 'balanceOf',
|
|
params: [signer.address],
|
|
},
|
|
{
|
|
contract: Token,
|
|
name: 'decimals',
|
|
}
|
|
]
|
|
);
|
|
|
|
$('#send-tokens-amount').val(ethers.formatUnits(balance, decimals));
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load wallet balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function signTransaction() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Signing Transaction', 'Signing Transaction');
|
|
|
|
const deserializedTx = ethers.Transaction.from($('#unsigned-tx').val()).toJSON();
|
|
|
|
const signer = await getSigner(Number(deserializedTx.chainId) || parseInt($('#unsigned-tx-network').val()));
|
|
|
|
const signedTx = await signer.signTransaction(deserializedTx);
|
|
|
|
showConfirmation(
|
|
'Signed Transaction',
|
|
'',
|
|
`
|
|
<p class="mb-2">Transaction Request</p>
|
|
<textarea type="text" class="form-control" rows="14" disabled>${JSON.stringify(deserializedTx, null, 2)}</textarea>
|
|
|
|
<p class="mt-2 mb-2">Signed Transaction</p>
|
|
<textarea type="text" class="form-control" rows="3" disabled>${signedTx}</textarea>
|
|
`
|
|
);
|
|
|
|
|
|
} catch (err) {
|
|
showStatus('Error while signing transaction', `Error while signing transaction: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function broadcastTransaction() {
|
|
try {
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Broadcasting Transaction', 'Broadcasting Transaction');
|
|
|
|
const signedTransaction = ethers.Transaction.from($('#raw-tx').val());
|
|
|
|
const netId = Number(signedTransaction.chainId) || parseInt($('#raw-tx-network').val());
|
|
|
|
const { explorerUrl } = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const { hash } = await provider.broadcastTransaction(signedTransaction.serialized);
|
|
|
|
showConfirmation(
|
|
'Broadcasted Transaction',
|
|
'',
|
|
`Broadcasted transaction <a href="${explorerUrl}/tx/${hash}" target="_blank" rel="noreferrer nofollow">${hash}</a>`
|
|
);
|
|
|
|
} catch (err) {
|
|
showStatus('Error while broadcasting transaction', `Error while broadcasting transaction: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function createWallet() {
|
|
const { mnemonic: { phrase }, address, privateKey } = ethers.HDNodeWallet.createRandom();
|
|
|
|
$('#create-wallet-mnemonic').val(phrase);
|
|
$('#create-wallet-address').val(address);
|
|
$('#create-wallet-private-key').val(privateKey);
|
|
}
|
|
|
|
/**
|
|
* Gas.zip
|
|
*/
|
|
async function getGasZipMinMax(netId) {
|
|
const {
|
|
multicallContract,
|
|
offchainOracleContract,
|
|
stablecoin,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const tokenPriceOracle = new Tornado.TokenPriceOracle(
|
|
provider,
|
|
TornadoContracts.Multicall__factory.connect(multicallContract, provider),
|
|
TornadoContracts.OffchainOracle__factory.connect(offchainOracleContract, provider),
|
|
);
|
|
|
|
const ethUsd = await tokenPriceOracle.fetchEthUSD(stablecoin);
|
|
|
|
return Tornado.gasZipMinMax(ethUsd);
|
|
}
|
|
|
|
let loadingGasZip = false;
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
function displayGasZipMax() {
|
|
if (loadingGasZip) {
|
|
return;
|
|
}
|
|
|
|
setTimeout(async () => {
|
|
try {
|
|
loadingGasZip = true;
|
|
|
|
const netId = parseInt($('#gas-zip-inbound').val());
|
|
|
|
const { currencyName } = Tornado.getConfig(netId);
|
|
|
|
const { min, max } = await getGasZipMinMax(netId);
|
|
|
|
$('#gas-zip-min').text(`${Number(min).toFixed(8)} ${currencyName}`);
|
|
$('#gas-zip-max').text(`${Number(max).toFixed(8)} ${currencyName}`);
|
|
|
|
loadingGasZip = false;
|
|
} catch {
|
|
loadingGasZip = false;
|
|
}
|
|
}, 500);
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function sendGasZip() {
|
|
try {
|
|
job = {};
|
|
|
|
// Toggle Modal
|
|
new bootstrap.Modal('#send', { backdrop: 'static', keyboard: false }).toggle();
|
|
|
|
showStatus('Preparing Gas.zip Deposit', 'Preparing Gas.zip Deposit');
|
|
|
|
const netId = parseInt($('#gas-zip-inbound').val());
|
|
|
|
const recipient = $('#gas-zip-recipient').val()
|
|
? ethers.getAddress($('#gas-zip-recipient').val())
|
|
: undefined;
|
|
|
|
const valueStr = $('#gas-zip-amount').val() || '0';
|
|
|
|
const value = ethers.parseEther(valueStr);
|
|
|
|
const outbounNetIds = [];
|
|
|
|
const outboundChains = Tornado.enabledChains.map(chainId => {
|
|
const isChecked = $(`#${chainId}-gas-zip`).is(':checked');
|
|
|
|
if (isChecked && chainId !== netId) {
|
|
outbounNetIds.push(chainId);
|
|
return Tornado.gasZipID[chainId];
|
|
}
|
|
}).filter(c => c);
|
|
|
|
const inboundAddress = Tornado.gasZipInbounds[netId];
|
|
|
|
if (!outboundChains.length) {
|
|
showStatus('Invalid gas.zip deposit chain', 'Invalid gas.zip deposit chain, must have more than one chain', 'error');
|
|
return;
|
|
}
|
|
|
|
// sepolia
|
|
if (!inboundAddress) {
|
|
showStatus('Chain unsupported for gas.zip', 'Chain unsupported for gas.zip', 'error');
|
|
return;
|
|
}
|
|
|
|
const { min, max } = await getGasZipMinMax(netId);
|
|
|
|
if (min > Number(valueStr)) {
|
|
showStatus('Deposit amount is lower than $1', 'Deposit amount is lower than $1', 'error');
|
|
return;
|
|
}
|
|
|
|
if (max < Number(valueStr)) {
|
|
showStatus('Deposit amount is bigger than $50', 'Deposit amount is bigger than $50', 'error');
|
|
return;
|
|
}
|
|
|
|
const {
|
|
explorerUrl,
|
|
currencyName,
|
|
ovmGasPriceOracleContract,
|
|
} = Tornado.getConfig(netId);
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const tornadoFeeOracle = new Tornado.TornadoFeeOracle(
|
|
provider,
|
|
ovmGasPriceOracleContract
|
|
? TornadoContracts.OvmGasPriceOracle__factory.connect(ovmGasPriceOracleContract, provider)
|
|
: undefined,
|
|
);
|
|
|
|
const tx = {
|
|
chainId: netId,
|
|
from: signer.address,
|
|
to: inboundAddress,
|
|
value,
|
|
data: Tornado.gasZipInput(recipient, outboundChains),
|
|
};
|
|
|
|
const [balance, nonce, feeData, l1Fee] = await Promise.all([
|
|
provider.getBalance(signer.address),
|
|
provider.getTransactionCount(signer.address, 'pending'),
|
|
provider.getFeeData(),
|
|
tornadoFeeOracle.fetchL1OptimismFee({
|
|
...tx,
|
|
from: undefined,
|
|
}),
|
|
]);
|
|
|
|
tx.nonce = nonce;
|
|
|
|
let gasPrice = BigInt(0);
|
|
|
|
if (feeData.maxFeePerGas) {
|
|
gasPrice = feeData.maxFeePerGas + feeData.maxPriorityFeePerGas;
|
|
|
|
tx.type = 2;
|
|
tx.maxFeePerGas = feeData.maxFeePerGas;
|
|
tx.maxPriorityFeePerGas = feeData.maxPriorityFeePerGas;
|
|
} else {
|
|
gasPrice = feeData.gasPrice;
|
|
|
|
tx.type = 0;
|
|
tx.gasPrice = feeData.gasPrice;
|
|
}
|
|
|
|
if (balance <= value) {
|
|
tx.value = balance - ((gasPrice * 200000n) + l1Fee);
|
|
|
|
const gasLimit = await provider.estimateGas(tx);
|
|
|
|
tx.value = balance - ((gasPrice * (gasLimit !== 21000n ? gasLimit * 12n / 10n : gasLimit)) + l1Fee);
|
|
|
|
}
|
|
|
|
const gasLimit = await provider.estimateGas(tx);
|
|
|
|
tx.gasLimit = gasLimit * 11n / 10n;
|
|
|
|
const txFee = (gasPrice * tx.gasLimit) + l1Fee;
|
|
|
|
showSendCoins(
|
|
'Deposit Gas.zip?',
|
|
`Confirm Gas.zip transaction ${ethers.formatEther(tx.value)} ${currencyName} to <a href="${explorerUrl}/address/${recipient || tx.from}" target="_blank" rel="noreferrer nofollow">${recipient || tx.from}</a> `
|
|
+ `(chains: ${outbounNetIds.join(', ')} (Should also spend ${ethers.formatEther(txFee)} ${currencyName} for tx fee).`,
|
|
`
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td>ChainID</td>
|
|
<td>${tx.chainId}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>From (Signer)</td>
|
|
<td><a href="${explorerUrl}/address/${tx.from}" target="_blank" rel="noreferrer nofollow">${tx.from}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>To</td>
|
|
<td><a href="${explorerUrl}/address/${recipient || tx.from}" target="_blank" rel="noreferrer nofollow">${recipient || tx.from}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>GasZip Deposit</td>
|
|
<td><a href="${explorerUrl}/address/${inboundAddress}" target="_blank" rel="noreferrer nofollow">${inboundAddress}</a></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Price</td>
|
|
<td>${ethers.formatUnits(gasPrice, 'gwei')} gwei</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Gas Limit</td>
|
|
<td>${tx.gasLimit}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Transaction Fee</td>
|
|
<td>${ethers.formatEther(txFee)} ${currencyName}</td>
|
|
</tr>
|
|
${l1Fee ? `
|
|
<tr>
|
|
<td>L1 Fee</td>
|
|
<td>${ethers.formatEther(l1Fee)} ${currencyName}</td>
|
|
</tr>
|
|
` : ''}
|
|
<tr>
|
|
<td>Value</td>
|
|
<td>${ethers.formatEther(tx.value)} ${currencyName}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Chains</td>
|
|
<td>${outbounNetIds.join(', ')}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Input Data</td>
|
|
<td class="table-column">${tx.data}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
`
|
|
);
|
|
|
|
job = {
|
|
explorerUrl,
|
|
tx,
|
|
signer,
|
|
};
|
|
|
|
} catch (err) {
|
|
showStatus('Error while preparing gas.zip tx', `Error while preparing gas.zip tx: ${err.message}`, 'error');
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
async function maxGasZipAmount() {
|
|
try {
|
|
const netId = parseInt($('#gas-zip-inbound').val());
|
|
|
|
const provider = await getProvider(netId);
|
|
|
|
const signer = await getSigner(netId);
|
|
|
|
const balance = await provider.getBalance(signer.address);
|
|
|
|
$('#gas-zip-amount').val(ethers.formatEther(balance));
|
|
|
|
} catch (err) {
|
|
errorMsg(`Failed to load wallet balance: ${err.message}`);
|
|
console.log(err);
|
|
}
|
|
}
|
|
|
|
$(document).ready(async function() {
|
|
$('.version').text(VERSION);
|
|
$('.donation').attr('href', `https://etherscan.io/address/${DONATION_ADDRESS}`);
|
|
|
|
if (typeof WebAssembly === 'undefined') {
|
|
errorMsg('Error: Please turn on WebAssembly in your browser settings.<br /> If you are using Tor browser, enable javascript.options.wasm in about:config', true);
|
|
}
|
|
|
|
settingsDB = await Tornado.getIndexedDB();
|
|
|
|
if (!settingsDB.dbExists) {
|
|
errorMsg('Can not use IndexedDB, perfomance of this UI may be degraded', true);
|
|
}
|
|
|
|
if (new URL(window.location.href).protocol === 'https:') {
|
|
alertMsg(
|
|
'You are accessing the UI from the remote environment which allows us to barely audit or secure the environment. Consider using the UI from the local environment cloned from git',
|
|
true,
|
|
);
|
|
}
|
|
|
|
await loadSettings();
|
|
checkIP();
|
|
await getAllRelayers();
|
|
displayNetworks();
|
|
displayCurrency();
|
|
displayAmount();
|
|
displayStatistics();
|
|
|
|
jQuery.extend(jQuery.fn.dataTableExt.oSort, {
|
|
'percent-pre': function ( a ) {
|
|
var x = (a == '-' || a === 'unknown') ? 0 : a.replace( /%/, '' );
|
|
return parseFloat( x );
|
|
},
|
|
|
|
'percent-asc': function ( a, b ) {
|
|
return ((a < b) ? -1 : ((a > b) ? 1 : 0));
|
|
},
|
|
|
|
'percent-desc': function ( a, b ) {
|
|
return ((a < b) ? 1 : ((a > b) ? -1 : 0));
|
|
},
|
|
|
|
'balance-pre': function (a) {
|
|
const x = (!a || a == '-' || a === 'unknown') ? 0 : Number(a.split(' ')[0]);
|
|
return Number(x);
|
|
},
|
|
|
|
'balance-asc': function ( a, b ) {
|
|
return ((a < b) ? -1 : ((a > b) ? 1 : 0));
|
|
},
|
|
|
|
'balance-desc': function ( a, b ) {
|
|
return ((a < b) ? 1 : ((a > b) ? -1 : 0));
|
|
},
|
|
});
|
|
|
|
proposalTable = $('#proposal-table').DataTable({
|
|
autoWidth: false,
|
|
columns: [
|
|
{ data: 'id' },
|
|
{ data: 'title' },
|
|
{ data: 'startTime' },
|
|
{ data: 'endTime' },
|
|
{ data: 'quorum' },
|
|
{ data: 'forVotes' },
|
|
{ data: 'againstVotes' },
|
|
{ data: 'state' },
|
|
{ data: 'view' },
|
|
],
|
|
columnDefs: [
|
|
{
|
|
targets: 1,
|
|
width: '300px',
|
|
},
|
|
{
|
|
targets: 7,
|
|
render: function (state) {
|
|
let badgeType = 'bg-primary';
|
|
|
|
switch (state) {
|
|
case 'AwaitingExecution':
|
|
case 'Active':
|
|
badgeType = 'bg-primary';
|
|
break;
|
|
case 'Executed':
|
|
badgeType = 'bg-purple';
|
|
break;
|
|
case 'Expired':
|
|
badgeType = 'bg-secondary';
|
|
break;
|
|
case 'Defeated':
|
|
badgeType = 'bg-danger';
|
|
break;
|
|
case 'Pending':
|
|
case 'Timelocked':
|
|
badgeType = 'bg-warning';
|
|
break;
|
|
}
|
|
|
|
return `<span class="badge ${badgeType} text-white">${state}</span>`;
|
|
},
|
|
},
|
|
{
|
|
targets: 8,
|
|
render: function (id) {
|
|
return `<button type="button" class="btn btn-primary" onclick="viewProposal('${id}')">View</button>`;
|
|
}
|
|
}
|
|
],
|
|
order: [[0, 'desc']],
|
|
});
|
|
|
|
$('.navbar-brand').click(function (e) {
|
|
e.preventDefault();
|
|
|
|
$('.page').addClass('d-none');
|
|
$('#home').removeClass('d-none');
|
|
|
|
$('.bar-link').removeClass('active');
|
|
$('.bar-link[data-page="home"]').addClass('active');
|
|
});
|
|
|
|
function nav(page) {
|
|
$('.page').addClass('d-none');
|
|
$(`#${page}`).removeClass('d-none');
|
|
|
|
$('.bar-link').removeClass('active');
|
|
$(`.bar-link[data-page="${page}"]`).addClass('active');
|
|
|
|
if (page === 'voting') {
|
|
navVoting();
|
|
}
|
|
|
|
if (page === 'home') {
|
|
window.location.hash = '';
|
|
} else {
|
|
window.location.hash = `#${page}`;
|
|
}
|
|
}
|
|
|
|
function parseNav() {
|
|
const page = window.location.hash.split('#')[1];
|
|
|
|
if (page) {
|
|
nav(page);
|
|
}
|
|
}
|
|
|
|
parseNav();
|
|
|
|
$('.bar-link').click(function (e) {
|
|
e.preventDefault();
|
|
|
|
nav($(this).data('page'));
|
|
});
|
|
|
|
$('.card-link').click(function (e) {
|
|
e.preventDefault();
|
|
|
|
const cardGroup = $(this).data('card-group');
|
|
const cardName = $(this).data('card');
|
|
|
|
$(`.${cardGroup}`).addClass('d-none');
|
|
$(`.${cardName}`).removeClass('d-none');
|
|
|
|
$(`.${cardGroup}-tabs .card-link`).removeClass('active');
|
|
$(this).addClass('active');
|
|
});
|
|
|
|
$('#deposit-network').on('change', function (e) {
|
|
e.preventDefault();
|
|
|
|
displayCurrency();
|
|
displayAmount();
|
|
displayStatistics();
|
|
});
|
|
|
|
$('#deposit-currency').on('change', function (e) {
|
|
e.preventDefault();
|
|
|
|
displayAmount();
|
|
displayStatistics();
|
|
});
|
|
|
|
$('#deposit-amount').on('change', function (e) {
|
|
e.preventDefault();
|
|
|
|
displayStatistics();
|
|
});
|
|
|
|
$('#invoice-note').on('input', function (e) {
|
|
e.preventDefault();
|
|
|
|
try {
|
|
const invoice = $('#invoice-note').val();
|
|
|
|
if (!invoice) {
|
|
return;
|
|
}
|
|
|
|
const { netId, currency, amount } = new Tornado.Invoice(invoice);
|
|
|
|
changeDisplay({ netId, currency, amount });
|
|
} catch {
|
|
return;
|
|
}
|
|
});
|
|
|
|
const parseWithdrawNote = () => {
|
|
const { networkName, netId, currency, amount } = checkNote($('#withdraw-note').val()) || {};
|
|
|
|
if (!networkName) {
|
|
hide('#withdraw-on-note');
|
|
return;
|
|
}
|
|
|
|
$('.note-network').val(networkName);
|
|
$('.note-currency').val(currency.toUpperCase());
|
|
$('.note-amount').val(amount);
|
|
$('.note-relayer').val($('#relayers').find(':selected').text());
|
|
|
|
changeDisplay({ netId, currency, amount });
|
|
|
|
show('#withdraw-on-note');
|
|
};
|
|
|
|
parseWithdrawNote();
|
|
|
|
$('#withdraw-note').on('input', parseWithdrawNote);
|
|
|
|
$('#compliance-note').on('input', function (e) {
|
|
e.preventDefault();
|
|
|
|
const { netId, currency, amount } = checkNote($('#compliance-note').val()) || {};
|
|
|
|
if (!netId) {
|
|
return;
|
|
}
|
|
|
|
changeDisplay({ netId, currency, amount });
|
|
});
|
|
|
|
$('#gas-zip-inbound').on('change', (e) => {
|
|
e.preventDefault();
|
|
|
|
displayGasZipMax();
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |