1679 lines
92 KiB
HTML
1679 lines
92 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<title>Bitcoin Wallet</title>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<link rel="shortcut icon" href="favicon.svg" type="image/x-icon">
|
||
<link rel="stylesheet" href="css/main.min.css">
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Calistoga&family=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
|
||
rel="stylesheet">
|
||
<script src="components.js" defer></script>
|
||
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
||
<script type="text/javascript" src="lib.js"></script>
|
||
<script type="text/javascript" src="btcOperator.js"></script>
|
||
</head>
|
||
|
||
<body class="hidden">
|
||
<sm-notifications id="notification_drawer"></sm-notifications>
|
||
<sm-popup id="confirmation_popup">
|
||
<h4 id="confirm_title"></h4>
|
||
<p id="confirm_message"></p>
|
||
<div class="flex align-center gap-0-3">
|
||
<button class="cancel-btn margin-left-auto">Cancel</button>
|
||
<button class="button button--primary submit-btn">OK</button>
|
||
</div>
|
||
</sm-popup>
|
||
<article id="loading_page">
|
||
<sm-spinner></sm-spinner>
|
||
<strong>Getting Bitcoin wallet ready</strong>
|
||
</article>
|
||
<div id="main_card">
|
||
<header id="main_header" class="flex align-center space-between">
|
||
<div class="flex align-center">
|
||
<img src="favicon.svg" alt="Bitcoin Web Wallet" class="icon margin-right-0-3"
|
||
style="height: 2rem; width: 2rem;">
|
||
<h4>Bitcoin Wallet</h4>
|
||
</div>
|
||
<div class="flex align-center gap-0-3">
|
||
<sm-select id="currency_selector" class="margin-right-0-5">
|
||
<sm-option value="btc">BTC</sm-option>
|
||
<sm-option value="inr">INR</sm-option>
|
||
<sm-option value="usd">USD</sm-option>
|
||
</sm-select>
|
||
<theme-toggle></theme-toggle>
|
||
</div>
|
||
</header>
|
||
<main id="pages_container" class="grid">
|
||
<div id="check_details" class="page hidden">
|
||
<section class="flex gap-0-5 margin-bottom-1-5">
|
||
<button id="gen_new_addr_btn" class="button primary-action interact">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
|
||
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<g>
|
||
<rect fill="none" height="24" width="24" />
|
||
</g>
|
||
<g>
|
||
<g>
|
||
<path
|
||
d="M18.32,4.26C16.84,3.05,15.01,2.25,13,2.05v2.02c1.46,0.18,2.79,0.76,3.9,1.62L18.32,4.26z M19.93,11h2.02 c-0.2-2.01-1-3.84-2.21-5.32L18.31,7.1C19.17,8.21,19.75,9.54,19.93,11z M18.31,16.9l1.43,1.43c1.21-1.48,2.01-3.32,2.21-5.32 h-2.02C19.75,14.46,19.17,15.79,18.31,16.9z M13,19.93v2.02c2.01-0.2,3.84-1,5.32-2.21l-1.43-1.43 C15.79,19.17,14.46,19.75,13,19.93z M13,12V7h-2v5H7l5,5l5-5H13z M11,19.93v2.02c-5.05-0.5-9-4.76-9-9.95s3.95-9.45,9-9.95v2.02 C7.05,4.56,4,7.92,4,12S7.05,19.44,11,19.93z" />
|
||
</g>
|
||
</g>
|
||
</svg>
|
||
Generate BTC address
|
||
</button>
|
||
<button id="retrieve_addr_btn" class="button primary-action interact"
|
||
onclick="openPopup('retrieve_btc_addr_popup')">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M14 12c0-1.1-.9-2-2-2s-2 .9-2 2 .9 2 2 2 2-.9 2-2zm-2-9c-4.97 0-9 4.03-9 9H0l4 4 4-4H5c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.51 0-2.91-.49-4.06-1.3l-1.42 1.44C8.04 20.3 9.94 21 12 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z" />
|
||
</svg>
|
||
Retrieve BTC address
|
||
</button>
|
||
</section>
|
||
<section>
|
||
<sm-form class="flex margin-bottom-2" style="--gap: 0.5rem;">
|
||
<sm-input type="search" id="search_query_input"
|
||
placeholder="Search BTC address or transaction ID details" required>
|
||
<svg slot="icon" class="icon" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
|
||
</svg>
|
||
</sm-input>
|
||
<button id="check_address_button" class="button button--primary cta" style="height: 3.2rem;"
|
||
type="submit" disabled>Search</button>
|
||
</sm-form>
|
||
<div id="address_details" class="hidden">
|
||
<div id="address_balance_card" class="grid gap-1 hidden">
|
||
<div class="flex">
|
||
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M21 7.28V5c0-1.1-.9-2-2-2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-2.28c.59-.35 1-.98 1-1.72V9c0-.74-.41-1.37-1-1.72zM20 9v6h-7V9h7zM5 19V5h14v2h-6c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h6v2H5z" />
|
||
<circle cx="16" cy="12" r="1.5" />
|
||
</svg>
|
||
Balance
|
||
</div>
|
||
<output id="address_balance" class="amount-shown"></output>
|
||
</div>
|
||
<div class="flex align-center space-between margin-bottom-1 sticky top-0"
|
||
style="background-color: rgba(var(--foreground-color), 1);">
|
||
<h5>Transactions</h5>
|
||
<sm-chips id="filter_selector">
|
||
<sm-chip value="all" selected>All</sm-chip>
|
||
<sm-chip value="sent">Sent</sm-chip>
|
||
<sm-chip value="received">Received</sm-chip>
|
||
</sm-chips>
|
||
</div>
|
||
<ul id="transactions_list" class="observe-empty-state"></ul>
|
||
<div class="empty-state align-self-center text-center">Balance and transactions will appear here
|
||
</div>
|
||
</div>
|
||
<div id="tx_details" class="grid gap-2"></div>
|
||
</section>
|
||
</div>
|
||
<div id="send" class="page hidden">
|
||
<sm-form id="send_tx" skip-submit>
|
||
<div class="margin-bottom-0-5">
|
||
<div class="flex align-center space-between margin-bottom-0-5">
|
||
<h3>Senders</h3>
|
||
<button class="button button--small" id="check_balance" onclick="checkBalance()">Check
|
||
Balance</button>
|
||
</div>
|
||
<div id="sender_container"></div>
|
||
<div class="flex align-center balance-wrapper">
|
||
<span>Total balance:</span>
|
||
<output id="total_balance" class="amount-shown" style="margin-left: 0.3rem;"></output>
|
||
</div>
|
||
<button id="add_sender" class=" button--small">
|
||
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||
</svg>
|
||
Add sender
|
||
</button>
|
||
</div>
|
||
<div>
|
||
<h3 class="margin-bottom-0-5">Receivers</h3>
|
||
<div id="receiver_container"></div>
|
||
<button id="add_receiver" class=" button--small">
|
||
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||
</svg>
|
||
Add receiver</button>
|
||
</div>
|
||
<div id="fees_section" class="grid gap-0-5">
|
||
<div class="flex align-center space-between">
|
||
<h4>Fees</h4>
|
||
<sm-chips id="fees_selector">
|
||
<sm-chip value="suggested" selected>Suggested</sm-chip>
|
||
<sm-chip value="custom">Custom</sm-chip>
|
||
</sm-chips>
|
||
</div>
|
||
<div id="fees_wrapper" class="grid gap-0-5 align-center"></div>
|
||
</div>
|
||
<div id="error_section"></div>
|
||
<div class="multi-state-button margin-bottom-1-5">
|
||
<button id="send_transaction" type="submit" class="button button--primary cta w-100"
|
||
disabled>Send</button>
|
||
</div>
|
||
</sm-form>
|
||
</div>
|
||
<div id="convert_key" class="page hidden">
|
||
<section class="margin-bottom-2 grid gap-1">
|
||
<div class="grid gap-0-3">
|
||
<h4>Private key converter</h4>
|
||
<p>Convert private key of FLO blockchain to corresponding BTC address & private key.</p>
|
||
</div>
|
||
<div class="grid gap-1-5">
|
||
<sm-form class="flex align-center">
|
||
<sm-input type="password" id="any_private" class="password-field"
|
||
placeholder="FLO private key" required animate>
|
||
<label slot="right" class="interact">
|
||
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
||
onchange="togglePrivateKeyVisibility(this)">
|
||
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<title>Hide password</title>
|
||
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
|
||
</svg>
|
||
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<title>Show password</title>
|
||
<path d="M0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
||
</svg>
|
||
</label>
|
||
</sm-input>
|
||
<button type="submit" id="convert_priv_key" class="button button--primary cta">Go</button>
|
||
</sm-form>
|
||
<div class="grid gap-0-5">
|
||
<sm-input type="password" id="btc_private" class="password-field"
|
||
placeholder="BTC private key" animate>
|
||
<label slot="right" class="interact">
|
||
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
||
onchange="togglePrivateKeyVisibility(this)">
|
||
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<title>Hide password</title>
|
||
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
|
||
</svg>
|
||
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<title>Show password</title>
|
||
<path d="M0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
||
</svg>
|
||
</label>
|
||
</sm-input>
|
||
<sm-input id="priv_key_bech32" placeholder="BTC address" data-btc-address
|
||
error-text="Invalid BTC address" animate required></sm-input>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<section class="grid gap-1">
|
||
<div class="grid gap-0-3">
|
||
<h4>Address converter</h4>
|
||
<p class="panel-footer">Convert address: BTC ⇔ FLO</p>
|
||
</div>
|
||
<div class="grid gap-0-5">
|
||
<div class="flex">
|
||
<sm-input id="convert_btc_input" placeholder="BTC Address"
|
||
style="--border-radius: 0.3rem 0 0 0.3rem;" data-btc-address
|
||
error-text="Invalid BTC address" animate required></sm-input>
|
||
<button id="convert_to_flo" class="button--primary justify-self-center"
|
||
style="border-radius: 0 0.3rem 0.3rem 0">
|
||
Convert to FLO
|
||
</button>
|
||
</div>
|
||
<div class="flex">
|
||
<sm-input id="convert_flo_input" placeholder="FLO Address"
|
||
style="--border-radius: 0.3rem 0 0 0.3rem;" animate></sm-input>
|
||
<button id="convert_to_btc" class="button--primary justify-self-center"
|
||
style="border-radius: 0 0.3rem 0.3rem 0">
|
||
Convert to BTC
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
<nav id="main_navbar">
|
||
<ul id="menu">
|
||
<li>
|
||
<a href="#/check_details" class="nav-item interactive">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path d="M12 5.69l5 4.5V18h-2v-6H9v6H7v-7.81l5-4.5M12 3L2 12h3v8h6v-6h2v6h6v-8h3L12 3z" />
|
||
</svg>
|
||
<span class="nav-item__title">
|
||
Address
|
||
</span>
|
||
</a>
|
||
</li>
|
||
<li>
|
||
<a href="#/send" class="nav-item interactive">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M4.01 6.03l7.51 3.22-7.52-1 .01-2.22m7.5 8.72L4 17.97v-2.22l7.51-1M2.01 3L2 10l15 2-15 2 .01 7L23 12 2.01 3z" />
|
||
</svg>
|
||
<span class="nav-item__title">
|
||
Send
|
||
</span></a>
|
||
</li>
|
||
<li>
|
||
<a href="#/convert_key" class="nav-item interactive">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0z" fill="none"></path>
|
||
<path d="M16 17.01V10h-2v7.01h-3L15 21l4-3.99h-3zM9 3L5 6.99h3V14h2V6.99h3L9 3z"></path>
|
||
</svg>
|
||
<span class="nav-item__title">
|
||
Convert
|
||
</span></a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</div>
|
||
<sm-popup id="generate_btc_addr_popup">
|
||
<header slot="header" class="popup__header">
|
||
<div class="flex align-center">
|
||
<button class="popup__header__close" onclick="closePopup()">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||
fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
<div class="grid gap-2">
|
||
<div id="flo_id_warning" class="grid justify-center gap-0-5">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||
fill="#000000">
|
||
<path d="M0 0h24v24H0z" fill="none" />
|
||
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z" />
|
||
</svg>
|
||
<h3>Keep your keys safe!</h3>
|
||
<strong>Don't share with anyone. The private key cannot be recovered if lost.</strong>
|
||
</div>
|
||
<div id="generated_btc_addr" class="generated-id-card"></div>
|
||
</div>
|
||
</sm-popup>
|
||
<sm-popup id="retrieve_btc_addr_popup">
|
||
<header slot="header" class="popup__header">
|
||
<div class="flex align-center">
|
||
<button class="popup__header__close" onclick="closePopup()">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||
fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
<section class="grid gap-1-5">
|
||
<div class="grid gap-0-5">
|
||
<h4>Did you forget your BTC address?</h4>
|
||
<p>If you have your private key, enter it here and recover your BTC address.</p>
|
||
</div>
|
||
<sm-form>
|
||
<div id="recovered_btc_addr_wrapper" class="hidden">
|
||
<h5>Recovered BTC address</h5>
|
||
<sm-copy id="recovered_btc_addr"></sm-copy>
|
||
</div>
|
||
<sm-input id="retrieve_btc_addr_field" type="password" error-text="Invalid private key"
|
||
placeholder="Private key" class="password-field" required autofocus>
|
||
<label slot="right" class="interact">
|
||
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
||
onchange="togglePrivateKeyVisibility(this)">
|
||
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<title>Hide password</title>
|
||
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
|
||
</svg>
|
||
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<title>Show password</title>
|
||
<path d="M0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
||
</svg>
|
||
</label>
|
||
</sm-input>
|
||
<button class="button button--primary cta" type="submit" onclick="retrieveBtcAddr()">Recover</button>
|
||
</sm-form>
|
||
</section>
|
||
</sm-popup>
|
||
<sm-popup id="txid_popup">
|
||
<header slot="header" class="popup__header">
|
||
<div class="flex align-center">
|
||
<button class="popup__header__close" onclick="closePopup()">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||
fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</header>
|
||
<div class="grid gap-2">
|
||
<svg class="icon user-action-result__icon success" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
|
||
</svg>
|
||
<div class="grid gap-0-5 justify-center text-center">
|
||
<h4>Transaction sent</h4>
|
||
<p>Confirmation of transaction might take few hours. </p>
|
||
</div>
|
||
<div class="grid">
|
||
<span class="label">Transaction ID</span>
|
||
<sm-copy id="txid"></sm-copy>
|
||
</div>
|
||
</div>
|
||
</sm-popup>
|
||
<template id="sender_template">
|
||
<fieldset class="sender-card card">
|
||
<sm-input class="sender-input" placeholder="Sender address" data-btc-address
|
||
error-text="Invalid BTC address" animate required></sm-input>
|
||
<sm-input class="priv-key-input password-field" type="password" placeholder="Private Key"
|
||
error-text="Invalid private key" animate required>
|
||
<svg class="icon" slot="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24"
|
||
height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<g>
|
||
<rect fill="none" height="24" width="24" />
|
||
</g>
|
||
<g>
|
||
<path
|
||
d="M21,10h-8.35C11.83,7.67,9.61,6,7,6c-3.31,0-6,2.69-6,6s2.69,6,6,6c2.61,0,4.83-1.67,5.65-4H13l2,2l2-2l2,2l4-4.04L21,10z M7,15c-1.65,0-3-1.35-3-3c0-1.65,1.35-3,3-3s3,1.35,3,3C10,13.65,8.65,15,7,15z" />
|
||
</g>
|
||
</svg>
|
||
<label slot="right" class="interact">
|
||
<input type="checkbox" class="hidden" autocomplete="off" readonly
|
||
onchange="togglePrivateKeyVisibility(this)">
|
||
<svg class="icon invisible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<title>Hide password</title>
|
||
<path d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
|
||
</svg>
|
||
<svg class="icon visible" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
|
||
width="24px" fill="#000000">
|
||
<title>Show password</title>
|
||
<path d="M0 0h24v24H0z" fill="none" />
|
||
<path
|
||
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
||
</svg>
|
||
</label>
|
||
</sm-input>
|
||
<div class="flex align-center space-between full-bleed remove-card-wrapper">
|
||
<div class="flex align-center">
|
||
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M21 7.28V5c0-1.1-.9-2-2-2H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-2.28c.59-.35 1-.98 1-1.72V9c0-.74-.41-1.37-1-1.72zM20 9v6h-7V9h7zM5 19V5h14v2h-6c-1.1 0-2 .9-2 2v6c0 1.1.9 2 2 2h6v2H5z" />
|
||
<circle cx="16" cy="12" r="1.5" />
|
||
</svg>
|
||
<output class="sender-balance amount-shown flex align-center">Balance</output>
|
||
</div>
|
||
<button class="remove-card button--small">
|
||
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
||
</svg>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
</fieldset>
|
||
</template>
|
||
<template id="receiver_template">
|
||
<fieldset class="card receiver-card">
|
||
<sm-input class="receiver-input" placeholder="Receiver address" data-btc-address
|
||
error-text="Invalid BTC address" animate required></sm-input>
|
||
<div class="flex align-start gap-0-5 remove-card-wrapper">
|
||
<sm-input type="number" class="amount-input amount-shown" placeholder="Amount" min="0.0000001"
|
||
step="0.00000001" error-text="Invalid amount" animate required>
|
||
<div class="currency-symbol flex" slot="icon"></div>
|
||
</sm-input>
|
||
<button class="remove-card button--small">
|
||
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||
<path
|
||
d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
|
||
</svg>
|
||
Remove
|
||
</button>
|
||
</div>
|
||
</fieldset>
|
||
</template>
|
||
<script id="ui_utils">
|
||
const { html, svg, render: renderElem } = uhtml;
|
||
const domRefs = {}
|
||
//Checks for internet connection status
|
||
if (!navigator.onLine)
|
||
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', '', true)
|
||
window.addEventListener('offline', () => {
|
||
notify('There seems to be a problem connecting to the internet, Please check you internet connection.', 'error', true, true)
|
||
})
|
||
window.addEventListener('online', () => {
|
||
getRef('notification_drawer').clearAll()
|
||
notify('We are back online.', 'success')
|
||
})
|
||
|
||
// Use instead of document.getElementById
|
||
function getRef(elementId) {
|
||
if (!domRefs.hasOwnProperty(elementId)) {
|
||
domRefs[elementId] = {
|
||
count: 1,
|
||
ref: null,
|
||
};
|
||
return document.getElementById(elementId);
|
||
} else {
|
||
if (domRefs[elementId].count < 3) {
|
||
domRefs[elementId].count = domRefs[elementId].count + 1;
|
||
return document.getElementById(elementId);
|
||
} else {
|
||
if (!domRefs[elementId].ref)
|
||
domRefs[elementId].ref = document.getElementById(elementId);
|
||
return domRefs[elementId].ref;
|
||
}
|
||
}
|
||
}
|
||
// Use when a function needs to be executed after user finishes changes
|
||
const debounce = (callback, wait) => {
|
||
let timeoutId = null;
|
||
return (...args) => {
|
||
window.clearTimeout(timeoutId);
|
||
timeoutId = window.setTimeout(() => {
|
||
callback.apply(null, args);
|
||
}, wait);
|
||
};
|
||
}
|
||
// adds a class to all elements in an array
|
||
function addClass(elements, className) {
|
||
elements.forEach((element) => {
|
||
document.querySelector(element).classList.add(className);
|
||
});
|
||
}
|
||
// removes a class from all elements in an array
|
||
function removeClass(elements, className) {
|
||
elements.forEach((element) => {
|
||
document.querySelector(element).classList.remove(className);
|
||
});
|
||
}
|
||
// return querySelectorAll elements as an array
|
||
function getAllElements(selector) {
|
||
return Array.from(document.querySelectorAll(selector));
|
||
}
|
||
|
||
let zIndex = 50
|
||
// function required for popups or modals to appear
|
||
function openPopup(popupId, pinned) {
|
||
zIndex++
|
||
getRef(popupId).setAttribute('style', `z-index: ${zIndex}`)
|
||
getRef(popupId).show({ pinned })
|
||
return getRef(popupId);
|
||
}
|
||
|
||
// hides the popup or modal
|
||
function closePopup() {
|
||
if (popupStack.peek() === undefined)
|
||
return;
|
||
popupStack.peek().popup.hide()
|
||
}
|
||
|
||
document.addEventListener('popupopened', e => {
|
||
switch (e.target.id) {
|
||
case 'edit_sections_popup':
|
||
break;
|
||
}
|
||
})
|
||
document.addEventListener('popupclosed', e => {
|
||
zIndex--
|
||
switch (e.target.id) {
|
||
case 'retrieve_btc_addr_popup':
|
||
getRef('recovered_btc_addr_wrapper').classList.add('hidden')
|
||
break;
|
||
}
|
||
})
|
||
|
||
// displays a popup for asking permission. Use this instead of JS confirm
|
||
const getConfirmation = (title, options = {}) => {
|
||
return new Promise(resolve => {
|
||
const { message = '', cancelText = 'Cancel', confirmText = 'OK' } = options
|
||
openPopup('confirmation_popup', true)
|
||
getRef('confirm_title').innerText = title;
|
||
getRef('confirm_message').innerText = message;
|
||
let cancelButton = getRef('confirmation_popup').children[2].children[0],
|
||
submitButton = getRef('confirmation_popup').children[2].children[1]
|
||
submitButton.textContent = confirmText
|
||
cancelButton.textContent = cancelText
|
||
submitButton.onclick = () => {
|
||
closePopup()
|
||
resolve(true);
|
||
}
|
||
cancelButton.onclick = () => {
|
||
closePopup()
|
||
resolve(false);
|
||
}
|
||
})
|
||
}
|
||
|
||
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
|
||
function notify(message, mode, options = {}) {
|
||
let icon
|
||
switch (mode) {
|
||
case 'success':
|
||
icon = `<svg class="icon icon--success" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M10 15.172l9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z"/></svg>`
|
||
break;
|
||
case 'error':
|
||
icon = `<svg class="icon icon--error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/></svg>`
|
||
options.pinned = true
|
||
break;
|
||
}
|
||
getRef("notification_drawer").push(message, { icon, ...options });
|
||
if (mode === 'error') {
|
||
console.error(message)
|
||
}
|
||
}
|
||
|
||
function getFormattedTime(timestamp, format) {
|
||
try {
|
||
if (String(timestamp).length < 13)
|
||
timestamp *= 1000
|
||
let [day, month, date, year] = new Date(timestamp).toString().split(' '),
|
||
minutes = new Date(timestamp).getMinutes(),
|
||
hours = new Date(timestamp).getHours(),
|
||
currentTime = new Date().toString().split(' ')
|
||
|
||
minutes = minutes < 10 ? `0${minutes}` : minutes
|
||
let finalHours = ``;
|
||
if (hours > 12)
|
||
finalHours = `${hours - 12}:${minutes}`
|
||
else if (hours === 0)
|
||
finalHours = `12:${minutes}`
|
||
else
|
||
finalHours = `${hours}:${minutes}`
|
||
|
||
finalHours = hours >= 12 ? `${finalHours} PM` : `${finalHours} AM`
|
||
switch (format) {
|
||
case 'date-only':
|
||
return `${month} ${date}, ${year}`;
|
||
break;
|
||
case 'time-only':
|
||
return finalHours;
|
||
case 'relative':
|
||
// check if timestamp is older than a day
|
||
if (Date.now() - new Date(timestamp) < 60 * 60 * 24 * 1000)
|
||
return `${finalHours}`;
|
||
else
|
||
return relativeTime.from(timestamp)
|
||
default:
|
||
return `${month} ${date}, ${year} at ${finalHours}`;
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
return timestamp;
|
||
}
|
||
}
|
||
// returns dom with specified element
|
||
function createElement(tagName, options = {}) {
|
||
const { className, textContent, innerHTML, attributes = {} } = options
|
||
const elem = document.createElement(tagName)
|
||
for (let attribute in attributes) {
|
||
elem.setAttribute(attribute, attributes[attribute])
|
||
}
|
||
if (className)
|
||
elem.className = className
|
||
if (textContent)
|
||
elem.textContent = textContent
|
||
if (innerHTML)
|
||
elem.innerHTML = innerHTML
|
||
return elem
|
||
}
|
||
// detect browser version
|
||
function detectBrowser() {
|
||
let ua = navigator.userAgent,
|
||
tem,
|
||
M = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
|
||
if (/trident/i.test(M[1])) {
|
||
tem = /\brv[ :]+(\d+)/g.exec(ua) || [];
|
||
return 'IE ' + (tem[1] || '');
|
||
}
|
||
if (M[1] === 'Chrome') {
|
||
tem = ua.match(/\b(OPR|Edge)\/(\d+)/);
|
||
if (tem != null) return tem.slice(1).join(' ').replace('OPR', 'Opera');
|
||
}
|
||
M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, '-?'];
|
||
if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]);
|
||
return M.join(' ');
|
||
}
|
||
window.addEventListener('hashchange', e => routeTo(window.location.hash))
|
||
let selectedCurrency
|
||
window.addEventListener("load", () => {
|
||
const [browserName, browserVersion] = detectBrowser().split(' ');
|
||
const supportedVersions = {
|
||
Chrome: 85,
|
||
Firefox: 75,
|
||
Safari: 13,
|
||
}
|
||
if (browserName in supportedVersions) {
|
||
if (parseInt(browserVersion) < supportedVersions[browserName]) {
|
||
notify(`${browserName} ${browserVersion} is not fully supported, some features may not work properly. Please update to ${supportedVersions[browserName]} or higher.`, 'error')
|
||
}
|
||
} else {
|
||
notify('Browser is not fully compatible, some features may not work. for best experience please use Chrome, Edge, Firefox or Safari', 'error')
|
||
}
|
||
document.body.classList.remove('hidden')
|
||
document.querySelectorAll('sm-input[data-btc-address]').forEach(input => input.customValidation = btcOperator.validateAddress)
|
||
document.addEventListener('keyup', (e) => {
|
||
if (e.key === 'Escape') {
|
||
closePopup()
|
||
}
|
||
})
|
||
document.addEventListener('copy', () => {
|
||
notify('copied', 'success')
|
||
})
|
||
document.addEventListener("pointerdown", (e) => {
|
||
if (e.target.closest("button, .interactive")) {
|
||
createRipple(e, e.target.closest("button, .interactive"));
|
||
}
|
||
});
|
||
getExchangeRate()
|
||
.then(() => {
|
||
selectedCurrency = localStorage.getItem('btc-wallet-currency') || 'btc'
|
||
getRef('currency_selector').value = selectedCurrency
|
||
})
|
||
.catch(e => {
|
||
selectedCurrency = 'btc'
|
||
// console.error(e)
|
||
getRef('currency_selector').classList.add('hidden')
|
||
}).finally(() => {
|
||
routeTo(window.location.hash)
|
||
setTimeout(() => {
|
||
getRef('loading_page').animate([
|
||
{ transform: 'translateY(0)', },
|
||
{ transform: 'translateY(-100%)', }
|
||
], {
|
||
duration: 300,
|
||
fill: 'forwards',
|
||
easing: 'ease'
|
||
}).onfinish = () => {
|
||
getRef('loading_page').remove()
|
||
}
|
||
}, 500);
|
||
document.querySelectorAll('.currency-symbol').forEach(el => el.innerHTML = currencyIcons[selectedCurrency])
|
||
getRef('add_sender').click();
|
||
getRef('add_receiver').click();
|
||
})
|
||
|
||
});
|
||
function createRipple(event, target) {
|
||
const circle = document.createElement("span");
|
||
const diameter = Math.max(target.clientWidth, target.clientHeight);
|
||
const radius = diameter / 2;
|
||
const targetDimensions = target.getBoundingClientRect();
|
||
circle.style.width = circle.style.height = `${diameter}px`;
|
||
circle.style.left = `${event.clientX - (targetDimensions.left + radius)}px`;
|
||
circle.style.top = `${event.clientY - (targetDimensions.top + radius)}px`;
|
||
circle.classList.add("ripple");
|
||
const rippleAnimation = circle.animate(
|
||
[
|
||
{
|
||
opacity: 1,
|
||
transform: `scale(0)`
|
||
},
|
||
{
|
||
transform: "scale(4)",
|
||
opacity: 0,
|
||
},
|
||
],
|
||
{
|
||
duration: 600,
|
||
fill: "forwards",
|
||
easing: "ease-out",
|
||
}
|
||
);
|
||
target.append(circle);
|
||
rippleAnimation.onfinish = () => {
|
||
circle.remove();
|
||
};
|
||
}
|
||
|
||
const pagesData = {
|
||
params: {}
|
||
}
|
||
|
||
let tempData
|
||
async function routeTo(targetPage, options = {}) {
|
||
const { firstLoad, hashChange, isPreview } = options
|
||
let pageId
|
||
let params = {}
|
||
let searchParams
|
||
if (targetPage === '') {
|
||
pageId = 'check_details'
|
||
} else {
|
||
if (targetPage.includes('/')) {
|
||
let path;
|
||
[path, searchParams] = targetPage.split('?');
|
||
[, pageId, subPageId1] = path.split('/')
|
||
} else {
|
||
pageId = targetPage
|
||
}
|
||
}
|
||
if (searchParams) {
|
||
const urlSearchParams = new URLSearchParams('?' + searchParams);
|
||
params = Object.fromEntries(urlSearchParams.entries());
|
||
}
|
||
if (params)
|
||
pagesData.params = params
|
||
switch (pageId) {
|
||
case 'check_details':
|
||
if (params.query) {
|
||
const query = getRef('search_query_input').value.trim();
|
||
if (params.query !== query)
|
||
getRef('search_query_input').value = params.query;
|
||
render.queryResult(params.query)
|
||
}
|
||
setTimeout(() => {
|
||
getRef('search_query_input').focusIn()
|
||
}, 200);
|
||
break;
|
||
}
|
||
const animOptions = {
|
||
duration: 100,
|
||
fill: 'forwards',
|
||
}
|
||
let previousActiveElement = getRef('main_navbar').querySelector('.nav-item--active')
|
||
const currentActiveElement = document.querySelector(`.nav-item[href="#/${pageId}"]`)
|
||
if (currentActiveElement) {
|
||
if (getRef('main_navbar').classList.contains('hidden')) {
|
||
getRef('main_navbar').classList.remove('hide-away')
|
||
getRef('main_navbar').classList.remove('hidden')
|
||
getRef('main_navbar').animate([
|
||
{
|
||
transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
|
||
opacity: 0,
|
||
},
|
||
{
|
||
transform: `none`,
|
||
opacity: 1,
|
||
},
|
||
], {
|
||
duration: 100,
|
||
fill: 'forwards',
|
||
easing: 'ease'
|
||
})
|
||
}
|
||
getRef('main_header').classList.remove('hidden')
|
||
const previousActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(previousActiveElement)
|
||
const currentActiveElementIndex = [...getRef('main_navbar').querySelectorAll('.nav-item')].indexOf(currentActiveElement)
|
||
const isOnTop = previousActiveElementIndex < currentActiveElementIndex
|
||
const currentIndicator = createElement('div', { className: 'nav-item__indicator' });
|
||
let previousIndicator = getRef('main_navbar').querySelector('.nav-item__indicator')
|
||
if (!previousIndicator) {
|
||
previousIndicator = currentIndicator.cloneNode(true)
|
||
previousActiveElement = currentActiveElement
|
||
previousActiveElement.append(previousIndicator)
|
||
} else if (currentActiveElementIndex !== previousActiveElementIndex) {
|
||
const indicatorDimensions = previousIndicator.getBoundingClientRect()
|
||
const currentActiveElementDimensions = currentActiveElement.getBoundingClientRect()
|
||
let moveBy
|
||
if (isMobileView) {
|
||
moveBy = ((currentActiveElementDimensions.width - indicatorDimensions.width) / 2) + indicatorDimensions.width
|
||
} else {
|
||
moveBy = ((currentActiveElementDimensions.height - indicatorDimensions.height) / 2) + indicatorDimensions.height
|
||
}
|
||
indicatorObserver.observe(previousIndicator)
|
||
previousIndicator.animate([
|
||
{
|
||
transform: 'none',
|
||
opacity: 1,
|
||
},
|
||
{
|
||
transform: `translate${isMobileView ? 'X' : 'Y'}(${isOnTop ? `${moveBy}px` : `-${moveBy}px`})`,
|
||
opacity: 0,
|
||
},
|
||
], { ...animOptions, easing: 'ease-in' }).onfinish = () => {
|
||
previousIndicator.remove()
|
||
}
|
||
tempData = {
|
||
currentActiveElement,
|
||
currentIndicator,
|
||
isOnTop,
|
||
animOptions,
|
||
moveBy
|
||
}
|
||
}
|
||
previousActiveElement.classList.remove('nav-item--active');
|
||
currentActiveElement.classList.add('nav-item--active')
|
||
} else {
|
||
if (!getRef('main_navbar').classList.contains('hidden')) {
|
||
getRef('main_navbar').classList.add('hide-away')
|
||
getRef('main_navbar').animate([
|
||
{
|
||
transform: `none`,
|
||
opacity: 1,
|
||
},
|
||
{
|
||
transform: isMobileView ? `translateY(100%)` : `translateX(-100%)`,
|
||
opacity: 0,
|
||
},
|
||
], {
|
||
duration: 200,
|
||
fill: 'forwards',
|
||
easing: 'ease'
|
||
}).onfinish = () => {
|
||
getRef('main_navbar').classList.add('hidden')
|
||
}
|
||
getRef('main_header').classList.add('hidden')
|
||
}
|
||
}
|
||
if (pagesData.lastPage !== pageId) {
|
||
document.querySelectorAll('.page').forEach(page => page.classList.add('hidden'))
|
||
getRef(pageId).classList.remove('hidden')
|
||
getRef(pageId).animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, fill: 'forwards', easing: 'ease' })
|
||
pagesData.lastPage = pageId
|
||
}
|
||
}
|
||
|
||
const indicatorObserver = new IntersectionObserver(entries => {
|
||
entries.forEach(entry => {
|
||
if (!entry.isIntersecting) {
|
||
const { currentActiveElement, currentIndicator, isOnTop, animOptions, moveBy } = tempData
|
||
currentActiveElement.append(currentIndicator)
|
||
currentIndicator.animate([
|
||
{
|
||
transform: `translate${isMobileView ? 'X' : 'Y'}(${isOnTop ? `-${moveBy}px` : `${moveBy}px`})`,
|
||
opacity: 0,
|
||
},
|
||
{
|
||
transform: 'none',
|
||
opacity: 1
|
||
},
|
||
], { ...animOptions, easing: 'ease-out' })
|
||
}
|
||
})
|
||
}, {
|
||
threshold: 1
|
||
})
|
||
// class based lazy loading
|
||
class LazyLoader {
|
||
constructor(container, elementsToRender, renderFn, options = {}) {
|
||
const { batchSize = 10, freshRender, bottomFirst = false, domUpdated } = options
|
||
|
||
this.elementsToRender = elementsToRender
|
||
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
||
this.renderFn = renderFn
|
||
this.intersectionObserver
|
||
|
||
this.batchSize = batchSize
|
||
this.freshRender = freshRender
|
||
this.domUpdated = domUpdated
|
||
this.bottomFirst = bottomFirst
|
||
|
||
this.shouldLazyLoad = false
|
||
this.lastScrollTop = 0
|
||
this.lastScrollHeight = 0
|
||
|
||
this.lazyContainer = document.querySelector(container)
|
||
|
||
this.update = this.update.bind(this)
|
||
this.render = this.render.bind(this)
|
||
this.init = this.init.bind(this)
|
||
this.clear = this.clear.bind(this)
|
||
}
|
||
get elements() {
|
||
return this.arrayOfElements
|
||
}
|
||
init() {
|
||
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||
entries.forEach(entry => {
|
||
if (entry.isIntersecting) {
|
||
observer.disconnect()
|
||
this.render({ lazyLoad: true })
|
||
}
|
||
})
|
||
})
|
||
this.mutationObserver = new MutationObserver(mutationList => {
|
||
mutationList.forEach(mutation => {
|
||
if (mutation.type === 'childList') {
|
||
if (mutation.addedNodes.length) {
|
||
if (this.bottomFirst) {
|
||
if (this.lazyContainer.firstElementChild)
|
||
this.intersectionObserver.observe(this.lazyContainer.firstElementChild)
|
||
} else {
|
||
if (this.lazyContainer.lastElementChild)
|
||
this.intersectionObserver.observe(this.lazyContainer.lastElementChild)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
})
|
||
this.mutationObserver.observe(this.lazyContainer, {
|
||
childList: true,
|
||
})
|
||
this.render()
|
||
}
|
||
update(elementsToRender) {
|
||
this.arrayOfElements = (typeof elementsToRender === 'function') ? this.elementsToRender() : elementsToRender || []
|
||
}
|
||
render(options = {}) {
|
||
let { lazyLoad = false } = options
|
||
this.shouldLazyLoad = lazyLoad
|
||
const frag = document.createDocumentFragment();
|
||
if (lazyLoad) {
|
||
if (this.bottomFirst) {
|
||
this.updateEndIndex = this.updateStartIndex
|
||
this.updateStartIndex = this.updateEndIndex - this.batchSize
|
||
} else {
|
||
this.updateStartIndex = this.updateEndIndex
|
||
this.updateEndIndex = this.updateEndIndex + this.batchSize
|
||
}
|
||
} else {
|
||
this.intersectionObserver.disconnect()
|
||
if (this.bottomFirst) {
|
||
this.updateEndIndex = this.arrayOfElements.length
|
||
this.updateStartIndex = this.updateEndIndex - this.batchSize - 1
|
||
} else {
|
||
this.updateStartIndex = 0
|
||
this.updateEndIndex = this.batchSize
|
||
}
|
||
this.lazyContainer.innerHTML = ``;
|
||
}
|
||
this.lastScrollHeight = this.lazyContainer.scrollHeight
|
||
this.lastScrollTop = this.lazyContainer.scrollTop
|
||
this.arrayOfElements.slice(this.updateStartIndex, this.updateEndIndex).forEach((element, index) => {
|
||
frag.append(this.renderFn(element))
|
||
})
|
||
if (this.bottomFirst) {
|
||
this.lazyContainer.prepend(frag)
|
||
// scroll anchoring for reverse scrolling
|
||
this.lastScrollTop += this.lazyContainer.scrollHeight - this.lastScrollHeight
|
||
this.lazyContainer.scrollTo({ top: this.lastScrollTop })
|
||
this.lastScrollHeight = this.lazyContainer.scrollHeight
|
||
} else {
|
||
this.lazyContainer.append(frag)
|
||
}
|
||
if (!lazyLoad && this.bottomFirst) {
|
||
this.lazyContainer.scrollTop = this.lazyContainer.scrollHeight
|
||
}
|
||
// Callback to be called if elements are updated or rendered for first time
|
||
if (!lazyLoad && this.freshRender)
|
||
this.freshRender()
|
||
}
|
||
clear() {
|
||
this.intersectionObserver.disconnect()
|
||
this.mutationObserver.disconnect()
|
||
this.lazyContainer.innerHTML = ``;
|
||
}
|
||
reset() {
|
||
this.arrayOfElements = (typeof this.elementsToRender === 'function') ? this.elementsToRender() : this.elementsToRender || []
|
||
this.render()
|
||
}
|
||
}
|
||
|
||
function buttonLoader(id, show) {
|
||
const button = typeof id === 'string' ? getRef(id) : id;
|
||
button.disabled = show;
|
||
const animOptions = {
|
||
duration: 200,
|
||
fill: 'forwards',
|
||
easing: 'ease'
|
||
}
|
||
if (show) {
|
||
button.animate([
|
||
{
|
||
clipPath: 'circle(100%)',
|
||
},
|
||
{
|
||
clipPath: 'circle(0)',
|
||
},
|
||
], animOptions).onfinish = e => {
|
||
e.target.commitStyles()
|
||
e.target.cancel()
|
||
}
|
||
button.parentNode.append(createElement('sm-spinner'))
|
||
} else {
|
||
button.style = ''
|
||
const potentialTarget = button.parentNode.querySelector('sm-spinner')
|
||
if (potentialTarget) potentialTarget.remove();
|
||
}
|
||
}
|
||
let isMobileView = false
|
||
const mobileQuery = window.matchMedia('(max-width: 40rem)')
|
||
function handleMobileChange(e) {
|
||
isMobileView = e.matches
|
||
}
|
||
mobileQuery.addEventListener('change', handleMobileChange)
|
||
|
||
handleMobileChange(mobileQuery)
|
||
function showChildElement(id, index, options = {}) {
|
||
const { mobileView = false, entry, exit } = options
|
||
const animOptions = {
|
||
duration: 150,
|
||
easing: 'ease',
|
||
fill: 'forwards'
|
||
}
|
||
const visibleElement = [...getRef(id).children].find(elem => !elem.classList.contains(mobileView ? 'hide-on-mobile' : 'hidden'));
|
||
if (visibleElement === getRef(id).children[index]) return;
|
||
if (visibleElement) {
|
||
if (exit) {
|
||
visibleElement.animate(exit, animOptions).onfinish = () => {
|
||
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
|
||
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
||
if (entry)
|
||
getRef(id).children[index].animate(entry, animOptions)
|
||
}
|
||
} else {
|
||
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
|
||
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
||
}
|
||
} else {
|
||
getRef(id).children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
|
||
getRef(id).children[index].animate(entry, animOptions)
|
||
}
|
||
}
|
||
</script>
|
||
<script>
|
||
let transactionsLazyLoader
|
||
let txDetailsAbortController
|
||
const render = {
|
||
transactionCard(transactionDetails) {
|
||
let { address, amount, time, txid, sender, receiver, type, block } = transactionDetails;
|
||
let transactionReceiver
|
||
let icon
|
||
// block = null
|
||
if (type === 'out') {
|
||
transactionReceiver = html`Sent to ${receiver.map(address => html`<a href="${`#/check_details?query=${address}`}" class="tx-participant wrap-around">${address}</a>`)}`;
|
||
icon = svg`<svg class="icon sent" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
|
||
} else if (type === 'in') {
|
||
transactionReceiver = html`Received from ${sender.map(address => html`<a href="${`#/check_details?query=${address}`}" class="tx-participant wrap-around">${address}</a>`)}`;
|
||
icon = svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
|
||
} else if (type === 'self') {
|
||
transactionReceiver = `Sent to self`;
|
||
icon = svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
|
||
}
|
||
if (!block) {
|
||
icon = svg`<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><path d="M6,2l0.01,6L10,12l-3.99,4.01L6,22h12v-6l-4-4l4-3.99V2H6z M16,16.5V20H8v-3.5l4-4L16,16.5z"/></g></svg>`;
|
||
}
|
||
const className = `transaction grid ${type} ${block === null ? 'unconfirmed-tx' : ''}`
|
||
return html.node`
|
||
<li class="${className}" data-txid="${txid}">
|
||
<div class="transaction__icon">${icon}</div>
|
||
<time class="transaction__time">${getFormattedTime(time)}</time>
|
||
<div class="transaction__amount amount-shown" data-btc-amount="${amount}">${formatAmount(getConvertedAmount(amount))}</div>
|
||
<div class="transaction__receiver">${transactionReceiver}</div>
|
||
<div class="transaction__id wrap-around">TXID: <a href="${`#/check_details?query=${txid}`}">${txid}</a></div>
|
||
${!block ? html`<p class="pending-badge">Confirmation pending: amount will be deducted after transaction is confirmed</p>` : ''}
|
||
</li>
|
||
`;
|
||
},
|
||
async transactions(address) {
|
||
try {
|
||
getRef('address_details').classList.remove('hidden')
|
||
getRef('transactions_list').innerHTML = '<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
|
||
getRef('address_balance').innerHTML = '<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
|
||
btcOperator.getAddressData(address).then(result => {
|
||
getRef('address_balance').value = formatAmount(getConvertedAmount(result.balance));
|
||
getRef('address_balance').dataset.btcAmount = result.balance;
|
||
getRef('address_balance').parentElement.classList.remove('hidden')
|
||
getRef('filter_selector').classList.remove('hidden')
|
||
// render transactions
|
||
if (result.txs.length) {
|
||
let allTransactions = result.txs;
|
||
const filter = getRef('filter_selector').value;
|
||
if (filter !== 'all') {
|
||
allTransactions = allTransactions.filter(t => filter === 'sent' ? t.type === 'out' : t.type === 'in')
|
||
}
|
||
if (transactionsLazyLoader) {
|
||
transactionsLazyLoader.update(allTransactions)
|
||
} else {
|
||
transactionsLazyLoader = new LazyLoader('#transactions_list', allTransactions, render.transactionCard)
|
||
}
|
||
transactionsLazyLoader.init()
|
||
getRef('transactions_list').previousElementSibling.classList.remove('hidden');
|
||
} else {
|
||
getRef('transactions_list').textContent = 'No transactions found';
|
||
}
|
||
}).catch(error => {
|
||
console.error(error)
|
||
getRef('filter_selector').classList.add('hidden')
|
||
getRef('transactions_list').textContent = `Looks like we couldn't fetch your transactions at this time. Please try again later.`;
|
||
}).finally(_ => getRef('check_address_button').disabled = false)
|
||
} catch (err) {
|
||
notify(err, 'error');
|
||
}
|
||
},
|
||
addressDetails(address) {
|
||
getRef('check_address_button').disabled = true;
|
||
render.transactions(address)
|
||
},
|
||
async txDetails(txid) {
|
||
getRef('tx_details').classList.remove('hidden')
|
||
renderElem(getRef('tx_details'), html`<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>`);
|
||
if (txDetailsAbortController) {
|
||
txDetailsAbortController.abort()
|
||
}
|
||
txDetailsAbortController = new AbortController();
|
||
btcOperator.getTx(txid).then(result => {
|
||
|
||
|
||
const { block, time, size, fee, inputs, outputs, confirmations, total_input_value, total_output_value } = result;
|
||
|
||
console.debug('tx', result);
|
||
renderElem(getRef('tx_details'), html`
|
||
<table class="margin-bottom-1-5 justify-self-center">
|
||
<tbody>
|
||
<tr>
|
||
<td>Hash</td>
|
||
<td class="wrap-around">${txid}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Block</td>
|
||
<td>${block}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Confirmations</td>
|
||
<td>${confirmations}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Size</td>
|
||
<td>${size} bytes</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Time</td>
|
||
<td>${getFormattedTime(time)}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Total Inputs</td>
|
||
<td class="amount-shown" data-btc-amount="${total_input_value}">${formatAmount(getConvertedAmount(total_input_value))}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Total Outputs</td>
|
||
<td class="amount-shown" data-btc-amount="${total_output_value}">${formatAmount(getConvertedAmount(total_output_value))}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Fee</td>
|
||
<td class="amount-shown" data-btc-amount="${fee}">${formatAmount(getConvertedAmount(fee))}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div id="in_out_wrapper" class="flex flex-wrap">
|
||
<div>
|
||
<div class="flex align-center space-between margin-bottom-1" style="padding: 0.5rem 1rem;">
|
||
<b>Input addresses</b>
|
||
<b>Value</b>
|
||
</div>
|
||
<ul>
|
||
${inputs.map(input => html`
|
||
<li class="in-out-card">
|
||
<a href="${`#/check_details?query=${input.address}`}" class="input-address wrap-around">${input.address}</a>
|
||
<div class="input-value amount-shown" data-btc-amount="${input.value}">${formatAmount(getConvertedAmount(input.value))}</div>
|
||
</li>
|
||
`)}
|
||
</ul>
|
||
</div>
|
||
<div>
|
||
<div class="flex align-center space-between margin-bottom-1" style="padding: 0.5rem 1rem;">
|
||
<b>Output addresses</b>
|
||
<b>Value</b>
|
||
</div>
|
||
<ul>
|
||
${outputs.map(output => html`
|
||
<li class="in-out-card">
|
||
<a href="${`#/check_details?query=${output.address}`}" class="output-address wrap-around">${output.address}</a>
|
||
<div class="output-value amount-shown" data-btc-amount="${output.value}">${formatAmount(getConvertedAmount(output.value))}</div>
|
||
</li>
|
||
`)}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
`)
|
||
}).catch(err => {
|
||
notify(err, 'error')
|
||
renderElem(getRef('tx_details'), html``)
|
||
})
|
||
},
|
||
queryResult(query) {
|
||
const type = checkQueryStringType(query);
|
||
if (type === 'address') {
|
||
getRef('tx_details').classList.add('hidden')
|
||
render.addressDetails(query)
|
||
} else if (type === 'txid') {
|
||
getRef('address_details').classList.add('hidden')
|
||
render.txDetails(query)
|
||
if (transactionsLazyLoader) {
|
||
transactionsLazyLoader.clear()
|
||
transactionsLazyLoader = null
|
||
}
|
||
} else {
|
||
if (transactionsLazyLoader) {
|
||
transactionsLazyLoader.clear()
|
||
transactionsLazyLoader = null
|
||
}
|
||
getRef('address_details').classList.add('hidden')
|
||
getRef('tx_details').classList.add('hidden')
|
||
notify('Invalid address or transaction id', 'error');
|
||
}
|
||
}
|
||
}
|
||
const currencyIcons = {
|
||
btc: ` <svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"> <g> <rect fill="none" height="24" width="24"></rect> </g> <g> <path d="M17.06,11.57C17.65,10.88,18,9.98,18,9c0-1.86-1.27-3.43-3-3.87L15,3h-2v2h-2V3H9v2H6v2h2v10H6v2h3v2h2v-2h2v2h2v-2 c2.21,0,4-1.79,4-4C19,13.55,18.22,12.27,17.06,11.57z M10,7h4c1.1,0,2,0.9,2,2s-0.9,2-2,2h-4V7z M15,17h-5v-4h5c1.1,0,2,0.9,2,2 S16.1,17,15,17z"> </path> </g> </svg> `,
|
||
usd: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11.8 10.9c-2.27-.59-3-1.2-3-2.15 0-1.09 1.01-1.85 2.7-1.85 1.78 0 2.44.85 2.5 2.1h2.21c-.07-1.72-1.12-3.3-3.21-3.81V3h-3v2.16c-1.94.42-3.5 1.68-3.5 3.61 0 2.31 1.91 3.46 4.7 4.13 2.5.6 3 1.48 3 2.41 0 .69-.49 1.79-2.7 1.79-2.06 0-2.87-.92-2.98-2.1h-2.2c.12 2.19 1.76 3.42 3.68 3.83V21h3v-2.15c1.95-.37 3.5-1.5 3.5-3.55 0-2.84-2.43-3.81-4.7-4.4z"/></svg>`,
|
||
inr: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M13.66,7C13.1,5.82,11.9,5,10.5,5L6,5V3h12v2l-3.26,0c0.48,0.58,0.84,1.26,1.05,2L18,7v2l-2.02,0c-0.25,2.8-2.61,5-5.48,5 H9.77l6.73,7h-2.77L7,14v-2h3.5c1.76,0,3.22-1.3,3.46-3L6,9V7L13.66,7z"/></g></g></svg>`
|
||
}
|
||
function formatAmount(amount = 0) {
|
||
// check if amount is a string and convert it to a number
|
||
if (typeof amount === 'string') {
|
||
amount = parseFloat(amount)
|
||
}
|
||
const currency = getRef('currency_selector').value;
|
||
if (!amount)
|
||
return '0';
|
||
return amount.toLocaleString(currency === 'inr' ? `en-IN` : 'en-US', { style: 'currency', currency, maximumFractionDigits: currency === 'btc' ? 8 : 2 })
|
||
}
|
||
let globalExchangeRate = {}
|
||
async function getExchangeRate() {
|
||
return new Promise((resolve, reject) => {
|
||
Promise.all(['usd', 'inr'].map(cur => fetch(`https://bitpay.com/api/rates/btc/${cur}`))).then(responses => {
|
||
Promise.all(responses.map(res => res.json())).then(rates => {
|
||
rates.forEach(rate => {
|
||
globalExchangeRate[rate.code.toLowerCase()] = rate.rate
|
||
})
|
||
globalExchangeRate.btc = 1
|
||
resolve(globalExchangeRate)
|
||
}).catch(err => reject(err))
|
||
}).catch(err => reject(err))
|
||
})
|
||
}
|
||
function getConvertedAmount(amount) {
|
||
// check if amount is a string and convert it to a number
|
||
if (typeof amount === 'string') {
|
||
amount = parseFloat(amount)
|
||
}
|
||
if (globalExchangeRate[getRef('currency_selector').value])
|
||
return parseFloat((amount * globalExchangeRate[getRef('currency_selector').value]).toFixed(8))
|
||
else return amount
|
||
}
|
||
function roundUp(amount, precision = 2) {
|
||
return parseFloat((Math.ceil(amount * Math.pow(10, precision)) / Math.pow(10, precision)).toFixed(precision))
|
||
}
|
||
getRef('currency_selector').addEventListener('change', async e => {
|
||
localStorage.setItem('btc-wallet-currency', e.target.value);
|
||
document.querySelectorAll('.currency-symbol').forEach(el => el.innerHTML = currencyIcons[e.target.value])
|
||
document.querySelectorAll('.amount-shown').forEach(el => {
|
||
if (el.tagName.includes('SM-')) {
|
||
const originalAmount = parseFloat(el.value.trim());
|
||
let convertedAmount
|
||
const rupeeRate = (globalExchangeRate.inr / globalExchangeRate.usd);
|
||
switch (selectedCurrency) {
|
||
case 'usd':
|
||
if (e.target.value === 'inr')
|
||
convertedAmount = roundUp(originalAmount * rupeeRate)
|
||
else
|
||
convertedAmount = roundUp((originalAmount / globalExchangeRate.usd), 8)
|
||
break;
|
||
case 'inr':
|
||
if (e.target.value === 'usd')
|
||
convertedAmount = roundUp((originalAmount / rupeeRate))
|
||
else
|
||
convertedAmount = roundUp((originalAmount / globalExchangeRate.inr), 8)
|
||
break;
|
||
case 'btc':
|
||
convertedAmount = roundUp(originalAmount * globalExchangeRate[e.target.value])
|
||
break;
|
||
}
|
||
el.value = convertedAmount
|
||
} else {
|
||
el.textContent = formatAmount(getConvertedAmount(el.dataset.btcAmount))
|
||
}
|
||
})
|
||
selectedCurrency = e.target.value;
|
||
})
|
||
getRef('filter_selector').addEventListener('change', async e => {
|
||
const address = getRef('search_query_input').value;
|
||
render.transactions(address)
|
||
})
|
||
getRef('gen_new_addr_btn').addEventListener('click', () => {
|
||
const { wif, address, segwitAddress, bech32Address } = btcOperator.newKeys;
|
||
renderElem(getRef('generated_btc_addr'), html`
|
||
<div>
|
||
<h5>BTC Address</h5>
|
||
<sm-copy value="${bech32Address}"></sm-copy>
|
||
</div>
|
||
<div>
|
||
<h5>Private Key</h5>
|
||
<sm-copy value="${wif}"></sm-copy>
|
||
</div>
|
||
`);
|
||
openPopup('generate_btc_addr_popup');
|
||
});
|
||
function retrieveBtcAddr() {
|
||
let wif = getRef('retrieve_btc_addr_field').value.trim();
|
||
getRef('recovered_btc_addr_wrapper').classList.remove('hidden')
|
||
getRef('recovered_btc_addr').value = btcOperator.bech32Address(wif);
|
||
}
|
||
|
||
function togglePrivateKeyVisibility(input) {
|
||
const target = input.closest('sm-input')
|
||
target.type = target.type === 'password' ? 'text' : 'password';
|
||
target.focusIn()
|
||
}
|
||
|
||
getRef('convert_priv_key').onclick = evt => {
|
||
let source_wif = getRef('any_private').value.trim();
|
||
let btc_wif = btcOperator.convert.wif(source_wif);
|
||
getRef('btc_private').value = btc_wif;
|
||
getRef('priv_key_bech32').value = btcOperator.bech32Address(btc_wif);
|
||
}
|
||
getRef('convert_to_flo').onclick = evt => {
|
||
const btc_bech = getRef('convert_btc_input').value.trim();
|
||
if (btc_bech === '') {
|
||
getRef('convert_btc_input').focusIn()
|
||
return notify('Please enter BTC address', 'error');
|
||
}
|
||
const type = coinjs.addressDecode(btc_bech).type
|
||
if (type === 'standard') {
|
||
getRef('convert_flo_input').value = btcOperator.convert.legacy2legacy(btc_bech, 0x23);
|
||
} else if (type === 'bech32') {
|
||
getRef('convert_flo_input').value = btcOperator.convert.bech2legacy(btc_bech, 0x23);
|
||
} else {
|
||
getRef('convert_flo_input').value = '';
|
||
notify(`Multisig address can't be converted to FLO`, 'error');
|
||
}
|
||
}
|
||
getRef('convert_to_btc').onclick = evt => {
|
||
const flo_addr = getRef('convert_flo_input').value.trim();
|
||
if (flo_addr === '') {
|
||
getRef('convert_flo_input').focusIn();
|
||
return notify('Please enter FLO address', 'error');
|
||
}
|
||
getRef('convert_btc_input').value = btcOperator.convert.legacy2bech(flo_addr);
|
||
}
|
||
const txParticipantsObserver = new MutationObserver(mutations => {
|
||
mutations.forEach(mutation => {
|
||
if (mutation.type === 'childList') {
|
||
if (mutation.addedNodes.length > 0 && mutation.target.children.length > 1) {
|
||
const removeButton = mutation.target.firstElementChild.querySelector('.remove-card')
|
||
if (!removeButton) {
|
||
const newRemoveButton = html.node`
|
||
<button class="remove-card button--small">
|
||
<svg class="icon margin-right-0-3" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000">
|
||
<path d="M0 0h24v24H0V0z" fill="none"></path>
|
||
<path d="M7 11v2h10v-2H7zm5-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"></path>
|
||
</svg>
|
||
Remove
|
||
</button>
|
||
`
|
||
mutation.target.firstElementChild.querySelector('.remove-card-wrapper').appendChild(newRemoveButton)
|
||
}
|
||
} else if (mutation.removedNodes.length > 0 && mutation.target.children.length === 1) {
|
||
const removeButton = mutation.target.firstElementChild.querySelector('.remove-card')
|
||
if (removeButton) {
|
||
removeButton.remove()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
})
|
||
txParticipantsObserver.observe(getRef('sender_container'), {
|
||
childList: true
|
||
})
|
||
txParticipantsObserver.observe(getRef('receiver_container'), {
|
||
childList: true
|
||
})
|
||
getRef('add_sender').onclick = evt => {
|
||
let senderCard = getRef('sender_template').content.cloneNode(true)
|
||
if (!getRef('sender_container').children.length) {
|
||
senderCard.querySelector('.remove-card').remove()
|
||
}
|
||
getRef('sender_container').appendChild(senderCard);
|
||
getRef('sender_container').querySelectorAll('sm-input[data-btc-address]').forEach(input => input.customValidation = btcOperator.validateAddress)
|
||
}
|
||
getRef('send_tx').addEventListener('click', e => {
|
||
if (e.target.closest('.remove-card')) {
|
||
e.target.closest('.card').remove()
|
||
}
|
||
})
|
||
async function checkBalance() {
|
||
const addresses = [...getRef('sender_container').querySelectorAll('.sender-input')].filter(input => input.value.trim() !== '').map(input => input.value.trim())
|
||
if (addresses.length === 0) {
|
||
notify('Please add at least one sender address', 'error')
|
||
return;
|
||
}
|
||
getRef("total_balance").innerHTML = '<sm-spinner></sm-spinner>';
|
||
let totalBalance = 0;
|
||
console.debug(addresses);
|
||
let senderBalances = [...getRef('sender_container').querySelectorAll('.sender-balance')];
|
||
senderBalances.forEach(el => el.innerHTML = '<sm-spinner></sm-spinner>');
|
||
Promise.all(addresses.map((addr, index) => btcOperator.getBalance(addr))).then(balances => {
|
||
balances.forEach((balance, index) => {
|
||
senderBalances[index].textContent = formatAmount(getConvertedAmount(balance));
|
||
senderBalances[index].dataset.btcAmount = balance;
|
||
totalBalance += balance;
|
||
})
|
||
console.log(totalBalance)
|
||
getRef("total_balance").textContent = `${formatAmount(getConvertedAmount(totalBalance))}`;
|
||
getRef("total_balance").dataset.btcAmount = totalBalance;
|
||
}).catch(err => {
|
||
console.error(err);
|
||
notify('Error while fetching balance', 'error');
|
||
senderBalances.forEach(el => el.innerHTML = '');
|
||
getRef("total_balance").innerHTML = '';
|
||
})
|
||
}
|
||
getRef('add_receiver').onclick = evt => {
|
||
let receiverCard = getRef('receiver_template').content.cloneNode(true)
|
||
if (!getRef('receiver_container').children.length) {
|
||
receiverCard.querySelector('.remove-card').remove()
|
||
}
|
||
receiverCard.querySelector('.currency-symbol') ? receiverCard.querySelector('.currency-symbol').innerHTML = currencyIcons[getRef('currency_selector').value] : null;
|
||
getRef('receiver_container').appendChild(receiverCard);
|
||
getRef('receiver_container').querySelectorAll('sm-input[data-btc-address]').forEach(input => input.customValidation = btcOperator.validateAddress)
|
||
}
|
||
|
||
getRef('fees_selector').addEventListener('change', renderFeesUI)
|
||
function renderFeesUI() {
|
||
switch (getRef('fees_selector').value) {
|
||
case 'custom':
|
||
renderElem(getRef('fees_wrapper'), html`
|
||
<p id="selected_fee_tip">Set custom fee</p>
|
||
<sm-input type="number" id="send_fee" class="amount-shown" placeholder="Fee" min="0.000001" step="0.00000001"
|
||
error-text="Please enter valid fees" animate required>
|
||
<div class="currency-symbol flex" slot="icon"></div>
|
||
</sm-input>
|
||
`)
|
||
document.getElementById('send_fee').focusIn();
|
||
getRef('fees_wrapper').querySelector('.currency-symbol').innerHTML = currencyIcons[getRef('currency_selector').value];
|
||
getRef('fees_section').classList.remove('hidden')
|
||
renderElem(getRef('error_section'), html``)
|
||
break;
|
||
case 'suggested':
|
||
renderElem(getRef('fees_wrapper'), html`<sm-spinner></sm-spinner>`)
|
||
if (getRef('send_tx').isFormValid) {
|
||
calculateExactFee()
|
||
} else {
|
||
calculateApproxFee().then(fees => {
|
||
renderElem(getRef('fees_wrapper'), html`
|
||
<div class="grid gap-0-3">
|
||
<div>
|
||
Approximate fee: <b id="recommended_fee" class="amount-shown" data-btc-amount=${fees}>${formatAmount(getConvertedAmount(fees))}</b>
|
||
</div>
|
||
<p style="opacity: 0.8;">*Exact fee will be calculated after you fill all the required fields</p>
|
||
</div>
|
||
`)
|
||
}).catch(e => {
|
||
getRef('fees_selector').children[1].click();
|
||
getRef('fees_selector').classList.add('hidden')
|
||
})
|
||
}
|
||
getRef('fees_section').classList.remove('hidden')
|
||
renderElem(getRef('error_section'), html``)
|
||
break;
|
||
}
|
||
}
|
||
|
||
function getTransactionInputs() {
|
||
const senders = [...new Set([...getRef('sender_container').querySelectorAll('.sender-input')].map(input => input.value.trim()))];
|
||
const privKeys = [...getRef('sender_container').querySelectorAll('.priv-key-input')].map(input => input.value.trim());
|
||
const receivers = [...getRef('receiver_container').querySelectorAll('.receiver-input')].map(input => input.value.trim());
|
||
const amounts = [...getRef('receiver_container').querySelectorAll('.amount-input')].map(input => {
|
||
return parseFloat(input.value.trim()) / (globalExchangeRate[getRef('currency_selector').value] || 1)
|
||
});
|
||
console.debug(senders, receivers, amounts); //for automatic fee calc, set fee = null
|
||
return [senders, privKeys, receivers, amounts]
|
||
}
|
||
function calculateApproxFee() {
|
||
return new Promise((resolve, reject) => {
|
||
fetch('https://bitcoiner.live/api/fees/estimates/latest')
|
||
.then(res => {
|
||
res.json()
|
||
.then(data => {
|
||
const satPerByte = data.estimates['60'].sat_per_vbyte;
|
||
const legacyBytes = 200;
|
||
const segwitBytes = 77;
|
||
resolve((legacyBytes * satPerByte + (0.25 * satPerByte) * segwitBytes) / Math.pow(10, 8));
|
||
}).catch(e => {
|
||
reject(e)
|
||
})
|
||
}).catch(e => {
|
||
reject(e)
|
||
})
|
||
})
|
||
}
|
||
function calculateExactFee() {
|
||
return new Promise((resolve, reject) => {
|
||
renderElem(getRef('fees_wrapper'), html`<sm-spinner></sm-spinner>`)
|
||
const [senders, privKeys, receivers, amounts] = getTransactionInputs();
|
||
btcOperator.createSignedTx(senders, privKeys, receivers, amounts).then(({ fee }) => {
|
||
renderElem(getRef('fees_wrapper'), html` <b id="recommended_fee" class="amount-shown" data-btc-amount=${fee}>${formatAmount(getConvertedAmount(fee))}</b> `)
|
||
getRef('send_transaction').disabled = false;
|
||
getRef('fees_section').classList.remove('hidden')
|
||
getRef('error_section').classList.add('hidden')
|
||
resolve(fee)
|
||
}).catch(e => {
|
||
getRef('send_transaction').disabled = true;
|
||
getRef('fees_section').classList.add('hidden')
|
||
getRef('error_section').classList.remove('hidden')
|
||
if (e.includes('Invalid private key')) {
|
||
const invalidKeys = e.split(':')[1].split(',').map(key => key.trim());
|
||
invalidKeys.forEach(key => {
|
||
const input = [...getRef('sender_container').querySelectorAll('.sender-input')].find(input => input.value.trim() === key);
|
||
if (input)
|
||
input.nextElementSibling.showError()
|
||
})
|
||
} else {
|
||
renderElem(getRef('error_section'), html`
|
||
<p id="selected_fee_tip" class="error flex align-center gap-0-5">
|
||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M11 15h2v2h-2v-2zm0-8h2v6h-2V7zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg>
|
||
${e}
|
||
</p>
|
||
`)
|
||
reject(e)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
getRef('send_tx').addEventListener('valid', debounce(e => {
|
||
if (getRef('fees_selector').value === 'suggested') {
|
||
calculateExactFee()
|
||
} else {
|
||
getRef('fees_section').classList.remove('hidden')
|
||
getRef('error_section').classList.add('hidden')
|
||
if (getRef('send_tx').validity)
|
||
getRef('send_transaction').disabled = false;
|
||
}
|
||
}, 300))
|
||
getRef('send_tx').addEventListener('invalid', e => {
|
||
renderFeesUI()
|
||
getRef('send_transaction').disabled = true;
|
||
})
|
||
|
||
|
||
getRef('send_transaction').onclick = evt => {
|
||
buttonLoader('send_transaction', true)
|
||
const [senders, privKeys, receivers, amounts] = getTransactionInputs();
|
||
let fee = null;
|
||
if (getRef('fees_selector').value === 'custom') {
|
||
const feeInput = document.getElementById('send_fee').value.trim();
|
||
if (!feeInput || isNaN(feeInput) || feeInput <= 0) {
|
||
notify('Please enter a valid fee', 'error');
|
||
buttonLoader('send_transaction', false)
|
||
return;
|
||
}
|
||
fee = parseFloat((parseFloat(feeInput) / (globalExchangeRate[getRef('currency_selector').value] || 1)).toFixed(8));
|
||
}
|
||
btcOperator.sendTx(senders, privKeys, receivers, amounts, fee).then(txid => {
|
||
console.log(txid);
|
||
getRef('txid').value = txid;
|
||
openPopup('txid_popup', true);
|
||
getRef('send_tx').reset();
|
||
}).catch(error => {
|
||
console.error(error)
|
||
notify(`Error sending transaction \n ${error}`, 'error');
|
||
}).finally(_ => {
|
||
buttonLoader('send_transaction', false)
|
||
})
|
||
}
|
||
|
||
// detect if given string is a bitcoin transaction id
|
||
function isTxId(str) {
|
||
return str.length === 64 && str.match(/^[0-9a-f]+$/i);
|
||
}
|
||
// detect if given string is a bitcoin address or transaction id
|
||
function checkQueryStringType(str) {
|
||
if (btcOperator.validateAddress(str)) {
|
||
return 'address';
|
||
} else if (isTxId(str)) {
|
||
return 'txid';
|
||
} else {
|
||
return 'invalid';
|
||
}
|
||
}
|
||
getRef('check_address_button').addEventListener('click', evt => {
|
||
const query = getRef('search_query_input').value.trim();
|
||
if (pagesData.params.hasOwnProperty('query') && query === pagesData.params.query) {
|
||
render.queryResult(query);
|
||
} else {
|
||
location.hash = `#/check_details?query=${query}`;
|
||
}
|
||
})
|
||
|
||
</script>
|
||
</body>
|
||
|
||
</html> |