btcwallet/index.html
2023-09-28 04:13:03 +05:30

2100 lines
121 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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=Roboto:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap"
rel="stylesheet">
<script src="scripts/components.js" defer></script>
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
<script type="text/javascript" src="scripts/lib.js"></script>
<script src="scripts/floCrypto.js"></script>
<script type="text/javascript" src="scripts/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="app-brand">
<img src="favicon.svg" alt="Bitcoin Web Wallet logo" class="icon">
<div class="app-name">
<div class="app-name__company">RanchiMall</div>
<h4 class="app-name__title">
Bitcoin Wallet
</h4>
</div>
</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" 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); z-index: 2">
<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" onclick="addSenderInput()">
<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" onclick="addReceiverInput()">
<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 flex flex-direction-column gap-1-5">
<sm-chips id="conversion_type_selector" class="margin-right-auto"
onchange="changeConversionView(event)">
<sm-chip value="flo" selected>FLO</sm-chip>
<sm-chip value="btc">BTC</sm-chip>
</sm-chips>
<div id="conversion_wrapper">
<div class="grid gap-2">
<div class="grid gap-1-5">
<div class="grid gap-0-3">
<h3>Private key converter</h3>
<p>Convert private key of FLO blockchain to corresponding BTC address & private key
</p>
</div>
<sm-form id="convert_flo_private_key_form">
<div class="input-action-wrapper">
<sm-input type="password" id="convert_flo_private_key" class="password-field"
placeholder="FLO private key" data-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" onclick="convertFloPrivateKey()"
class="button button--primary cta">Convert</button>
</div>
</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>
<div class="grid gap-1">
<div class="grid gap-0-3">
<h3>Address converter</h3>
<p class="panel-footer">Convert FLO address to BTC address</p>
</div>
<sm-form id="convert_flo_address_form" class="flex align-items-start">
<div class="input-action-wrapper">
<sm-input id="convert_flo_input" placeholder="FLO Address" data-flo-address
error-text="Invalid FLO address. It usually starts with 'F'" animate
required></sm-input>
<button id="convert_to_btc" class="button--primary justify-self-center cta"
type="submit">
Convert
</button>
</div>
</sm-form>
<div id="flo_address_converter_result"></div>
</div>
</div>
<div class="grid gap-2 hidden">
<div class="grid gap-1-5">
<div class="grid gap-0-3">
<h3>Private key converter</h3>
<p>Convert private key of BTC blockchain to corresponding FLO address & private
key
</p>
</div>
<sm-form id="convert_btc_private_key_form">
<div class="input-action-wrapper">
<sm-input type="password" id="convert_btc_private_key" class="password-field"
placeholder="BTC private key" data-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" onclick="convertBtcPrivateKey()"
class="button button--primary cta">Convert</button>
</div>
</sm-form>
<div class="grid gap-0-5">
<sm-input type="password" id="flo_private" class="password-field"
placeholder="FLO 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="converted_flo_address" placeholder="FLO address" animate></sm-input>
</div>
</div>
<div class="grid gap-1">
<div class="grid gap-0-3">
<h3>Address converter</h3>
<p class="panel-footer">Convert BTC address to FLO address</p>
</div>
<sm-form id="convert_btc_address_form" class="flex">
<div class="input-action-wrapper">
<sm-input id="convert_btc_input" placeholder="BTC Address" data-btc-address
error-text="Invalid BTC address. It usually starts with 'b'" animate
required></sm-input>
<button id="convert_to_flo" class="button--primary justify-self-center cta"
type="submit">
Convert
</button>
</div>
</sm-form>
<div id="btc_address_converter_result"></div>
</div>
</div>
</div>
</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" placeholder="Private key" class="password-field"
data-private-key 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="increase_fee_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>
<h3>Increase fee</h3>
</div>
</header>
<div id="increase_fee_popup_content"></div>
</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_popup__resolved_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" 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-content-start gap-0-5 remove-card-wrapper">
<sm-input type="number" class="amount-input amount-shown" placeholder="Amount" min="0.00000001"
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) {
}
})
document.addEventListener('popupclosed', e => {
zIndex--
switch (e.target.id) {
case 'retrieve_btc_addr_popup':
getRef('recovered_btc_addr_wrapper').classList.add('hidden')
break;
case 'increase_fee_popup':
renderElem(getRef('increase_fee_popup_content'), html``)
break;
}
})
//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 = 'btc'
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 = (value) => { return { isValid: btcOperator.validateAddress(value) } })
document.querySelectorAll('sm-input[data-flo-address]').forEach(input => input.customValidation = (value) => { return { isValid: floCrypto.validateFloID(value) } })
document.querySelectorAll('sm-input[data-private-key]').forEach(input => input.customValidation = (value) => {
return {
isValid: floCrypto.getPubKeyHex(value),
errorText: `Invalid private key. It's a long string of random characters usually starting with 'L' or 'R'.`
}
})
document.addEventListener('keyup', (e) => {
if (e.key === 'Escape') {
closePopup()
}
})
document.addEventListener('copy', () => {
notify('copied', 'success')
})
document.addEventListener("pointerdown", (e) => {
if (e.target.closest("button:not(:disabled), .interactive:not(:disabled)")) {
createRipple(e, e.target.closest("button, .interactive"));
}
});
getExchangeRate()
.then(() => {
selectedCurrency = localStorage.getItem('btc-wallet-currency') || 'btc'
setTimeout(() => {
document.getElementById('currency_selector').value = selectedCurrency
}, 100)
})
.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);
addSenderInput()
addReceiverInput()
document.querySelectorAll('.currency-symbol').forEach(el => el.innerHTML = currencyIcons[selectedCurrency])
})
});
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' ? document.getElementById(id) : id;
button.disabled = show;
const animOptions = {
duration: 200,
fill: 'forwards',
easing: 'ease'
}
if (show) {
button.parentNode.append(createElement('sm-spinner'))
button.animate([
{
clipPath: 'circle(100%)',
},
{
clipPath: 'circle(0)',
},
], animOptions)
} else {
button.getAnimations().forEach(anim => anim.cancel())
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)
const slideInLeft = [
{
opacity: 0,
transform: 'translateX(1.5rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutLeft = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(-1.5rem)'
},
]
const slideInRight = [
{
opacity: 0,
transform: 'translateX(-1.5rem)'
},
{
opacity: 1,
transform: 'translateX(0)'
}
]
const slideOutRight = [
{
opacity: 1,
transform: 'translateX(0)'
},
{
opacity: 0,
transform: 'translateX(1.5rem)'
},
]
const slideInDown = [
{
opacity: 0,
transform: 'translateY(-1.5rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutUp = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(-1.5rem)'
},
]
function showChildElement(id, index, options = {}) {
return new Promise((resolve) => {
const { mobileView = false, entry, exit } = options
const animOptions = {
duration: 150,
easing: 'ease',
fill: 'forwards'
}
const parent = typeof id === 'string' ? document.getElementById(id) : id;
const visibleElement = [...parent.children].find(elem => !elem.classList.contains(mobileView ? 'hide-on-mobile' : 'hidden'));
if (visibleElement === parent.children[index]) return;
visibleElement.getAnimations().forEach(anim => anim.cancel())
parent.children[index].getAnimations().forEach(anim => anim.cancel())
if (visibleElement) {
if (exit) {
visibleElement.animate(exit, animOptions).onfinish = () => {
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
if (entry)
parent.children[index].animate(entry, animOptions).onfinish = () => resolve()
}
} else {
visibleElement.classList.add(mobileView ? 'hide-on-mobile' : 'hidden')
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
resolve()
}
} else {
parent.children[index].classList.remove(mobileView ? 'hide-on-mobile' : 'hidden')
parent.children[index].animate(entry, animOptions).onfinish = () => resolve()
}
})
}
</script>
<script>
let transactionsLazyLoader
let txDetailsAbortController
const render = {
transactionCard(transactionDetails) {
let { address, amount, time, txid, sender, receiver, type, block } = transactionDetails;
let transactionReceiver
let icon
const transactingAddresses = (receiver || sender || [])
const transactingAddressesLinks = transactingAddresses
.slice(0, 2).map(address => html`<a href="${`#/check_details?query=${address}`}" class="tx-participant wrap-around">${address}</a>`)
// block = null
if (type === 'out') {
transactionReceiver = html`Sent to ${transactingAddressesLinks}`;
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 ${transactingAddressesLinks}`;
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 queriedAddress = pagesData.params?.query || getRef('search_query_input').value.trim()
const isSender = type === 'out' || type === 'self'
const isMultisig = btcOperator.validateAddress(queriedAddress) !== 'standard'
const className = `transaction grid ${type} ${block === null ? 'unconfirmed-tx' : ''}`
return html.node`
<li class="${className}" data-txid="${txid}" data-transacting-addresses=${transactingAddresses.slice(2)}>
<div class="transaction__icon">${icon}</div>
<div class="grid gap-0-5">
<div class="flex gap-0-5 space-between align-center flex-wrap">
<time class="transaction__time">${getFormattedTime(time)}</time>
<div class="transaction__amount amount-shown" data-btc-amount="${amount}">${formatAmount(getConvertedAmount(amount))}</div>
</div>
<div class="transaction__receiver">
${transactionReceiver}
${transactingAddresses.length > 2 ? html`<button onclick=${showAllAddresses} class="button button--small show-more" title="See all addresses">... +${transactingAddresses.length - 2} more</button>` : ''}
</div>
<div class="flex gap-0-5 flex-wrap align-center">
<a class="button button--small gap-0-3 align-center button--colored transaction__id" href="${`#/check_details?query=${txid}`}">
<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 7h2v2h-2zm0 4h2v6h-2zm1-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>
View details
</a>
${isSender && !block && !isMultisig ? html`
<div class="multi-state-button">
<button class="button button--small gap-0-3" onclick=${initFeeChange} title="Resend transaction with greater fees to reduce confirmation time">
<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 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
Increase fee
</button>
</div>
` : ''}
</div>
${!block ? html`
<p class="pending-badge">
Confirmation pending: amount will be deducted after transaction is confirmed.
${isSender && !block && !isMultisig ? ' Try increasing fee to speed up confirmation' : ''}
</p>
` : ''}
</div>
</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 = `The data service is temporarily unavailable due to over-usage. Please try again in an hour.`;
}).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`
<h3>Transaction Details</h3>
<div class="flex align-center gap-1 space-between flex-wrap">
<time>${getFormattedTime(time)}</time>
${!block ? html` <h4 id="tx_status">
<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="M11.99 2C6.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"/><path d="M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
Unconfirmed
</h4> ` : ''}
</div>
<div id="tx_technicals" class="justify-self-center details-wrapper">
<div class="tx-detail">
<div class="flex align-center gap-0-3">
<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="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>
<div>Confirmations</div>
</div>
<div style="font-size: 1.5rem">${confirmations}</div>
</div>
${block ? html`
<div class="tx-detail">
<div class="flex align-center gap-0-3">
<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><g><path d="M3,3v8h8V3H3z M9,9H5V5h4V9z M3,13v8h8v-8H3z M9,19H5v-4h4V19z M13,3v8h8V3H13z M19,9h-4V5h4V9z M13,13v8h8v-8H13z M19,19h-4v-4h4V19z"/></g></g></g></svg>
<div>Block</div>
</div>
<div>${block}</div>
</div>
` : ''}
<div class="tx-detail">
<div class="flex align-center gap-0-3">
<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="M15 4c-4.42 0-8 3.58-8 8s3.58 8 8 8 8-3.58 8-8-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zM3 12c0-2.61 1.67-4.83 4-5.65V4.26C3.55 5.15 1 8.27 1 12s2.55 6.85 6 7.74v-2.09c-2.33-.82-4-3.04-4-5.65z"/></svg>
<div>Fee</div>
</div>
<div class="amount-shown" data-btc-amount="${fee}">${formatAmount(getConvertedAmount(fee))}</div>
</div>
</div>
<details class="margin-bottom-1-5 justify-self-center w-100">
<summary>
More details
<svg class="icon down-arrow" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M24 24H0V0h24v24z" fill="none" opacity=".87"/><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z"/></svg>
</summary>
<div class="details-wrapper">
<div class="tx-detail">
<div class="flex align-center gap-0-3">
<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>
<div>Total Inputs</div>
</div>
<div class="amount-shown" data-btc-amount="${total_input_value}">${formatAmount(getConvertedAmount(total_input_value))}</div>
</div>
<div class="tx-detail">
<div class="flex align-center gap-0-3">
<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 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>
<div>Total Outputs</div>
</div>
<div class="amount-shown" data-btc-amount="${total_output_value}">${formatAmount(getConvertedAmount(total_output_value))}</div>
</div>
<div class="tx-detail">
<div class="flex align-center gap-0-3">
<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="M2 20h20v-4H2v4zm2-3h2v2H4v-2zM2 4v4h20V4H2zm4 3H4V5h2v2zm-4 7h20v-4H2v4zm2-3h2v2H4v-2z"/></svg>
<div>Size</div>
</div>
<div>${size} bytes</div>
</div>
</div>
</details>
<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>Sender addresses</b>
<b>Amount</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>Receiver addresses</b>
<b>Amount</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('Invalid transaction ID', '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)
}
if (!amount)
return '0';
return amount.toLocaleString(selectedCurrency === 'inr' ? `en-IN` : 'en-US', { style: 'currency', currency: selectedCurrency, maximumFractionDigits: selectedCurrency === '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[selectedCurrency])
return parseFloat((amount * globalExchangeRate[selectedCurrency]).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))
}
let previouslySelectedCurrency = localStorage.getItem('btc-wallet-currency') || 'btc';
getRef('currency_selector').addEventListener('change', e => {
selectedCurrency = e.target.value;
localStorage.setItem('btc-wallet-currency', selectedCurrency);
document.querySelectorAll('.currency-symbol').forEach(el => el.innerHTML = currencyIcons[selectedCurrency])
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 (previouslySelectedCurrency) {
case 'usd':
if (selectedCurrency === 'inr')
convertedAmount = roundUp(originalAmount * rupeeRate)
else
convertedAmount = roundUp((originalAmount / globalExchangeRate.usd), 8)
break;
case 'inr':
if (selectedCurrency === 'usd')
convertedAmount = roundUp((originalAmount / rupeeRate))
else
convertedAmount = roundUp((originalAmount / globalExchangeRate.inr), 8)
break;
case 'btc':
convertedAmount = roundUp(originalAmount * globalExchangeRate[selectedCurrency])
break;
}
el.value = convertedAmount
} else {
if (el.dataset.btcAmount === undefined) return
el.textContent = formatAmount(getConvertedAmount(el.dataset.btcAmount))
}
})
previouslySelectedCurrency = selectedCurrency
})
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()
}
function changeConversionView(e) {
switch (e.target.value) {
case 'flo':
showChildElement('conversion_wrapper', 0, {
entry: slideInRight,
exit: slideOutRight
})
break;
case 'btc':
showChildElement('conversion_wrapper', 1, {
entry: slideInLeft,
exit: slideOutLeft
})
break;
default:
break;
}
}
function convertFloPrivateKey() {
const source_wif = getRef('convert_flo_private_key').value.trim();
const btc_wif = btcOperator.convert.wif(source_wif);
getRef('btc_private').value = btc_wif;
getRef('priv_key_bech32').value = btcOperator.bech32Address(btc_wif);
}
getRef('convert_flo_private_key_form').addEventListener('invalid', e => {
getRef('btc_private').value = '';
getRef('priv_key_bech32').value = '';
})
getRef('convert_to_btc').onclick = evt => {
try {
const flo_addr = getRef('convert_flo_input').value.trim();
const convertedAddress = btcOperator.convert.legacy2bech(flo_addr);
renderElem(getRef('flo_address_converter_result'), html`
<span class="label">Converted BTC Address</span>
<sm-copy value="${convertedAddress}"></sm-copy>
`)
} catch (err) {
renderElem(getRef('flo_address_converter_result'), html` `)
notify('Invalid FLO address', 'error')
}
}
getRef('convert_flo_address_form').addEventListener('invalid', e => {
renderElem(getRef('flo_address_converter_result'), html` `)
})
function convertBtcPrivateKey() {
const source_wif = getRef('convert_btc_private_key').value.trim();
const flo_wif = btcOperator.convert.wif(source_wif, bitjs.priv);
getRef('flo_private').value = flo_wif;
getRef('converted_flo_address').value = floCrypto.getFloID(flo_wif);
}
getRef('convert_btc_private_key_form').addEventListener('invalid', e => {
getRef('flo_private').value = '';
getRef('converted_flo_address').value = '';
})
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
let convertedAddress
if (type === 'standard') {
convertedAddress = btcOperator.convert.legacy2legacy(btc_bech, 0x23);
} else if (type === 'bech32') {
convertedAddress = btcOperator.convert.bech2legacy(btc_bech, 0x23);
} else {
try {
convertedAddress = floCrypto.toMultisigFloID(btc_bech)
} catch (e) {
notify(`Multisig address can't be converted to FLO`, 'error');
}
}
if (convertedAddress) {
renderElem(getRef('btc_address_converter_result'), html`
<span class="label">Converted FLO Address</span>
<sm-copy value="${convertedAddress}"></sm-copy>
`)
} else {
renderElem(getRef('btc_address_converter_result'), html` `)
}
}
getRef('convert_btc_address_form').addEventListener('invalid', e => {
renderElem(getRef('btc_address_converter_result'), html` `)
})
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
})
function addSenderInput(address) {
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 = (value) => {
const validate = btcOperator.validateAddress(value)
const isValid = !(validate === false || validate === 'bech32m')
console.log(validate)
return { isValid, errorText: validate === 'bech32m' ? `This is a Taproot address. This wallet does't support claiming Taproot funds yet.` : 'Please enter valid BTC address' }
})
if (address) {
getRef('sender_container').lastElementChild.querySelector('.sender-input').value = address
}
}
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 = '';
})
}
function addReceiverInput(address, value) {
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[selectedCurrency] : null;
getRef('receiver_container').appendChild(receiverCard);
getRef('receiver_container').querySelectorAll('sm-input[data-btc-address]').forEach(input => input.customValidation = (value) => { return { isValid: btcOperator.validateAddress(value) } })
if (address) {
getRef('receiver_container').lastElementChild.querySelector('.receiver-input').value = address
}
if (value) {
getRef('receiver_container').lastElementChild.querySelector('.amount-input').value = getConvertedAmount(value)
}
}
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[selectedCurrency];
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[selectedCurrency] || 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.createTx(senders, 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[selectedCurrency] || 1)).toFixed(8));
}
console.log(senders, privKeys, receivers, amounts, fee);
btcOperator.sendTx(senders, privKeys, receivers, amounts, fee).then(txid => {
console.log(txid);
getRef('txid_popup__resolved_txid').value = txid;
openPopup('txid_popup', true);
getRef('send_tx').reset();
}).catch(error => {
console.error(error)
if (error === 'TypeError: Failed to fetch') {
notify(`
<div style="display: grid; gap: 0.3rem;">
<h4>Something went wrong!</h4>
<p style="font-size: 0.9rem; opacity: 0.8">Please check transaction history before retrying.</p>
</div>
`, 'error', {
action: {
label: 'Check History',
callback: () => {
location.hash = `/check_details?query=${senders[0]}`;
document.getElementById('notification_drawer').clearAll()
}
}
});
} else {
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) || /^bc1[a-z0-9]{59}$/i.test(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}`;
}
})
function showAllAddresses(e) {
const addresses = e.target.closest('li').dataset.transactingAddresses
const addressesList = addresses.split(',').map(address => html.node`<a href="${`#/check_details?query=${address}`}" class="tx-participant wrap-around">${address}</a>`)
e.target.closest('button').before(...addressesList)
e.target.closest('button').remove()
}
let changingFeeOf = null
async function initFeeChange(e) {
const button = e.target.closest('button')
buttonLoader(button, true)
const txid = button.closest('li').dataset.txid
changingFeeOf = txid
try {
const { inputs, outputs, fee } = await btcOperator.getTx(txid)
const { witness } = btcOperator.deserializeTx(await btcOperator.getTx.hex(txid))
const requiredSigns = extractLastHexStrings(witness).reduce((acc, hex) => {
const { address, required } = btcOperator.decodeRedeemScript(hex) || {}
if (address)
return { ...acc, [address]: required }
else return acc
}, {})
const senders = inputs.map(input => input.address)
const receivers = outputs.map(output => output.address)
const amounts = outputs.map(output => 0.00000001)
const { fee: recommendedFee = 0 } = await btcOperator.createTx(senders, receivers, amounts)
renderElem(getRef('increase_fee_popup_content'), html`
<sm-form style="--gap: 2rem">
<div class="grid gap-0-5">
<h4>Senders</h4>
<ul class="grid gap-0-5">
${senders.map((address) => html.node`<li class="increase-fee-sender grid gap-1">
<div>
<div class="label">Address</div>
<b class="sender__address wrap-around">${address}</b>
</div>
${[...Array(requiredSigns[address] || 1)].map(_ => html`<sm-input class="sender__private-key password-field" type="password" placeholder="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"></rect> </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"></path> </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> <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"></path> </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> <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"></path> </svg>
</label>
</sm-input>`)}
</li>`)}
</ul>
</div>
<div class="grid gap-0-5">
<h4>Receivers</h4>
<ul class="grid gap-0-5">
${outputs.map(output => html.node`<li class="increase-fee-receiver grid gap-1">
<div>
<div class="label">Address</div>
<b class="wrap-around">${output.address}</b>
</div>
<div>
<div class="label">Amount</div>
<b>${formatAmount(getConvertedAmount(output.value))}</b>
</div>
</li>`)}
</ul>
</div>
<div class="grid gap-0-5">
<p>
Previous fee: <b>${formatAmount(getConvertedAmount(fee))}</b> ${recommendedFee ? html`| Recommended fee: <b>${formatAmount(getConvertedAmount(recommendedFee))}</b>` : ''}
</p>
<sm-input id="new_fee" placeholder="New fee" type="number" min=${getConvertedAmount(fee)} step="0.00000001" error-text=${`New fee should be greater than ${formatAmount(getConvertedAmount(fee))}`} animate required>
<div class="currency-symbol flex" slot="icon"> </div>
</sm-input>
</div>
<div class="multi-state-button">
<button id="increase_fee" class="button button--primary" onclick=${increaseFee} type="submit">Increase fee</button>
</div>
</sm-form>
`)
document.getElementById('new_fee').querySelector('.currency-symbol').innerHTML = currencyIcons[selectedCurrency]
openPopup('increase_fee_popup')
} catch (e) {
console.error(e)
} finally {
buttonLoader(button, false)
}
}
function increaseFee() {
buttonLoader(document.getElementById('increase_fee'), true)
const newFee = parseFloat((parseFloat(document.getElementById('new_fee').value.trim()) / (globalExchangeRate[selectedCurrency] || 1)).toFixed(8))
const privateKeys = []
document.querySelectorAll('.increase-fee-sender').forEach(sender => {
const address = sender.querySelector('.sender__address').textContent.trim()
const privateKey = sender.querySelector('.sender__private-key').value.trim()
if (!btcOperator.verifyKey(address, privateKey))
return notify(`Invalid private key for address ${address}`, 'error')
if (privateKey) {
privateKeys.push(privateKey)
}
})
console.log(changingFeeOf, newFee, privateKeys)
btcOperator.editFee(changingFeeOf, newFee, privateKeys).then(signedTxHex => {
btcOperator.broadcastTx(signedTxHex).then(txId => {
console.log(txId)
closePopup()
getRef('txid_popup__resolved_txid').value = txId;
openPopup('txid_popup', true);
}).catch(e => {
notify(e, 'error')
}).finally(_ => {
buttonLoader(document.getElementById('increase_fee'), false)
changingFeeOf = null
})
}).catch(e => {
notify(e, 'error')
buttonLoader(document.getElementById('increase_fee'), false)
})
}
function extractLastHexStrings(arr) {
return arr
.filter(innerArray => innerArray.length > 0)
.map(innerArray => innerArray[innerArray.length - 1])
}
</script>
</body>
</html>