3812 lines
147 KiB
HTML
3812 lines
147 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>Solana Wallet</title>
|
|
<link rel="stylesheet" href="css/main.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" />
|
|
</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" class="breakable"></p>
|
|
<div class="flex align-center gap-0-5 margin-left-auto">
|
|
<button class="button cancel-button">Cancel</button>
|
|
<button class="button button--primary confirm-button">OK</button>
|
|
</div>
|
|
</sm-popup>
|
|
<!-- // Main file to control all ui files -->
|
|
<main>
|
|
<header id="main_header">
|
|
<div id="logo" class="app-brand">
|
|
<svg id="main_logo" class="icon" viewBox="0 0 27.25 32">
|
|
<title>RanchiMall</title>
|
|
<path
|
|
d="M27.14,30.86c-.74-2.48-3-4.36-8.25-6.94a20,20,0,0,1-4.2-2.49,6,6,0,0,1-1.25-1.67,4,4,0,0,1,0-2.26c.37-1.08.79-1.57,3.89-4.55a11.66,11.66,0,0,0,3.34-4.67,6.54,6.54,0,0,0,.05-2.82C20,3.6,18.58,2,16.16.49c-.89-.56-1.29-.64-1.3-.24a3,3,0,0,1-.3.72l-.3.55L13.42.94C13,.62,12.4.26,12.19.15c-.4-.2-.73-.18-.72.05a9.39,9.39,0,0,1-.61,1.33s-.14,0-.27-.13C8.76.09,8-.27,8,.23A11.73,11.73,0,0,1,6.76,2.6C4.81,5.87,2.83,7.49.77,7.49c-.89,0-.88,0-.61,1,.22.85.33.92,1.09.69A5.29,5.29,0,0,0,3,8.33c.23-.17.45-.29.49-.26a2,2,0,0,1,.22.63A1.31,1.31,0,0,0,4,9.34a5.62,5.62,0,0,0,2.27-.87L7,8l.13.55c.19.74.32.82,1,.65a7.06,7.06,0,0,0,3.46-2.47l.6-.71-.06.64c-.17,1.63-1.3,3.42-3.39,5.42L6.73,14c-3.21,3.06-3,5.59.6,8a46.77,46.77,0,0,0,4.6,2.41c.28.13,1,.52,1.59.87,3.31,2,4.95,3.92,4.95,5.93a2.49,2.49,0,0,0,.07.77h0c.09.09,0,.1.9-.14a2.61,2.61,0,0,0,.83-.32,3.69,3.69,0,0,0-.55-1.83A11.14,11.14,0,0,0,17,26.81a35.7,35.7,0,0,0-5.1-2.91C9.37,22.64,8.38,22,7.52,21.17a3.53,3.53,0,0,1-1.18-2.48c0-1.38.71-2.58,2.5-4.23,2.84-2.6,3.92-3.91,4.67-5.65a3.64,3.64,0,0,0,.42-2A3.37,3.37,0,0,0,13.61,5l-.32-.74.29-.48c.17-.27.37-.63.46-.8l.15-.3.44.64a5.92,5.92,0,0,1,1,2.81,5.86,5.86,0,0,1-.42,1.94c0,.12-.12.3-.15.4a9.49,9.49,0,0,1-.67,1.1,28,28,0,0,1-4,4.29C8.62,15.49,8.05,16.44,8,17.78a3.28,3.28,0,0,0,1.11,2.76c.95,1,2.07,1.74,5.25,3.32,3.64,1.82,5.22,2.9,6.41,4.38A4.78,4.78,0,0,1,21.94,31a3.21,3.21,0,0,0,.14.92,1.06,1.06,0,0,0,.43-.05l.83-.22.46-.12-.06-.46c-.21-1.53-1.62-3.25-3.94-4.8a37.57,37.57,0,0,0-5.22-2.82A13.36,13.36,0,0,1,11,21.19a3.36,3.36,0,0,1-.8-4.19c.41-.85.83-1.31,3.77-4.15,2.39-2.31,3.43-4.13,3.43-6a5.85,5.85,0,0,0-2.08-4.29c-.23-.21-.44-.43-.65-.65A2.5,2.5,0,0,1,15.27.69a10.6,10.6,0,0,1,2.91,2.78A4.16,4.16,0,0,1,19,6.16a4.91,4.91,0,0,1-.87,3c-.71,1.22-1.26,1.82-4.27,4.67a9.47,9.47,0,0,0-2.07,2.6,2.76,2.76,0,0,0-.33,1.54,2.76,2.76,0,0,0,.29,1.47c.57,1.21,2.23,2.55,4.65,3.73a32.41,32.41,0,0,1,5.82,3.24c2.16,1.6,3.2,3.16,3.2,4.8a1.94,1.94,0,0,0,.09.76,4.54,4.54,0,0,0,1.66-.4C27.29,31.42,27.29,31.37,27.14,30.86ZM6.1,7h0a3.77,3.77,0,0,1-1.46.45L4,7.51l.68-.83a25.09,25.09,0,0,0,3-4.82A12,12,0,0,1,8.28.76c.11-.12.77.32,1.53,1l.63.58-.57.84A10.34,10.34,0,0,1,6.1,7Zm5.71-1.78A9.77,9.77,0,0,1,9.24,7.18h0a5.25,5.25,0,0,1-1.17.28l-.58,0,.65-.78a21.29,21.29,0,0,0,2.1-3.12c.22-.41.42-.76.44-.79s.5.43.9,1.24L12,5ZM13.41,3a2.84,2.84,0,0,1-.45.64,11,11,0,0,1-.9-.91l-.84-.9.19-.45c.34-.79.39-.8,1-.31A9.4,9.4,0,0,1,13.8,2.33q-.18.34-.39.69Z" />
|
|
</svg>
|
|
<div class="app-name">
|
|
<div class="app-name__company">RanchiMall</div>
|
|
<h4 class="app-name__title">Solana Wallet</h4>
|
|
</div>
|
|
</div>
|
|
<button id="meta_mask_status_button" class="button interactive flex align-center hidden"
|
|
data-status="disconnected" onclick="connectToMetaMask()">
|
|
<div class="icon-wrapper">
|
|
<svg class="icon" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" id="Layer_1" x="0" y="0"
|
|
version="1.1" viewBox="0 0 318.6 318.6">
|
|
<style>
|
|
.st1,
|
|
.st6 {
|
|
fill: #e4761b;
|
|
stroke: #e4761b;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.st6 {
|
|
fill: #f6851b;
|
|
stroke: #f6851b;
|
|
}
|
|
</style>
|
|
<path fill="#e2761b" stroke="#e2761b" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m274.1 35.5-99.5 73.9L193 65.8z" />
|
|
<path
|
|
d="m44.4 35.5 98.7 74.6-17.5-44.3zm193.9 171.3-26.5 40.6 56.7 15.6 16.3-55.3zm-204.4.9L50.1 263l56.7-15.6-26.5-40.6z"
|
|
class="st1" />
|
|
<path
|
|
d="m103.6 138.2-15.8 23.9 56.3 2.5-2-60.5zm111.3 0-39-34.8-1.3 61.2 56.2-2.5zM106.8 247.4l33.8-16.5-29.2-22.8zm71.1-16.5 33.9 16.5-4.7-39.3z"
|
|
class="st1" />
|
|
<path fill="#d7c1b3" stroke="#d7c1b3" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m211.8 247.4-33.9-16.5 2.7 22.1-.3 9.3zm-105 0 31.5 14.9-.2-9.3 2.5-22.1z" />
|
|
<path fill="#233447" stroke="#233447" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m138.8 193.5-28.2-8.3 19.9-9.1zm40.9 0 8.3-17.4 20 9.1z" />
|
|
<path fill="#cd6116" stroke="#cd6116" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m106.8 247.4 4.8-40.6-31.3.9zM207 206.8l4.8 40.6 26.5-39.7zm23.8-44.7-56.2 2.5 5.2 28.9 8.3-17.4 20 9.1zm-120.2 23.1 20-9.1 8.2 17.4 5.3-28.9-56.3-2.5z" />
|
|
<path fill="#e4751f" stroke="#e4751f" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m87.8 162.1 23.6 46-.8-22.9zm120.3 23.1-1 22.9 23.7-46zm-64-20.6-5.3 28.9 6.6 34.1 1.5-44.9zm30.5 0-2.7 18 1.2 45 6.7-34.1z" />
|
|
<path d="m179.8 193.5-6.7 34.1 4.8 3.3 29.2-22.8 1-22.9zm-69.2-8.3.8 22.9 29.2 22.8 4.8-3.3-6.6-34.1z"
|
|
class="st6" />
|
|
<path fill="#c0ad9e" stroke="#c0ad9e" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m180.3 262.3.3-9.3-2.5-2.2h-37.7l-2.3 2.2.2 9.3-31.5-14.9 11 9 22.3 15.5h38.3l22.4-15.5 11-9z" />
|
|
<path fill="#161616" stroke="#161616" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m177.9 230.9-4.8-3.3h-27.7l-4.8 3.3-2.5 22.1 2.3-2.2h37.7l2.5 2.2z" />
|
|
<path fill="#763d16" stroke="#763d16" stroke-linecap="round" stroke-linejoin="round"
|
|
d="m278.3 114.2 8.5-40.8-12.7-37.9-96.2 71.4 37 31.3 52.3 15.3 11.6-13.5-5-3.6 8-7.3-6.2-4.8 8-6.1zM31.8 73.4l8.5 40.8-5.4 4 8 6.1-6.1 4.8 8 7.3-5 3.6 11.5 13.5 52.3-15.3 37-31.3-96.2-71.4z" />
|
|
<path
|
|
d="m267.2 153.5-52.3-15.3 15.9 23.9-23.7 46 31.2-.4h46.5zm-163.6-15.3-52.3 15.3-17.4 54.2h46.4l31.1.4-23.6-46zm71 26.4 3.3-57.7 15.2-41.1h-67.5l15 41.1 3.5 57.7 1.2 18.2.1 44.8h27.7l.2-44.8z"
|
|
class="st6" />
|
|
</svg>
|
|
</div>
|
|
<div id="meta_mask_status">Disconnected</div>
|
|
</button>
|
|
<div class="flex align-center gap-0-3">
|
|
<sm-select id="currency_selector" class="margin-right-0-5">
|
|
<sm-option value="sol">SOL</sm-option>
|
|
<sm-option value="usd">USD</sm-option>
|
|
<sm-option value="inr">INR</sm-option>
|
|
</sm-select>
|
|
<theme-toggle></theme-toggle>
|
|
</div>
|
|
</header>
|
|
<nav id="main_navbar">
|
|
<ul>
|
|
<li>
|
|
<a class="nav-item nav-item--active interactive" href="#/balance">
|
|
<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="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>
|
|
<span class="nav-item__title"> Balance </span>
|
|
<div class="nav-item__indicator"></div>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="nav-item interactive" href="#/send">
|
|
<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>
|
|
<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">
|
|
</path>
|
|
</svg>
|
|
<span class="nav-item__title"> Send </span>
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a class="nav-item interactive" href="#/create">
|
|
<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>
|
|
<path
|
|
d="M13 7h-2v4H7v2h4v4h2v-4h4v-2h-4V7zm-1-5C6.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>
|
|
<span class="nav-item__title"> Create </span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
<div id="page_container"></div>
|
|
</main>
|
|
<sm-popup id="transaction_result_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 id="transaction_result_popup__content" class="grid gap-2"></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 Solana Address?</h4>
|
|
<p>
|
|
If you have your Solana Seed, enter it here and recover your Solana
|
|
Address.
|
|
</p>
|
|
</div>
|
|
<sm-form>
|
|
<div id="recovered_btc_addr_wrapper" class="hidden">
|
|
<h5>Recovered Solana Address</h5>
|
|
<sm-copy id="recovered_btc_addr"></sm-copy>
|
|
</div>
|
|
<sm-input id="retrieve_btc_addr_field" type="password" placeholder="Solana Seed" class="password-field"
|
|
data-seed-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="retrieveSolanaAddr()">
|
|
Recover
|
|
</button>
|
|
</sm-form>
|
|
</section>
|
|
</sm-popup>
|
|
<script>
|
|
/* Constants for FLO blockchain operations !!Make sure to add this at beginning!! */
|
|
const floGlobals = {
|
|
blockchain: "FLO",
|
|
tokenURL: "https://ranchimallflo.ranchimall.net/",
|
|
expirationDays: 60,
|
|
};
|
|
</script>
|
|
<script src="https://unpkg.com/uhtml@3.0.1/es.js"></script>
|
|
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>
|
|
<script src="scripts/components.min.js" type="text/javascript"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bs58@5.0.0/index.min.js"></script>
|
|
<script src="scripts/btcwallet_scripts_lib.js" type="text/javascript"></script>
|
|
|
|
<!-- ethers.js version 5.6 -->
|
|
|
|
<script
|
|
src="https://cdn.jsdelivr.net/gh/ethereumjs/browser-builds/dist/ethereumjs-tx/ethereumjs-tx-1.3.3.min.js"></script>
|
|
<script src="scripts/btcOperator.js" type="text/javascript"></script>
|
|
<script src="scripts/floCrypto.js" type="text/javascript"></script>
|
|
<script src="scripts/floSolana.js" type="text/javascript"></script>
|
|
<script src="scripts/tap_combined.js" type="text/javascript"></script>
|
|
<script src="scripts/keccak.js" type="text/javascript"></script>
|
|
<script src="scripts/compactIDB.js" type="text/javascript"></script>
|
|
<script src="solanawallet.js" type="text/javascript"></script>
|
|
<script src="scripts/ether.umd.min.js" type="text/javascript"></script>
|
|
<script src="scripts/ethOperator.js" type="text/javascript"></script>
|
|
<script>
|
|
const uiGlobals = {};
|
|
const { html, svg, render: renderElem } = uhtml;
|
|
uiGlobals.connectionErrorNotification = [];
|
|
//Checks for internet connection status
|
|
if (!navigator.onLine)
|
|
uiGlobals.connectionErrorNotification.push(
|
|
notify(
|
|
"There seems to be a problem connecting to the internet, Please check you internet connection.",
|
|
"error"
|
|
)
|
|
);
|
|
window.addEventListener("offline", () => {
|
|
uiGlobals.connectionErrorNotification.push(
|
|
notify(
|
|
"There seems to be a problem connecting to the internet, Please check you internet connection.",
|
|
"error"
|
|
)
|
|
);
|
|
});
|
|
window.addEventListener("online", () => {
|
|
uiGlobals.connectionErrorNotification.forEach((notification) => {
|
|
getRef("notification_drawer").remove(notification);
|
|
});
|
|
notify("We are back online.", "success");
|
|
});
|
|
// Use instead of document.getElementById
|
|
function getRef(elementId) {
|
|
return document.getElementById(elementId);
|
|
}
|
|
let zIndex = 50;
|
|
// function required for popups or modals to appear
|
|
function openPopup(popupId, pinned) {
|
|
if (popupStack.peek() === undefined) {
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape") {
|
|
closePopup();
|
|
}
|
|
});
|
|
}
|
|
zIndex++;
|
|
getRef(popupId).setAttribute("style", `z-index: ${zIndex}`);
|
|
return getRef(popupId).show({ pinned });
|
|
}
|
|
|
|
// hides the popup or modal
|
|
function closePopup(options = {}) {
|
|
if (popupStack.peek() === undefined) return;
|
|
popupStack.peek().popup.hide(options);
|
|
}
|
|
|
|
document.addEventListener("popupopened", async (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;
|
|
}
|
|
});
|
|
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;
|
|
}
|
|
if (mode === "error") {
|
|
console.error(message);
|
|
}
|
|
return getRef("notification_drawer").push(message, {
|
|
icon,
|
|
...options,
|
|
});
|
|
}
|
|
//Function for displaying toast notifications. pass in error for mode param if you want to show an error.
|
|
/**
|
|
@param {string} title - Title of the popup
|
|
@param {object} options - Options for the popup
|
|
@param {string} options.message - Message to be displayed in the popup
|
|
@param {string} options.cancelText - Text for the cancel button
|
|
@param {string} options.confirmText - Text for the confirm button
|
|
@param {boolean} options.danger - If true, confirm button will be red
|
|
*/
|
|
const getConfirmation = (title, options = {}) => {
|
|
return new Promise((resolve) => {
|
|
const {
|
|
message = "",
|
|
cancelText = "Cancel",
|
|
confirmText = "OK",
|
|
danger = false,
|
|
} = options;
|
|
getRef("confirm_title").innerText = title;
|
|
getRef("confirm_message").innerText = message;
|
|
const cancelButton =
|
|
getRef("confirmation_popup").querySelector(".cancel-button");
|
|
const confirmButton =
|
|
getRef("confirmation_popup").querySelector(".confirm-button");
|
|
confirmButton.textContent = confirmText;
|
|
cancelButton.textContent = cancelText;
|
|
if (danger) confirmButton.classList.add("button--danger");
|
|
else confirmButton.classList.remove("button--danger");
|
|
const { opened, closed } = openPopup("confirmation_popup");
|
|
confirmButton.onclick = () => {
|
|
closePopup({ payload: true });
|
|
};
|
|
cancelButton.onclick = () => {
|
|
closePopup();
|
|
};
|
|
closed.then((payload) => {
|
|
confirmButton.onclick = null;
|
|
cancelButton.onclick = null;
|
|
if (payload) resolve(true);
|
|
else resolve(false);
|
|
});
|
|
});
|
|
};
|
|
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();
|
|
};
|
|
}
|
|
function buttonLoader(id, show) {
|
|
const button =
|
|
typeof id === "string" ? document.getElementById(id) : id;
|
|
if (!button) return;
|
|
if (!button.dataset.hasOwnProperty("wasDisabled"))
|
|
button.dataset.wasDisabled = button.disabled;
|
|
const animOptions = {
|
|
duration: 200,
|
|
fill: "forwards",
|
|
easing: "ease",
|
|
};
|
|
if (show) {
|
|
button.disabled = true;
|
|
button.parentNode.append(document.createElement("sm-spinner"));
|
|
button.animate(
|
|
[
|
|
{
|
|
clipPath: "circle(100%)",
|
|
},
|
|
{
|
|
clipPath: "circle(0)",
|
|
},
|
|
],
|
|
animOptions
|
|
);
|
|
} else {
|
|
button.disabled = button.dataset.wasDisabled === "true";
|
|
button.animate(
|
|
[
|
|
{
|
|
clipPath: "circle(0)",
|
|
},
|
|
{
|
|
clipPath: "circle(100%)",
|
|
},
|
|
],
|
|
animOptions
|
|
).onfinish = (e) => {
|
|
button.removeAttribute("data-original-state");
|
|
};
|
|
const potentialTarget = button.parentNode.querySelector("sm-spinner");
|
|
if (potentialTarget) potentialTarget.remove();
|
|
}
|
|
}
|
|
|
|
class Router {
|
|
/**
|
|
* @constructor {object} options - options for the router
|
|
* @param {object} options.routes - routes for the router
|
|
* @param {object} options.state - initial state for the router
|
|
* @param {function} options.routingStart - function to be called before routing
|
|
* @param {function} options.routingEnd - function to be called after routing
|
|
*/
|
|
constructor(options = {}) {
|
|
const { routes = {}, state = {}, routingStart, routingEnd } = options;
|
|
this.routes = routes;
|
|
this.state = state;
|
|
this.routingStart = routingStart;
|
|
this.routingEnd = routingEnd;
|
|
this.lastPage = null;
|
|
window.addEventListener("hashchange", (e) =>
|
|
this.routeTo(window.location.hash)
|
|
);
|
|
}
|
|
/**
|
|
* @param {string} route - route to be added
|
|
* @param {function} callback - function to be called when route is matched
|
|
*/
|
|
addRoute(route, callback) {
|
|
this.routes[route] = callback;
|
|
}
|
|
/**
|
|
* @param {string} route
|
|
*/
|
|
handleRouting = async (page) => {
|
|
if (this.routingStart) {
|
|
this.routingStart(this.state);
|
|
}
|
|
if (this.routes[page]) {
|
|
await this.routes[page](this.state);
|
|
this.lastPage = page;
|
|
} else {
|
|
if (this.routes["404"]) {
|
|
this.routes["404"](this.state);
|
|
} else {
|
|
console.error(
|
|
`No route found for '${page}' and no '404' route is defined.`
|
|
);
|
|
}
|
|
}
|
|
if (this.routingEnd) {
|
|
this.routingEnd(this.state);
|
|
}
|
|
};
|
|
async routeTo(destination) {
|
|
try {
|
|
let page;
|
|
let wildcards = [];
|
|
let params = {};
|
|
let [path, queryString] = destination.split("?");
|
|
if (path.includes("#")) path = path.split("#")[1];
|
|
if (path.includes("/")) [, page, ...wildcards] = path.split("/");
|
|
else page = path;
|
|
this.state = { page, wildcards, lastPage: this.lastPage, params };
|
|
if (queryString) {
|
|
params = new URLSearchParams(queryString);
|
|
this.state.params = Object.fromEntries(params);
|
|
}
|
|
if (document.startViewTransition) {
|
|
document.startViewTransition(async () => {
|
|
await this.handleRouting(page);
|
|
});
|
|
} else {
|
|
// Fallback for browsers that don't support View transition API:
|
|
await this.handleRouting(page);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
<script>
|
|
const assetIcons = {
|
|
sol: `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="#000000">
|
|
<path d="M16 0c8.837 0 16 7.163 16 16s-7.163 16-16 16S0 24.837 0 16 7.163 0 16 0zm8.706 19.517H10.34a.59.59 0 00-.415.17l-2.838 2.815a.291.291 0 00.207.498H21.66a.59.59 0 00.415-.17l2.838-2.816a.291.291 0 00-.207-.497zm-3.046-5.292H7.294l-.068.007a.291.291 0 00-.14.49l2.84 2.816.07.06c.1.07.22.11.344.11h14.366l.068-.007a.291.291 0 00.14-.49l-2.84-2.816-.07-.06a.59.59 0 00-.344-.11zM24.706 9H10.34a.59.59 0 00-.415.17l-2.838 2.816a.291.291 0 00.207.497H21.66a.59.59 0 00.415-.17l2.838-2.815A.291.291 0 0024.706 9z"/>
|
|
</svg>`,
|
|
};
|
|
|
|
window.smCompConfig = {
|
|
"sm-input": [
|
|
{
|
|
selector: "[data-sol-address]",
|
|
customValidation: (value) => {
|
|
if (!value)
|
|
return {
|
|
isValid: false,
|
|
errorText: "Please enter a Solana address",
|
|
};
|
|
return {
|
|
isValid: (() => {
|
|
try {
|
|
new solanaWeb3.PublicKey(value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
})(),
|
|
errorText: `Invalid Solana address.`,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
selector: "[data-private-key]",
|
|
customValidation: (value) => {
|
|
if (!value)
|
|
return {
|
|
isValid: false,
|
|
errorText:
|
|
"Please enter a Solana Seed OR Bitcoin Private Key OR FLO Private Key",
|
|
};
|
|
return {
|
|
isValid: (() => {
|
|
try {
|
|
const secret =
|
|
value.length < 64
|
|
? floSolana.wif2SolanaSecret(value)
|
|
: floSolana.solanaSeed2SolanaSecret(value);
|
|
const secretKey =
|
|
floSolana.solanaSecret2UsableInCode(secret);
|
|
const keypair = solanaWeb3.Keypair.fromSecretKey(
|
|
Uint8Array.from(
|
|
secretKey.toString().split(",").map(Number)
|
|
)
|
|
);
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
})(),
|
|
errorText: `Invalid Solana Seed OR Bitcoin Private Key OR FLO Private Key`,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
selector: "[data-seed-key]",
|
|
customValidation: (value) => {
|
|
if (!value)
|
|
return {
|
|
isValid: false,
|
|
errorText: "Please enter a Solana Seed",
|
|
};
|
|
return {
|
|
isValid: (() => {
|
|
try {
|
|
const secret = floSolana.solanaSeed2SolanaSecret(value);
|
|
const secretKey =
|
|
floSolana.solanaSecret2UsableInCode(secret);
|
|
const keypair = solanaWeb3.Keypair.fromSecretKey(
|
|
Uint8Array.from(
|
|
secretKey.toString().split(",").map(Number)
|
|
)
|
|
);
|
|
return true;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
})(),
|
|
errorText: `Invalid Solana Seed. Please check and try again`,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
selector: "#wallet_address_input",
|
|
customValidation: (value) => {
|
|
if (!value) {
|
|
return {
|
|
isValid: false,
|
|
errorText:
|
|
"Please enter a Solana address or transaction signature",
|
|
};
|
|
}
|
|
|
|
// Valid if it's a valid address or a signature (88-89 characters)
|
|
let isValid = false;
|
|
|
|
try {
|
|
// Check if it's a valid Solana address
|
|
new solanaWeb3.PublicKey(value);
|
|
isValid = true;
|
|
} catch (error) {
|
|
// If not an address, check if it's likely a signature
|
|
if (value.length >= 87 && value.length <= 89) {
|
|
isValid = true;
|
|
} else {
|
|
// Try other formats (BTC/FLO keys)
|
|
try {
|
|
const solonaAdress = floSolana.validationOfWif(value);
|
|
new solanaWeb3.PublicKey(solonaAdress);
|
|
isValid = true;
|
|
} catch (error) {
|
|
isValid = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
isValid,
|
|
errorText: "Invalid Solana address or transaction signature",
|
|
};
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const router = new Router({
|
|
routes: {},
|
|
routingEnd: (state) => {
|
|
const page = state.page;
|
|
const previousTarget =
|
|
getRef("main_navbar").querySelector(".nav-item--active");
|
|
if (previousTarget) {
|
|
previousTarget.classList.remove("nav-item--active");
|
|
previousTarget.querySelector(".nav-item__indicator")?.remove();
|
|
}
|
|
const target = getRef("main_navbar").querySelector(
|
|
`.nav-item[href="#/${page}"]`
|
|
);
|
|
if (target) {
|
|
target.classList.add("nav-item--active");
|
|
target.append(html.node`<div class="nav-item__indicator"></div>`);
|
|
}
|
|
},
|
|
});
|
|
function setMetaMaskStatus(isConnected) {
|
|
if (isConnected) {
|
|
getRef("meta_mask_status").textContent = "Connected";
|
|
getRef("meta_mask_status_button").dataset.status = "connected";
|
|
} else {
|
|
getRef("meta_mask_status").textContent = "Disconnected";
|
|
getRef("meta_mask_status_button").dataset.status = "disconnected";
|
|
}
|
|
}
|
|
// Add after uiGlobals definition or before window.addEventListener("load"...)
|
|
|
|
let showCurrentValue = false;
|
|
let currentCurrency = localStorage.getItem("preferredCurrency") || "sol";
|
|
let exchangeRates = {
|
|
usd: 0,
|
|
inr: 0,
|
|
};
|
|
let historicalRates = {};
|
|
let isHistoricApiAvailable = false;
|
|
|
|
// Function to ensure UI matches the current currency
|
|
function syncCurrencySelector() {
|
|
const selector = getRef("currency_selector");
|
|
if (selector && selector.value !== currentCurrency) {
|
|
// Update the selector value
|
|
selector.value = currentCurrency;
|
|
|
|
// Also update the selected attribute for proper visual indication
|
|
const options = selector.querySelectorAll("sm-option");
|
|
options.forEach((option) => {
|
|
if (option.value === currentCurrency) {
|
|
option.setAttribute("selected", "");
|
|
} else {
|
|
option.removeAttribute("selected");
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Initialize exchange rates if needed
|
|
(async function initExchangeRates() {
|
|
if (
|
|
currentCurrency !== "sol" &&
|
|
(exchangeRates.usd === 0 || exchangeRates.inr === 0)
|
|
) {
|
|
try {
|
|
await fetchExchangeRates();
|
|
} catch (error) {
|
|
console.error("Failed to initialize exchange rates:", error);
|
|
}
|
|
}
|
|
})();
|
|
|
|
// Fetch exchange rates on load
|
|
async function fetchExchangeRates() {
|
|
try {
|
|
const response = await fetch(
|
|
"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd,inr"
|
|
);
|
|
const data = await response.json();
|
|
console.log("Exchange rates data:", data);
|
|
|
|
if (data && data.solana) {
|
|
exchangeRates = {
|
|
usd: parseFloat(data.solana.usd) || 0,
|
|
inr: parseFloat(data.solana.inr) || 0,
|
|
};
|
|
} else {
|
|
console.warn("Using fallback exchange rates");
|
|
exchangeRates = {
|
|
usd: 100, // Fallback rate
|
|
inr: 8000, // Fallback rate
|
|
};
|
|
}
|
|
return exchangeRates;
|
|
} catch (error) {
|
|
console.error("Error fetching exchange rates:", error);
|
|
notify("Failed to fetch currency rates", "error");
|
|
// Fallback to hardcoded values if API fails
|
|
exchangeRates = {
|
|
usd: 100, // Fallback rate
|
|
inr: 8000, // Fallback rate
|
|
};
|
|
isHistoricApiAvailable = false;
|
|
return exchangeRates;
|
|
}
|
|
}
|
|
|
|
async function fetchHistoricalRate(timestamp) {
|
|
if (historicalRates[timestamp]) {
|
|
return historicalRates[timestamp];
|
|
}
|
|
|
|
const cacheKey = `sol-historical-${timestamp}`;
|
|
const cachedData = localStorage.getItem(cacheKey);
|
|
if (cachedData) {
|
|
const parsedData = JSON.parse(cachedData);
|
|
historicalRates[timestamp] = parsedData;
|
|
return parsedData;
|
|
}
|
|
|
|
const today = new Date();
|
|
const todayStart =
|
|
new Date(
|
|
today.getFullYear(),
|
|
today.getMonth(),
|
|
today.getDate()
|
|
).getTime() / 1000;
|
|
const todayEnd = todayStart + 86399;
|
|
|
|
if (timestamp >= todayStart && timestamp <= todayEnd) {
|
|
console.log(
|
|
"Using current rates instead of historical API for today's date"
|
|
);
|
|
return exchangeRates;
|
|
}
|
|
|
|
try {
|
|
const date = new Date(timestamp * 1000);
|
|
const day = date.getDate().toString().padStart(2, "0");
|
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
|
const year = date.getFullYear();
|
|
const formattedDate = `${day}-${month}-${year}`;
|
|
|
|
const response = await fetch(
|
|
`https://api.coingecko.com/api/v3/coins/solana/history?date=${formattedDate}`
|
|
);
|
|
const data = await response.json();
|
|
console.log("Historical rate data:", data);
|
|
|
|
if (data && data.market_data && data.market_data.current_price) {
|
|
const rates = {
|
|
usd: parseFloat(data.market_data.current_price.usd) || 0,
|
|
inr: parseFloat(data.market_data.current_price.inr) || 0,
|
|
};
|
|
|
|
historicalRates[timestamp] = rates;
|
|
localStorage.setItem(cacheKey, JSON.stringify(rates));
|
|
return rates;
|
|
}
|
|
return exchangeRates;
|
|
} catch (error) {
|
|
console.error("Error fetching historical rate:", error);
|
|
return exchangeRates;
|
|
}
|
|
}
|
|
|
|
async function updateCurrency(currency) {
|
|
const previousCurrency = currentCurrency;
|
|
currentCurrency = currency;
|
|
localStorage.setItem("preferredCurrency", currency);
|
|
|
|
if (
|
|
currency !== "sol" &&
|
|
(exchangeRates.usd === 0 || exchangeRates.inr === 0)
|
|
) {
|
|
await fetchExchangeRates();
|
|
}
|
|
|
|
if (currency !== "sol" && previousCurrency === "sol") {
|
|
testHistoricalAPIAndRenderToggle();
|
|
} else if (currency === "sol" && previousCurrency !== "sol") {
|
|
const container = document.querySelector(
|
|
".flex.flex-direction-column.gap-0-5.sticky.top-0"
|
|
);
|
|
if (container) {
|
|
const existingToggle =
|
|
container.querySelector("sm-switch")?.parentElement;
|
|
if (existingToggle) existingToggle.remove();
|
|
}
|
|
}
|
|
|
|
updateTransactionDetailsToggle();
|
|
convertAllDisplayedCurrency();
|
|
syncCurrencySelector();
|
|
}
|
|
|
|
function handleValuationTypeChange(isCurrentValue) {
|
|
if (isCurrentValue && isCurrentValue.target) {
|
|
isCurrentValue = isCurrentValue.target.checked;
|
|
}
|
|
showCurrentValue = isCurrentValue;
|
|
localStorage.setItem("sol-wallet-show-current-value", isCurrentValue);
|
|
convertAllDisplayedCurrency();
|
|
}
|
|
|
|
async function convertAllDisplayedCurrency() {
|
|
const currency = currentCurrency;
|
|
const elements = document.querySelectorAll(".sol-value");
|
|
|
|
for (const el of elements) {
|
|
const solAmount = parseFloat(el.dataset.sol || "0");
|
|
const timestamp = el.dataset.timestamp;
|
|
|
|
let formattedValue;
|
|
if (currency === "sol") {
|
|
formattedValue = formatCurrency(solAmount);
|
|
} else {
|
|
formattedValue = await getConvertedAmount(solAmount, timestamp);
|
|
}
|
|
|
|
el.textContent = formattedValue;
|
|
}
|
|
}
|
|
|
|
async function getConvertedAmount(amountInSol, timestamp) {
|
|
if (!amountInSol) return "0 SOL";
|
|
|
|
const value = Number(amountInSol);
|
|
if (isNaN(value)) return amountInSol;
|
|
|
|
if (currentCurrency === "sol") {
|
|
return `${value} SOL`;
|
|
} else {
|
|
let rates = exchangeRates;
|
|
|
|
if (!showCurrentValue && timestamp) {
|
|
try {
|
|
const historicalRates = await fetchHistoricalRate(timestamp);
|
|
if (historicalRates) {
|
|
rates = historicalRates;
|
|
}
|
|
} catch (error) {
|
|
console.error(
|
|
"Error getting historical rates, using current rates:",
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
if (rates && currentCurrency === "usd" && rates.usd) {
|
|
return `${(value * rates.usd).toFixed(2)} USD`;
|
|
} else if (rates && currentCurrency === "inr" && rates.inr) {
|
|
return `${(value * rates.inr).toFixed(2)} INR`;
|
|
}
|
|
return `${value} SOL`;
|
|
}
|
|
}
|
|
|
|
function formatCurrency(amountInSol) {
|
|
if (!amountInSol) return "0 SOL";
|
|
|
|
const value = Number(amountInSol);
|
|
if (isNaN(value)) return amountInSol;
|
|
|
|
if (currentCurrency === "sol") {
|
|
return `${value} SOL`;
|
|
} else if (currentCurrency === "usd") {
|
|
return `${(value * exchangeRates.usd).toFixed(2)} USD`;
|
|
} else if (currentCurrency === "inr") {
|
|
return `${(value * exchangeRates.inr).toFixed(2)} INR`;
|
|
}
|
|
|
|
return `${value} SOL`;
|
|
}
|
|
let currentWalletAddress = "";
|
|
|
|
function testHistoricalAPIAndRenderToggle() {
|
|
// Get date from a week ago for testing historical data
|
|
const oneWeekAgo = Math.floor(Date.now() / 1000) - 7 * 24 * 60 * 60;
|
|
|
|
fetchHistoricalRate(oneWeekAgo)
|
|
.then((rates) => {
|
|
if (rates && (rates.usd > 0 || rates.inr > 0)) {
|
|
isHistoricApiAvailable = true;
|
|
renderValuationToggle();
|
|
} else {
|
|
isHistoricApiAvailable = false;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
isHistoricApiAvailable = false;
|
|
});
|
|
}
|
|
|
|
function renderValuationToggle() {
|
|
if (isHistoricApiAvailable && currentCurrency !== "sol") {
|
|
const container = document.querySelector(
|
|
".flex.flex-direction-column.gap-0-5.sticky.top-0"
|
|
);
|
|
if (container) {
|
|
const existingToggle = container.querySelector("sm-switch");
|
|
if (!existingToggle) {
|
|
const toggleDiv = document.createElement("div");
|
|
toggleDiv.className = "margin-left-auto";
|
|
toggleDiv.innerHTML = `
|
|
<sm-switch id="valuation_toggle" ${showCurrentValue
|
|
? "checked"
|
|
: ""
|
|
}>
|
|
<p slot="left" class="margin-right-0-5">
|
|
Show current value
|
|
</p>
|
|
</sm-switch>
|
|
`;
|
|
container.appendChild(toggleDiv);
|
|
|
|
// Add event handler to the new toggle
|
|
document
|
|
.getElementById("valuation_toggle")
|
|
.addEventListener("change", (e) => {
|
|
handleValuationTypeChange(e.target.checked);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateTransactionDetailsToggle() {
|
|
const txDetailsPage = document.querySelector("[data-page='tdx']");
|
|
if (!txDetailsPage) return;
|
|
|
|
const toggleContainer = document.querySelector(".tx-header");
|
|
if (!toggleContainer) return;
|
|
|
|
const existingToggle =
|
|
toggleContainer.querySelector(".margin-left-auto");
|
|
|
|
if (currentCurrency === "sol") {
|
|
if (existingToggle) existingToggle.remove();
|
|
return;
|
|
}
|
|
|
|
if (
|
|
currentCurrency !== "sol" &&
|
|
isHistoricApiAvailable &&
|
|
!existingToggle
|
|
) {
|
|
const toggleDiv = document.createElement("div");
|
|
toggleDiv.className = "margin-left-auto";
|
|
toggleDiv.innerHTML = `
|
|
<sm-switch id="tx_valuation_toggle" ${showCurrentValue ? "checked" : ""
|
|
}>
|
|
<p slot="left" class="margin-right-0-5">
|
|
Show current value
|
|
</p>
|
|
</sm-switch>
|
|
`;
|
|
toggleContainer.appendChild(toggleDiv);
|
|
|
|
const toggle = toggleDiv.querySelector("sm-switch");
|
|
toggle.addEventListener("change", async (e) => {
|
|
showCurrentValue = e.target.checked;
|
|
localStorage.setItem(
|
|
"sol-wallet-show-current-value",
|
|
showCurrentValue
|
|
);
|
|
|
|
const valueElement = getRef("tx_value");
|
|
const feeElement = getRef("tx_fee");
|
|
|
|
if (valueElement && valueElement.dataset.sol) {
|
|
const solAmount = parseFloat(valueElement.dataset.sol);
|
|
const timestamp = valueElement.dataset.timestamp;
|
|
valueElement.textContent = await getConvertedAmount(
|
|
solAmount,
|
|
timestamp
|
|
);
|
|
}
|
|
|
|
if (feeElement && feeElement.dataset.sol) {
|
|
const solFee = parseFloat(feeElement.dataset.sol);
|
|
const timestamp = feeElement.dataset.timestamp;
|
|
feeElement.textContent = await getConvertedAmount(
|
|
solFee,
|
|
timestamp
|
|
);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
window.addEventListener("load", () => {
|
|
router.routeTo(location.hash);
|
|
document.body.classList.remove("hidden");
|
|
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"));
|
|
}
|
|
});
|
|
compactIDB
|
|
.initDB("solanaWallet", {
|
|
solanaContacts: {},
|
|
})
|
|
.then((result) => {
|
|
console.log(result);
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
});
|
|
// connectToMetaMask().then(() => {
|
|
if (window.ethereum) {
|
|
// setMetaMaskStatus(window.ethereum.isConnected())
|
|
window.ethereum.on("chainChanged", (chainId) => {
|
|
window.currentChainId = chainId;
|
|
if (chainId !== "0x1") {
|
|
renderError("Please switch MetaMask to Ethereum Mainnet");
|
|
} else {
|
|
router.routeTo(location.hash);
|
|
}
|
|
});
|
|
window.ethereum
|
|
.request({
|
|
method: "eth_chainId",
|
|
})
|
|
.then((chainId) => {
|
|
window.currentChainId = chainId;
|
|
if (chainId !== "0x1") {
|
|
renderError("Please switch MetaMask to Ethereum Mainnet");
|
|
} else {
|
|
router.routeTo(location.hash);
|
|
}
|
|
});
|
|
}
|
|
// }).catch((error) => {
|
|
// setMetaMaskStatus(false)
|
|
// if (error.code === 4001) {
|
|
// // EIP-1193 userRejectedRequest error
|
|
// notify('Please connect to MetaMask to continue', 'error')
|
|
// } else {
|
|
// if (error === 'MetaMask not installed') {
|
|
// getRef('balance_section').classList.add('hidden')
|
|
// getRef('error_section').classList.remove('hidden')
|
|
// }
|
|
// else
|
|
// console.error(error)
|
|
// }
|
|
// })
|
|
if (typeof window.ethereum !== "undefined") {
|
|
ethereum.on("accountsChanged", (accounts) => {
|
|
getRef("sol_balance_wrapper").classList.add("hidden");
|
|
setMetaMaskStatus(accounts.length > 0);
|
|
});
|
|
ethereum.on("connect", (accounts) => {
|
|
setMetaMaskStatus(accounts.length > 0);
|
|
});
|
|
ethereum.on("disconnect", (accounts) => {
|
|
setMetaMaskStatus(false);
|
|
});
|
|
}
|
|
|
|
// Add this inside the window.addEventListener("load", ...) function
|
|
|
|
// Initialize currency selector
|
|
const selector = getRef("currency_selector");
|
|
if (selector) {
|
|
const stored = localStorage.getItem("preferredCurrency");
|
|
if (stored) {
|
|
currentCurrency = stored;
|
|
selector.value = stored;
|
|
|
|
const options = selector.querySelectorAll("sm-option");
|
|
options.forEach((option) => {
|
|
if (option.value === stored) {
|
|
option.setAttribute("selected", "");
|
|
} else {
|
|
option.removeAttribute("selected");
|
|
}
|
|
});
|
|
|
|
showCurrentValue =
|
|
localStorage.getItem("sol-wallet-show-current-value") === "true";
|
|
|
|
if (
|
|
currentCurrency !== "sol" &&
|
|
(exchangeRates.usd === 0 || exchangeRates.inr === 0)
|
|
) {
|
|
fetchExchangeRates().then(() => {
|
|
convertAllDisplayedCurrency();
|
|
testHistoricalAPIAndRenderToggle();
|
|
});
|
|
} else {
|
|
if (currentCurrency !== "sol") {
|
|
testHistoricalAPIAndRenderToggle();
|
|
}
|
|
}
|
|
}
|
|
|
|
selector.addEventListener("change", (e) => {
|
|
updateCurrency(e.target.value);
|
|
});
|
|
}
|
|
});
|
|
router.addRoute("404", () => {
|
|
renderElem(getRef("page_container"), html` <h1>Page not found</h1> `);
|
|
});
|
|
router.addRoute("", renderHome);
|
|
router.addRoute("balance", renderHome);
|
|
router.addRoute("tdx", renderTdx);
|
|
|
|
class LazyLoader {
|
|
constructor(container, elementsToRender, renderFn, options = {}) {
|
|
const { pageSize = 10 } = options;
|
|
|
|
this.container = document.querySelector(container);
|
|
this.elementsToRender = elementsToRender;
|
|
this.renderFn = renderFn;
|
|
this.pageSize = pageSize;
|
|
this.currentPage = 1;
|
|
this.totalPages = Math.ceil(elementsToRender.length / pageSize);
|
|
}
|
|
|
|
init() {
|
|
this.container.innerHTML = "";
|
|
this.renderCurrentPage();
|
|
this.renderPagination();
|
|
}
|
|
|
|
async update(newElements) {
|
|
this.elementsToRender = newElements;
|
|
this.currentPage = 1;
|
|
currentPage = 1; // Sync global state
|
|
this.totalPages = Math.ceil(newElements.length / this.pageSize);
|
|
await this.renderCurrentPage();
|
|
this.renderPagination();
|
|
}
|
|
|
|
async renderCurrentPage() {
|
|
try {
|
|
// Show loading spinner
|
|
this.container.innerHTML =
|
|
'<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
|
|
|
|
const start = (this.currentPage - 1) * this.pageSize;
|
|
const end = start + this.pageSize;
|
|
|
|
if (
|
|
!this.elementsToRender ||
|
|
!Array.isArray(this.elementsToRender) ||
|
|
this.elementsToRender.length === 0
|
|
) {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No transactions to display</p>';
|
|
return;
|
|
}
|
|
|
|
const currentElements = this.elementsToRender.slice(start, end);
|
|
const renderedElements = [];
|
|
|
|
// Process elements concurrently with error handling for each
|
|
await Promise.all(
|
|
currentElements.map(async (element, index) => {
|
|
try {
|
|
const renderedCard = await this.renderFn(element);
|
|
if (renderedCard && renderedCard instanceof Element) {
|
|
renderedElements[index] = renderedCard;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error rendering element:", error);
|
|
renderedElements[index] = null;
|
|
}
|
|
})
|
|
);
|
|
|
|
// Clear container and add all successfully rendered elements
|
|
this.container.innerHTML = "";
|
|
const validElements = renderedElements.filter(Boolean);
|
|
|
|
if (validElements.length > 0) {
|
|
validElements.forEach((element) => {
|
|
if (element) this.container.appendChild(element);
|
|
});
|
|
} else {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No transactions to display</p>';
|
|
}
|
|
} catch (err) {
|
|
console.error("Error in renderCurrentPage:", err);
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">Error loading transactions</p>';
|
|
notify("Failed to display transactions", "error");
|
|
}
|
|
}
|
|
renderPagination() {
|
|
const existingPagination = document.querySelector(
|
|
".pagination-controls"
|
|
);
|
|
if (existingPagination) {
|
|
existingPagination.remove();
|
|
}
|
|
|
|
// Pagination controls
|
|
const paginationDiv = document.createElement("div");
|
|
paginationDiv.className = "pagination-controls";
|
|
paginationDiv.style.cssText = `
|
|
width: 100%;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.75rem;
|
|
border-radius: 0.5rem;
|
|
background-color: rgba(var(--foreground-color), 1);
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
|
border: 1px solid rgba(var(--text-color), 0.1);
|
|
`;
|
|
|
|
// Previous button
|
|
const prevButton = document.createElement("button");
|
|
prevButton.className = "button";
|
|
prevButton.textContent = "Previous";
|
|
prevButton.disabled = this.currentPage === 1;
|
|
prevButton.onclick = () => this.fetchPreviousPage();
|
|
|
|
// Next button
|
|
const nextButton = document.createElement("button");
|
|
nextButton.className = "button";
|
|
nextButton.textContent = "Next";
|
|
nextButton.disabled =
|
|
!hasMoreTransactions && this.currentPage === this.totalPages;
|
|
nextButton.onclick = () => this.fetchNextPage();
|
|
nextButton.id = "next_page_button";
|
|
|
|
// Page info
|
|
const pageInfo = document.createElement("span");
|
|
pageInfo.textContent = `Page ${this.currentPage}`;
|
|
pageInfo.id = "page_info";
|
|
|
|
paginationDiv.appendChild(prevButton);
|
|
paginationDiv.appendChild(pageInfo);
|
|
paginationDiv.appendChild(nextButton);
|
|
|
|
// Add pagination controls after the container
|
|
this.container.parentNode.insertBefore(
|
|
paginationDiv,
|
|
this.container.nextSibling
|
|
);
|
|
}
|
|
|
|
async fetchNextPage() {
|
|
try {
|
|
const targetPage = this.currentPage + 1;
|
|
console.log(`Going to next page: ${targetPage}`);
|
|
|
|
// Check if we have cached data for this page
|
|
if (transactionPageCache.has(targetPage)) {
|
|
console.log(`Using cached data for page ${targetPage}`);
|
|
const cachedPage = transactionPageCache.get(targetPage);
|
|
|
|
// Apply current filter
|
|
const filter =
|
|
document.getElementById("filter_selector")?.value || "all";
|
|
let filteredTransactions = cachedPage.formatted;
|
|
|
|
if (filter === "sent") {
|
|
filteredTransactions = cachedPage.formatted.filter(
|
|
(tx) => tx.type === "out"
|
|
);
|
|
} else if (filter === "received") {
|
|
filteredTransactions = cachedPage.formatted.filter(
|
|
(tx) => tx.type === "in"
|
|
);
|
|
}
|
|
|
|
// Clear container and render cached transactions
|
|
this.container.innerHTML = "";
|
|
|
|
if (filteredTransactions.length > 0) {
|
|
for (const tx of filteredTransactions) {
|
|
try {
|
|
const card = await this.renderFn(tx);
|
|
if (card && card instanceof Element) {
|
|
this.container.appendChild(card);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error rendering transaction card:", error);
|
|
}
|
|
}
|
|
} else {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No transactions found for this filter</p>';
|
|
}
|
|
|
|
// Update page state
|
|
this.currentPage = targetPage;
|
|
currentPage = targetPage; // Sync global state
|
|
|
|
// Update page indicator
|
|
const pageInfo = document.getElementById("page_info");
|
|
if (pageInfo) pageInfo.textContent = `Page ${this.currentPage}`;
|
|
|
|
// Update button states
|
|
const prevButton = document.querySelector(
|
|
".pagination-controls button:first-child"
|
|
);
|
|
if (prevButton) prevButton.disabled = this.currentPage <= 1;
|
|
|
|
const nextButton = document.getElementById("next_page_button");
|
|
if (nextButton) nextButton.disabled = !hasMoreTransactions && this.currentPage >= transactionPageCache.size;
|
|
|
|
return;
|
|
}
|
|
|
|
// If not in cache, fetch new transactions from blockchain
|
|
if (!hasMoreTransactions) return;
|
|
|
|
// Show loading state
|
|
this.container.innerHTML =
|
|
'<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
|
|
|
|
const addressInput = currentWalletAddress;
|
|
if (!addressInput) {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No address input found</p>';
|
|
return;
|
|
}
|
|
|
|
if (!lastFetchedSignature) {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No more transactions to load</p>';
|
|
hasMoreTransactions = false;
|
|
return;
|
|
}
|
|
|
|
// Get next batch of signatures using the last signature from previous batch
|
|
const newSignatures = await getSolanaTransactionHistory(
|
|
addressInput,
|
|
lastFetchedSignature,
|
|
false
|
|
);
|
|
|
|
if (!newSignatures || newSignatures.length === 0) {
|
|
hasMoreTransactions = false;
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No more transactions to load</p>';
|
|
const nextBtn = document.getElementById("next_page_button");
|
|
if (nextBtn) nextBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
// Process the new transactions
|
|
const newTransactions = await processTransactions(newSignatures);
|
|
|
|
// Format
|
|
const formattedTxsBatch = newTransactions.map((tx) => {
|
|
return formatTransaction(addressInput, tx);
|
|
});
|
|
|
|
// Save this page for instant navigation
|
|
transactionPageCache.set(targetPage, {
|
|
signatures: newSignatures,
|
|
transactions: newTransactions,
|
|
formatted: formattedTxsBatch
|
|
});
|
|
|
|
// Apply current filter
|
|
const filterSelector = document.getElementById("filter_selector");
|
|
const filter = filterSelector ? filterSelector.value : "all";
|
|
let filteredTransactions = formattedTxsBatch;
|
|
|
|
if (filter === "sent") {
|
|
filteredTransactions = formattedTxsBatch.filter(
|
|
(tx) => tx.type === "out"
|
|
);
|
|
} else if (filter === "received") {
|
|
filteredTransactions = formattedTxsBatch.filter(
|
|
(tx) => tx.type === "in"
|
|
);
|
|
}
|
|
|
|
// Render
|
|
this.container.innerHTML = "";
|
|
if (filteredTransactions.length > 0) {
|
|
for (const tx of filteredTransactions) {
|
|
try {
|
|
const card = await this.renderFn(tx);
|
|
if (card) this.container.appendChild(card);
|
|
} catch (error) {
|
|
console.error("Error rendering transaction card:", error);
|
|
}
|
|
}
|
|
} else {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No transactions found for this filter</p>';
|
|
}
|
|
|
|
this.currentPage = targetPage;
|
|
currentPage = targetPage; // Sync global state
|
|
|
|
// Update UI
|
|
const pageInfo = document.getElementById("page_info");
|
|
if (pageInfo) pageInfo.textContent = `Page ${this.currentPage}`;
|
|
|
|
const prevBtn = document.querySelector(
|
|
".pagination-controls button:first-child"
|
|
);
|
|
if (prevBtn) prevBtn.disabled = false;
|
|
|
|
const nextBtn = document.getElementById("next_page_button");
|
|
if (nextBtn) nextBtn.disabled = !hasMoreTransactions;
|
|
|
|
} catch (error) {
|
|
console.error("Error fetching next page:", error, error.stack);
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">Error loading transactions</p>';
|
|
}
|
|
}
|
|
async fetchPreviousPage() {
|
|
try {
|
|
// Can't go back if we're already on page 1
|
|
if (currentPage <= 1) {
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
this.container.innerHTML =
|
|
'<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
|
|
|
|
const address = currentWalletAddress;
|
|
if (!address) {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No address specified</p>';
|
|
return;
|
|
}
|
|
|
|
const targetPage = this.currentPage - 1;
|
|
console.log(`Going to previous page: ${targetPage}`);
|
|
|
|
// Check if we have cached data for this page
|
|
if (transactionPageCache.has(targetPage)) {
|
|
console.log(`Using cached data for page ${targetPage}`);
|
|
const cachedPage = transactionPageCache.get(targetPage);
|
|
|
|
// Apply current filter
|
|
const filter =
|
|
document.getElementById("filter_selector")?.value || "all";
|
|
let filteredTransactions = cachedPage.formatted;
|
|
|
|
if (filter === "sent") {
|
|
filteredTransactions = cachedPage.formatted.filter(
|
|
(tx) => tx.type === "out"
|
|
);
|
|
} else if (filter === "received") {
|
|
filteredTransactions = cachedPage.formatted.filter(
|
|
(tx) => tx.type === "in"
|
|
);
|
|
}
|
|
|
|
// Clear container and render cached transactions
|
|
this.container.innerHTML = "";
|
|
|
|
if (filteredTransactions.length > 0) {
|
|
for (const tx of filteredTransactions) {
|
|
try {
|
|
const card = await this.renderFn(tx);
|
|
if (card && card instanceof Element) {
|
|
this.container.appendChild(card);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error rendering transaction card:", error);
|
|
}
|
|
}
|
|
} else {
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">No transactions found for this filter</p>';
|
|
}
|
|
|
|
// Update page state
|
|
this.currentPage = targetPage;
|
|
currentPage = targetPage; // Sync global state
|
|
|
|
// Update page indicator
|
|
const pageInfo = document.getElementById("page_info");
|
|
if (pageInfo) pageInfo.textContent = `Page ${this.currentPage}`;
|
|
|
|
// Update button states
|
|
const prevButton = document.querySelector(
|
|
".pagination-controls button:first-child"
|
|
);
|
|
if (prevButton) prevButton.disabled = this.currentPage <= 1;
|
|
|
|
const nextButton = document.getElementById("next_page_button");
|
|
if (nextButton) nextButton.disabled = false; // Can always go forward from cached page
|
|
|
|
return;
|
|
}
|
|
|
|
// If not cached (shouldn't happen in normal flow), show error
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">Page data not available. Please refresh.</p>';
|
|
console.warn(`Page ${targetPage} not found in cache`);
|
|
} catch (error) {
|
|
console.error("Error fetching previous page:", error);
|
|
this.container.innerHTML =
|
|
'<p class="text-center margin-top-2">Error loading transactions</p>';
|
|
}
|
|
}
|
|
}
|
|
function formatTransaction(address, tx) {
|
|
try {
|
|
if (!tx) {
|
|
console.warn("Transaction object is undefined");
|
|
return {
|
|
txid: "unknown",
|
|
time: "Unknown",
|
|
originalTimestamp: 0,
|
|
block: -1,
|
|
type: "unknown",
|
|
amount: 0,
|
|
sender: "N/A",
|
|
receiver: "N/A",
|
|
address: address,
|
|
};
|
|
}
|
|
|
|
const amount = calculateTransactionAmount(tx, address);
|
|
|
|
// Extract from and to addresses - prioritize parsed instructions
|
|
let fromAddress = "N/A";
|
|
let toAddress = "N/A";
|
|
|
|
// First, try to get from parsed instructions (most reliable)
|
|
if (tx?.transaction?.message?.instructions) {
|
|
for (const ix of tx.transaction.message.instructions) {
|
|
if (ix.parsed && ix.parsed.info) {
|
|
// System transfer
|
|
if (ix.parsed.type === "transfer" && ix.parsed.info.source && ix.parsed.info.destination) {
|
|
fromAddress = ix.parsed.info.source;
|
|
toAddress = ix.parsed.info.destination;
|
|
break;
|
|
}
|
|
// Token transfer
|
|
if (ix.parsed.info.authority && ix.parsed.info.destination) {
|
|
fromAddress = ix.parsed.info.authority;
|
|
toAddress = ix.parsed.info.destination;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: use account keys if parsed instructions not available
|
|
if (fromAddress === "N/A" && tx?.transaction?.message?.accountKeys) {
|
|
const keys = tx.transaction.message.accountKeys;
|
|
fromAddress = keys[0]?.pubkey?.toString() || String(keys[0]) || "N/A";
|
|
toAddress = keys[1]?.pubkey?.toString() || String(keys[1]) || "N/A";
|
|
}
|
|
|
|
// Add more comprehensive null check before accessing signatures
|
|
let txid = "unknown";
|
|
try {
|
|
if (
|
|
tx?.transaction?.signatures &&
|
|
Array.isArray(tx.transaction.signatures) &&
|
|
tx.transaction.signatures.length > 0
|
|
) {
|
|
txid = tx.transaction.signatures[0];
|
|
} else if (tx?.signature) {
|
|
// Some transactions might have signature at the top level
|
|
txid = tx.signature;
|
|
} else if (
|
|
tx?.signatures &&
|
|
Array.isArray(tx.signatures) &&
|
|
tx.signatures.length > 0
|
|
) {
|
|
// Or in a different structure
|
|
txid = tx.signatures[0];
|
|
}
|
|
} catch (e) {
|
|
console.warn("Error accessing transaction signature:", e);
|
|
}
|
|
txid = String(txid);
|
|
|
|
// Detect transaction type
|
|
let transactionType = "transfer";
|
|
const logMessages = tx.meta?.logMessages || [];
|
|
|
|
// Check for NFT mint
|
|
const isNFTMint = logMessages.some(log =>
|
|
log.includes("MintToCollectionV1") ||
|
|
log.includes("MintV1") ||
|
|
log.includes("Bubblegum") ||
|
|
(log.includes("Instruction: Mint") && !log.includes("MintTo") && !log.includes("JUP"))
|
|
);
|
|
|
|
if (isNFTMint) {
|
|
transactionType = "nft_mint";
|
|
}
|
|
// Check for token swap
|
|
else if (tx.meta?.preTokenBalances && tx.meta?.postTokenBalances) {
|
|
const preTokens = tx.meta.preTokenBalances;
|
|
const postTokens = tx.meta.postTokenBalances;
|
|
|
|
let hasTokenSent = false;
|
|
let hasTokenReceived = false;
|
|
|
|
for (const preTok of preTokens) {
|
|
const postTok = postTokens.find(p => p.accountIndex === preTok.accountIndex);
|
|
if (postTok) {
|
|
const preAmount = parseFloat(preTok.uiTokenAmount.amount);
|
|
const postAmount = parseFloat(postTok.uiTokenAmount.amount);
|
|
|
|
if (preAmount > postAmount && (preAmount - postAmount) > 0.000001) {
|
|
hasTokenSent = true;
|
|
}
|
|
if (postAmount > preAmount && (postAmount - preAmount) > 0.000001) {
|
|
hasTokenReceived = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasTokenSent && hasTokenReceived) {
|
|
transactionType = "swap";
|
|
}
|
|
}
|
|
|
|
return {
|
|
txid: txid,
|
|
time: tx.blockTime ? getFormattedTime(tx.blockTime) : "Unknown",
|
|
originalTimestamp: tx.blockTime || 0,
|
|
block: tx.slot || -1,
|
|
type: amount > 0 ? "in" : "out",
|
|
amount: Math.abs(amount),
|
|
sender: fromAddress,
|
|
receiver: toAddress,
|
|
address: address,
|
|
transactionType: transactionType,
|
|
typeLabel: transactionType === "swap" ? "Swap" : transactionType === "nft_mint" ? "NFT Mint" : "Transfer",
|
|
|
|
};
|
|
} catch (err) {
|
|
console.error("Error in formatTransaction:", err);
|
|
return {
|
|
txid: "error",
|
|
time: "Error",
|
|
originalTimestamp: 0,
|
|
block: -1,
|
|
type: "error",
|
|
amount: 0,
|
|
sender: "Error",
|
|
receiver: "Error",
|
|
address: address,
|
|
};
|
|
}
|
|
}
|
|
|
|
let transactionsLazyLoader;
|
|
function calculateTransactionAmount(tx, address) {
|
|
try {
|
|
if (
|
|
!tx ||
|
|
!tx.meta ||
|
|
!tx.meta.postBalances ||
|
|
!tx.meta.preBalances
|
|
) {
|
|
console.warn("Transaction missing balance data:", tx?.signature);
|
|
return 0;
|
|
}
|
|
|
|
// Find the index of the address in the accounts array
|
|
let addressIndex = 0;
|
|
if (address && tx.transaction?.message?.accountKeys) {
|
|
const accountKeys = tx.transaction.message.accountKeys;
|
|
for (let i = 0; i < accountKeys.length; i++) {
|
|
const accountKey =
|
|
accountKeys[i]?.pubkey?.toString() ||
|
|
accountKeys[i]?.toString();
|
|
if (accountKey === address) {
|
|
addressIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const preBalance = tx.meta.preBalances[addressIndex] || 0;
|
|
const postBalance = tx.meta.postBalances[addressIndex] || 0;
|
|
return Number(((postBalance - preBalance) / solanaWeb3.LAMPORTS_PER_SOL).toFixed(9));
|
|
} catch (error) {
|
|
console.error("Error calculating transaction amount:", error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Add render functions for transactions
|
|
const render = {
|
|
async transactions(address) {
|
|
try {
|
|
if (!address) return;
|
|
|
|
getRef("address_transactions").classList.remove("hidden");
|
|
getRef("transactions_list").innerHTML =
|
|
'<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
|
|
|
|
const signatures = await getSolanaTransactionHistory(address);
|
|
console.log("signatures", signatures);
|
|
if (signatures.length === 0) {
|
|
getRef("transactions_list").textContent = "No transactions found";
|
|
return;
|
|
}
|
|
|
|
// Process transactions and cache them
|
|
cachedTransactions = await processTransactions(signatures);
|
|
console.log("transaction", cachedTransactions);
|
|
|
|
if (
|
|
!cachedTransactions ||
|
|
cachedTransactions.error ||
|
|
cachedTransactions.length === 0
|
|
) {
|
|
getRef("transactions_list").textContent =
|
|
"Error fetching transactions";
|
|
return;
|
|
}
|
|
|
|
// Format and cache formatted transactions
|
|
formattedTxs = cachedTransactions.map((tx) =>
|
|
formatTransaction(address, tx)
|
|
);
|
|
|
|
// Save first page and clear old cache when loading new address
|
|
transactionPageCache.clear();
|
|
transactionPageCache.set(1, {
|
|
signatures: signatures,
|
|
transactions: cachedTransactions,
|
|
formatted: formattedTxs
|
|
});
|
|
|
|
// Filter transactions based on selection
|
|
const filter = getRef("filter_selector").value || "all";
|
|
let filteredTransactions = formattedTxs;
|
|
|
|
if (filter === "sent") {
|
|
filteredTransactions = formattedTxs.filter(
|
|
(tx) => tx.type === "out"
|
|
);
|
|
} else if (filter === "received") {
|
|
filteredTransactions = formattedTxs.filter(
|
|
(tx) => tx.type === "in"
|
|
);
|
|
}
|
|
|
|
// Initialize or update the lazy loader with true pagination
|
|
if (transactionsLazyLoader) {
|
|
transactionsLazyLoader.update(filteredTransactions);
|
|
} else {
|
|
transactionsLazyLoader = new LazyLoader(
|
|
"#transactions_list",
|
|
filteredTransactions,
|
|
render.transactionCard,
|
|
{ pageSize: TX_FETCH_LIMIT }
|
|
);
|
|
transactionsLazyLoader.init();
|
|
}
|
|
|
|
|
|
// Enable transaction card click to view details
|
|
const list = getRef("transactions_list");
|
|
list.onclick = (event) => {
|
|
const transactionCard = event.target.closest(".transaction");
|
|
if (transactionCard && event.target === transactionCard) {
|
|
const txid = transactionCard.dataset.txid;
|
|
window.location.hash = `#/tdx/${txid}`;
|
|
}
|
|
};
|
|
|
|
getRef("transactions_list").previousElementSibling.classList.remove(
|
|
"hidden"
|
|
);
|
|
} catch (error) {
|
|
console.error("Error fetching transactions:", error);
|
|
getRef("transactions_list").textContent =
|
|
"Error fetching transactions";
|
|
}
|
|
},
|
|
|
|
async transactionCard(transactionDetails) {
|
|
const transactionCard = document.createElement("li");
|
|
transactionCard.className = `transaction ${transactionDetails.type} ${transactionDetails.block < 0 ? "unconfirmed-tx" : ""
|
|
}`;
|
|
transactionCard.dataset.txid = transactionDetails.txid;
|
|
transactionCard.style.cursor = "pointer";
|
|
|
|
// Icon based on transaction type
|
|
let icon;
|
|
if (transactionDetails.type === "out") {
|
|
icon = `<svg class="icon sent" style="display: block; margin: auto;" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#FF4B4B"><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 {
|
|
icon = `<svg class="icon received" style="display: block; margin: auto;" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#4BC84B"><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>`;
|
|
}
|
|
|
|
// Format addresses for display
|
|
const shortenAddress = (addr) => {
|
|
if (!addr) return "";
|
|
addr = String(addr);
|
|
return (
|
|
addr.substring(0, 8) + "..." + addr.substring(addr.length - 6)
|
|
);
|
|
};
|
|
|
|
const fromAddress = transactionDetails.sender
|
|
? shortenAddress(transactionDetails.sender)
|
|
: "";
|
|
const toAddress = transactionDetails.receiver
|
|
? shortenAddress(transactionDetails.receiver)
|
|
: "";
|
|
const txHashShort = shortenAddress(transactionDetails.txid);
|
|
|
|
transactionCard.innerHTML = `
|
|
<div class="flex gap-1 align-center w-100 padding-0-5" style="border-radius: 0.5rem; border: 1px solid rgba(var(--text-color), 0.1); padding: 0.75rem; box-shadow: 0 2px 4px rgba(0,0,0,0.05);">
|
|
<div class="transaction__icon flex align-center justify-center" style="background-color: ${transactionDetails.type === "out"
|
|
? "rgba(255,75,75,0.1)"
|
|
: "rgba(75,200,75,0.1)"
|
|
}; border-radius: 50%; min-width: 40px; height: 40px; padding: 8px;">
|
|
${icon}
|
|
</div>
|
|
<div class="grid gap-0-5 flex-1">
|
|
<div class="flex align-center space-between flex-wrap">
|
|
${transactionDetails.transactionType === "transfer" ? `<div class="transaction__type" style="font-weight: 500; margin-right: 8px;">
|
|
${transactionDetails.type ===
|
|
"out"
|
|
? "Sent"
|
|
: "Received"
|
|
}
|
|
</div>` : ""}
|
|
${transactionDetails.typeLabel ? `<span style="background: rgba(var(--text-color), 0.1); padding: 4px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 500; display: inline-flex; align-items: center; gap: 4px;"> ${transactionDetails.typeLabel}</span>` : ""}
|
|
<time style="color: rgba(var(--text-color), 0.6); font-size: 0.85rem;">${typeof transactionDetails.time ===
|
|
"string"
|
|
? transactionDetails.time
|
|
: getFormattedTime(
|
|
transactionDetails.time
|
|
)
|
|
}</time>
|
|
</div>
|
|
<div class="flex align-center space-between flex-wrap">
|
|
<div class="transaction__amount sol-value" data-sol="${transactionDetails.amount
|
|
}" data-timestamp="${transactionDetails.originalTimestamp || ""
|
|
}" style="font-weight: 600; color: ${transactionDetails.type === "out"
|
|
? "var(--color-danger, #ff4b4b)"
|
|
: "var(--color-success, #4bc84b)"
|
|
}; margin-right: 8px; text-overflow: ellipsis; overflow: hidden; max-width: 100%;">
|
|
${formatCurrency(
|
|
transactionDetails.amount
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-wrap gap-0-5 margin-top-0-5">
|
|
${transactionDetails.transactionType === "transfer" && transactionDetails.sender
|
|
? `<div class="flex align-center gap-0-3">
|
|
<span style="font-size: 0.85rem; color: rgba(var(--text-color), 0.7);">From:</span>
|
|
<span class="address-from interactive" data-address="${transactionDetails.sender}" style="font-size: 0.85rem; color: var(--color-primary); cursor: pointer;">${fromAddress}</span>
|
|
</div>`
|
|
: ""
|
|
}
|
|
${transactionDetails.transactionType === "transfer" && transactionDetails.receiver
|
|
? `<div class="flex align-center gap-0-3">
|
|
<span style="font-size: 0.85rem; color: rgba(var(--text-color), 0.7);">To:</span>
|
|
<span class="address-to interactive" data-address="${transactionDetails.receiver}" style="font-size: 0.85rem; color: var(--color-primary); cursor: pointer;">${toAddress}</span>
|
|
</div>`
|
|
: ""
|
|
}
|
|
<div class="flex align-center gap-0-3">
|
|
<span style="font-size: 0.85rem; color: rgba(var(--text-color), 0.7);">Tx:</span>
|
|
<span class="txid interactive" data-txid="${transactionDetails.txid
|
|
}" style="font-size: 0.85rem; color: var(--color-primary,#92a2ff); cursor: pointer;">${txHashShort}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
transactionCard.addEventListener("click", () => {
|
|
window.location.hash = `#/tdx/${transactionDetails.txid}`;
|
|
});
|
|
// Add click event handlers for addresses
|
|
transactionCard
|
|
.querySelectorAll(".address-from, .address-to")
|
|
.forEach((el) => {
|
|
el.addEventListener("click", (e) => {
|
|
e.stopPropagation(); // Prevent card click
|
|
const address = el.dataset.address;
|
|
if (address) {
|
|
window.location.hash = `#/balance/${address}`;
|
|
}
|
|
});
|
|
});
|
|
|
|
transactionCard
|
|
.querySelector(".txid")
|
|
.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
transactionCard.classList.add("interactive");
|
|
return transactionCard;
|
|
},
|
|
};
|
|
|
|
// Utility function to format time
|
|
function getFormattedTime(timestamp) {
|
|
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();
|
|
|
|
minutes = minutes < 10 ? `0${minutes}` : minutes;
|
|
const finalHours =
|
|
hours >= 12
|
|
? `${hours === 12 ? 12 : hours - 12}:${minutes} PM`
|
|
: `${hours === 0 ? 12 : hours}:${minutes} AM`;
|
|
|
|
return `${date} ${month} ${year}, ${finalHours}`;
|
|
} catch (e) {
|
|
console.error(e);
|
|
return timestamp;
|
|
}
|
|
}
|
|
let allSignatures = [];
|
|
|
|
const TX_FETCH_LIMIT = 10;
|
|
const TX_DETAILS_BATCH_SIZE = 10;
|
|
const BATCH_DELAY = 0;
|
|
let lastFetchedSignature = null;
|
|
let hasMoreTransactions = true;
|
|
let currentPage = 1;
|
|
let cachedTransactions = []; // Stores processed transaction data
|
|
let formattedTxs = []; // Stores formatted transactions for display
|
|
|
|
// Cache pages to avoid re-fetching when clicking Previous button
|
|
let transactionPageCache = new Map(); // pageNumber -> { signatures, transactions, formatted }
|
|
|
|
async function getSolanaTransactionHistory(
|
|
address,
|
|
before = null,
|
|
reset = true
|
|
) {
|
|
try {
|
|
const publicKey = new solanaWeb3.PublicKey(address);
|
|
const options = { limit: TX_FETCH_LIMIT };
|
|
|
|
if (before) {
|
|
options.before = before;
|
|
}
|
|
|
|
if (reset) {
|
|
allSignatures = [];
|
|
lastFetchedSignature = null;
|
|
hasMoreTransactions = true;
|
|
currentPage = 1;
|
|
}
|
|
|
|
// Show graceful loading state
|
|
if (getRef("transactions_list")) {
|
|
getRef("transactions_list").innerHTML =
|
|
'<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
|
|
}
|
|
|
|
try {
|
|
const signatures = await connection.getSignaturesForAddress(
|
|
publicKey,
|
|
options
|
|
);
|
|
|
|
// Update state
|
|
if (signatures.length > 0) {
|
|
lastFetchedSignature =
|
|
signatures[signatures.length - 1].signature;
|
|
hasMoreTransactions = signatures.length === TX_FETCH_LIMIT;
|
|
|
|
// Add to our full list of signatures
|
|
if (reset) {
|
|
allSignatures = signatures;
|
|
} else {
|
|
allSignatures = allSignatures.concat(signatures);
|
|
}
|
|
|
|
return signatures;
|
|
} else {
|
|
hasMoreTransactions = false;
|
|
return [];
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching signatures:", error);
|
|
notify(
|
|
`Error fetching transaction history: ${error.message}`,
|
|
"error"
|
|
);
|
|
return [];
|
|
}
|
|
} catch (error) {
|
|
console.error("Error in getSolanaTransactionHistory:", error);
|
|
notify(
|
|
`Invalid address or connection error: ${error.message}`,
|
|
"error"
|
|
);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async function processTransactions(signatures) {
|
|
try {
|
|
const parsedTxs = [];
|
|
for (let i = 0; i < signatures.length; i += TX_DETAILS_BATCH_SIZE) {
|
|
const batch = signatures.slice(i, i + TX_DETAILS_BATCH_SIZE);
|
|
try {
|
|
const batchResults = await Promise.all(
|
|
batch.map((txSig) =>
|
|
connection.getParsedTransaction(
|
|
typeof txSig === "string" ? txSig : txSig.signature,
|
|
{ maxSupportedTransactionVersion: 0 }
|
|
)
|
|
)
|
|
);
|
|
|
|
// Filter out null results but provide details on failures
|
|
const validResults = batchResults.filter((result, index) => {
|
|
if (!result) {
|
|
console.warn(
|
|
`Failed to fetch transaction: ${batch[index].signature || batch[index]
|
|
}`
|
|
);
|
|
}
|
|
return Boolean(result);
|
|
});
|
|
|
|
parsedTxs.push(...validResults);
|
|
} catch (batchError) {
|
|
console.error("Batch processing error:", batchError);
|
|
}
|
|
|
|
// Add a small delay between batches to avoid rate limiting
|
|
if (i + TX_DETAILS_BATCH_SIZE < signatures.length) {
|
|
await new Promise((resolve) => setTimeout(resolve, BATCH_DELAY));
|
|
}
|
|
}
|
|
|
|
return parsedTxs;
|
|
} catch (error) {
|
|
console.error("Error in processTransactions:", error);
|
|
notify("Failed to process transaction data", "error");
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
async function applyFilter() {
|
|
const filterSelector = document.getElementById("filter_selector");
|
|
if (!filterSelector) return;
|
|
|
|
const filter = filterSelector.value || "all";
|
|
const container = document.getElementById("transactions_list");
|
|
if (!container) return;
|
|
|
|
// Get current page data from cache
|
|
const cachedPage = transactionPageCache.get(currentPage);
|
|
if (!cachedPage) return;
|
|
|
|
let filtered = cachedPage.formatted;
|
|
if (filter === "sent") {
|
|
filtered = cachedPage.formatted.filter((tx) => tx.type === "out");
|
|
} else if (filter === "received") {
|
|
filtered = cachedPage.formatted.filter((tx) => tx.type === "in");
|
|
}
|
|
|
|
// Show loading spinner for visual feedback
|
|
container.innerHTML = '<div class="flex justify-center w-100 margin-top-2"><sm-spinner></sm-spinner></div>';
|
|
|
|
// Use a timeout to let the spinner show and keep UI snappy
|
|
setTimeout(async () => {
|
|
const filteredList = filtered; // Capture current filtered state
|
|
|
|
// Render all cards first in memory to avoid jumping
|
|
const cards = await Promise.all(filteredList.map(tx => render.transactionCard(tx)));
|
|
|
|
container.innerHTML = "";
|
|
if (cards.length > 0) {
|
|
cards.forEach(card => {
|
|
if (card) container.appendChild(card);
|
|
});
|
|
} else {
|
|
container.innerHTML = '<p class="text-center margin-top-2">No transactions found for this filter in this page</p>';
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
function handleInvalidSearch() {
|
|
if (document.startViewTransition)
|
|
document.startViewTransition(() => {
|
|
getRef("sol_balance_wrapper").classList.add("hidden");
|
|
});
|
|
else {
|
|
getRef("sol_balance_wrapper").classList.add("hidden");
|
|
}
|
|
}
|
|
|
|
function renderHome(state) {
|
|
getRef("page_container").dataset.page = "home";
|
|
const addressParam = state.wildcards && state.wildcards[0];
|
|
renderElem(
|
|
getRef("page_container"),
|
|
html`
|
|
<aside
|
|
id="saved_addresses_wrapper"
|
|
class="flex flex-direction-column"
|
|
>
|
|
<h4>Searched addresses</h4>
|
|
<ul id="searched_addresses_list" class="grid gap-0-5"></ul>
|
|
</aside>
|
|
<section id="balance_section" class="grid gap-1-5">
|
|
<h2>Check SOL balance and transaction history</h2>
|
|
<sm-form>
|
|
<div id="input_wrapper">
|
|
<sm-input
|
|
id="wallet_address_input"
|
|
class="password-field flex-1"
|
|
placeholder="Enter Solana Address or Transaction Signature"
|
|
type="text"
|
|
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>
|
|
<div class="multi-state-button">
|
|
<button
|
|
id="check_balance_button"
|
|
class="button button--primary h-100 w-100"
|
|
type="submit"
|
|
onclick="checkBalance()"
|
|
>
|
|
Check
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</sm-form>
|
|
<div id="sol_balance_wrapper" class="grid gap-2 hidden"></div>
|
|
<div
|
|
id="address_transactions"
|
|
class="grid gap-2 hidden"
|
|
style="width: min(32rem, 100%);"
|
|
>
|
|
<div
|
|
class="flex flex-direction-column gap-0-5 sticky top-0"
|
|
style="background-color: rgba(var(--foreground-color), 1); z-index: 2; padding: 1rem 0; border-bottom: 1px solid rgba(var(--text-color), 0.1);"
|
|
>
|
|
<div class="flex align-center gap-0-5 space-between">
|
|
<h4>Transactions</h4>
|
|
<sm-chips
|
|
id="filter_selector"
|
|
onchange=${() => applyFilter()}
|
|
>
|
|
<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>
|
|
${isHistoricApiAvailable && currentCurrency !== "sol"
|
|
? html`
|
|
<div class="margin-left-auto">
|
|
<sm-switch
|
|
id="valuation_toggle"
|
|
?checked=${showCurrentValue}
|
|
onchange=${(e) =>
|
|
handleValuationTypeChange(e.target.checked)}
|
|
>
|
|
<p slot="left" class="margin-right-0-5">
|
|
Show current value
|
|
</p>
|
|
</sm-switch>
|
|
</div>
|
|
`
|
|
: ""}
|
|
</div>
|
|
<ul
|
|
id="transactions_list"
|
|
class="observe-empty-state grid gap-1"
|
|
></ul>
|
|
<div
|
|
class="empty-state align-self-center text-center margin-top-2"
|
|
>
|
|
<svg
|
|
class="icon"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
height="48px"
|
|
viewBox="0 0 24 24"
|
|
width="48px"
|
|
fill="rgba(var(--text-color), 0.5)"
|
|
>
|
|
<path d="M0 0h24v24H0V0z" fill="none" />
|
|
<path
|
|
d="M19.5 3.5L18 2l-1.5 1.5L15 2l-1.5 1.5L12 2l-1.5 1.5L9 2 7.5 3.5 6 2 4.5 3.5 3 2v20l1.5-1.5L6 22l1.5-1.5L9 22l1.5-1.5L12 22l1.5-1.5L15 22l1.5-1.5L18 22l1.5-1.5L21 22V2l-1.5 1.5zM19 19.09H5V4.91h14v14.18zM6 15h12v2H6zm0-4h12v2H6zm0-4h12v2H6z"
|
|
/>
|
|
</svg>
|
|
<p
|
|
class="margin-top-1"
|
|
style="color: rgba(var(--text-color), 0.7);"
|
|
>
|
|
Transactions will appear here
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
`
|
|
);
|
|
|
|
// Reset LazyLoader since page re-render creates new DOM elements
|
|
transactionsLazyLoader = null;
|
|
|
|
renderSearchedAddressList();
|
|
|
|
// Use address from URL or restore previously viewed address
|
|
const addressToLoad = addressParam || currentWalletAddress;
|
|
|
|
if (addressToLoad) {
|
|
// Wait for DOM to be ready before loading transactions
|
|
setTimeout(async () => {
|
|
const inputField = getRef("wallet_address_input");
|
|
if (inputField) {
|
|
inputField.value = addressToLoad;
|
|
|
|
// Load balance if checkBalance function is available
|
|
if (typeof checkBalance === 'function') {
|
|
try {
|
|
await checkBalance(addressToLoad);
|
|
} catch (error) {
|
|
console.error("Error in checkBalance:", error);
|
|
}
|
|
}
|
|
|
|
// Ensure DOM is fully ready, then load transaction history
|
|
setTimeout(async () => {
|
|
try {
|
|
await render.transactions(addressToLoad);
|
|
} catch (error) {
|
|
console.error("Error rendering transactions:", error);
|
|
}
|
|
}, 100);
|
|
}
|
|
}, 200); // Increased timeout to ensure DOM is ready
|
|
}
|
|
}
|
|
|
|
function renderError(title, description) {
|
|
if (!title) title = "MetaMask not installed";
|
|
if (!description) description = "";
|
|
renderElem(
|
|
getRef("page_container"),
|
|
html`
|
|
<section id="error_section">
|
|
<svg
|
|
class="icon"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
xml:space="preserve"
|
|
id="Layer_1"
|
|
x="0"
|
|
y="0"
|
|
version="1.1"
|
|
viewBox="0 0 318.6 318.6"
|
|
>
|
|
<style>
|
|
.st1,
|
|
.st6 {
|
|
fill: #e4761b;
|
|
stroke: #e4761b;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.st6 {
|
|
fill: #f6851b;
|
|
stroke: #f6851b;
|
|
}
|
|
</style>
|
|
<path
|
|
fill="#e2761b"
|
|
stroke="#e2761b"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
d="m274.1 35.5-99.5 73.9L193 65.8z"
|
|
/>
|
|
<path
|
|
d="m44.4 35.5 98.7 74.6-17.5-44.3zm193.9 171.3-26.5 40.6 56.7 15.6 16.3-55.3zm61.4-99.1 18.3-8.7-58.4-26.3zM20.8 99 38.7 107.7l56.8-26.3zM197.7 206l-77.1-2.6 3.1 27.5-58.8 16.1 15.8 56.8 48.6-35 11.1 9.2 2.5 7.9h29.6l3.1-8.1 10.2-9.2 48.6 35 16.7-56.8-58.4-16.1zm-75.4-55.7 49.7-.5-51.2-23.2zM123.8 206l26.7-32.1-54.5-1.3zm71.1-32.1 26.8 32.1 27.8-33.4zm12.7-1.1-17.1-31.7 36.7-1.5zm-52.4-33.2-16.6 32.2-20-33.6zm101.7-57.7-12.2 24.5 41.5 2.6 4.7-39.4zM44.7 107.7l4.4 39.4 41.2-2.6-12.2-24.5zM101.2 170.5l27.2 33.4 27.1-32.1-17.6-32zm117.2 0-36.7-1.5 27.2 33.6zm-117.5 79.5 32.6-9.7-4.7 39.4zm85.7 29.7h-33l3.1-39.2h26.6zm21.5-40.3 32.5 9.7-27.8-48.8z"
|
|
class="st6"
|
|
/>
|
|
</svg>
|
|
<h2>${title}</h2>
|
|
<p>${description}</p>
|
|
</section>
|
|
`
|
|
);
|
|
}
|
|
|
|
function renderSearchedAddressList() {
|
|
compactIDB
|
|
.readAllData("solanaContacts")
|
|
.then((solanaContacts) => {
|
|
if (!getRef("searched_addresses_list")) return;
|
|
if (Object.keys(solanaContacts).length === 0) {
|
|
renderElem(
|
|
getRef("searched_addresses_list"),
|
|
html`<li class="flex align-center justify-center">
|
|
<p>
|
|
Your searched addresses will appear here for easier access
|
|
in future.
|
|
</p>
|
|
</li>`
|
|
);
|
|
return;
|
|
}
|
|
const renderedContacts = [];
|
|
for (const keys in solanaContacts) {
|
|
const walletAddress =
|
|
solanaContacts[keys].walletAddressOrSignature;
|
|
|
|
const isFloAddr = solanaContacts[keys].isfloaddr;
|
|
console.log("isfloaddr", isFloAddr);
|
|
renderedContacts.push(html`<li
|
|
class="contact"
|
|
.dataset=${{ address: walletAddress }}
|
|
>
|
|
${!isFloAddr
|
|
? html`<sm-copy value="${walletAddress}"></sm-copy>`
|
|
: html`<sm-chips
|
|
onchange=${(e) =>
|
|
(e.target
|
|
.closest(".contact")
|
|
.querySelector("sm-copy").value = e.target.value)}
|
|
>
|
|
<sm-chip value=${walletAddress} selected>SOL</sm-chip>
|
|
<sm-chip value=${isFloAddr}
|
|
>${isFloAddr.startsWith("bc")
|
|
? "BTC"
|
|
: "FLO"}</sm-chip
|
|
>
|
|
</sm-chips>
|
|
<sm-copy value="${walletAddress}"></sm-copy>`}
|
|
<div class="flex align-center space-between gap-0-5">
|
|
<button
|
|
class="button button--small"
|
|
onclick=${() => deleteContact(walletAddress)}
|
|
>
|
|
Delete
|
|
</button>
|
|
<button
|
|
class="button button--colored button--small"
|
|
onclick=${() => checkBalance(walletAddress)}
|
|
>
|
|
Check balance
|
|
</button>
|
|
</div>
|
|
</li>`);
|
|
}
|
|
renderElem(
|
|
getRef("searched_addresses_list"),
|
|
html`${renderedContacts}`
|
|
);
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
});
|
|
}
|
|
|
|
function renderTdx(state) {
|
|
getRef("page_container").dataset.page = "tdx";
|
|
|
|
const txId = state.wildcards && state.wildcards[0];
|
|
|
|
if (!txId) {
|
|
notify("Invalid transaction signature", "error");
|
|
router.routeTo("#/balance");
|
|
return;
|
|
}
|
|
|
|
renderElem(
|
|
getRef("page_container"),
|
|
html`
|
|
<section class="tx-details-container">
|
|
<div class="tx-header">
|
|
<h2 class="tx-title">Transaction Details</h2>
|
|
${currentCurrency !== "sol" && isHistoricApiAvailable
|
|
? html`
|
|
<div class="margin-left-auto">
|
|
<sm-switch
|
|
id="tx_valuation_toggle"
|
|
?checked=${showCurrentValue}
|
|
>
|
|
<p slot="left" class="margin-right-0-5">
|
|
Show current value
|
|
</p>
|
|
</sm-switch>
|
|
</div>
|
|
`
|
|
: ""}
|
|
</div>
|
|
|
|
<div id="tx_details_container" class="tx-content">
|
|
<div class="loading-state">
|
|
<sm-spinner
|
|
class="justify-self-center margin-top-1-5"
|
|
></sm-spinner>
|
|
<p class="loading-text">Fetching transaction details...</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
`
|
|
);
|
|
|
|
// Fetch and render transaction details
|
|
fetchSolanaTransactionDetails(txId)
|
|
.then((tx) => {
|
|
if (!tx) {
|
|
renderNotFound();
|
|
return;
|
|
}
|
|
console.log("Transaction details:", tx);
|
|
|
|
// Calculate fee first
|
|
const fee = tx.meta?.fee
|
|
? tx.meta.fee / solanaWeb3.LAMPORTS_PER_SOL
|
|
: 0;
|
|
|
|
// Extract transaction information
|
|
// Sender's balance change includes both transfer amount and fee
|
|
// So we subtract the fee to get just the transfer amount
|
|
const value =
|
|
tx.meta?.postBalances && tx.meta?.preBalances
|
|
? (Math.abs(tx.meta.postBalances[0] - tx.meta.preBalances[0]) /
|
|
solanaWeb3.LAMPORTS_PER_SOL) - fee
|
|
: 0;
|
|
|
|
const timestamp = tx.blockTime
|
|
? getFormattedTime(tx.blockTime)
|
|
: "Pending";
|
|
|
|
const status =
|
|
tx && typeof tx.slot === "number" && tx.meta
|
|
? "confirmed"
|
|
: "pending";
|
|
|
|
renderElem(
|
|
getRef("tx_details_container"),
|
|
html`
|
|
<div class="tx-card" id="tx-status">
|
|
<!-- Status Header -->
|
|
<div class="tx-status-header" style="display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 1rem;">
|
|
<div style="display: flex; align-items: center; gap: 1rem; flex: 1; min-width: 200px;">
|
|
<div
|
|
id="tx_status_indicator"
|
|
class="status-indicator"
|
|
></div>
|
|
<div class="status-details">
|
|
<h3 id="tx_status_title" class="status-title"></h3>
|
|
<p id="tx_status_subtext" class="status-subtext"></p>
|
|
</div>
|
|
</div>
|
|
<a id="tx_explorer_link_header" href="#" target="_blank" rel="noopener noreferrer" style="display: inline-flex; align-items: center; gap: 0.5rem; color: var(--color-primary); text-decoration: none; font-weight: 500; padding: 0.5rem 1rem; border: 1px solid rgba(var(--text-color), 0.2); border-radius: 8px; transition: all 0.2s; font-size: 0.9rem; white-space: nowrap;">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
</svg>
|
|
View on Solscan
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Main Transaction Info -->
|
|
<div class="tx-info-grid">
|
|
<!-- From/To Section -->
|
|
<div class="tx-address-section">
|
|
<div class="address-card">
|
|
<label class="address-label">From</label>
|
|
<sm-copy id="tx_from" class="address-value"></sm-copy>
|
|
</div>
|
|
|
|
<div class="tx-arrow">→</div>
|
|
|
|
<div class="address-card">
|
|
<label class="address-label">To</label>
|
|
<sm-copy id="tx_to" class="address-value"></sm-copy>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Transaction Hash -->
|
|
<div class="tx-hash-section">
|
|
<label class="section-label">Transaction Signature</label>
|
|
<sm-copy id="tx_hash" class="hash-value"></sm-copy>
|
|
</div>
|
|
|
|
<!-- Transaction Type -->
|
|
<div class="tx-type-section" style="margin-top: 1rem;">
|
|
<label class="section-label">Transaction Type</label>
|
|
<div id="tx_type" style="font-weight: 600; font-size: 1rem; margin-top: 0.5rem; color: var(--color-primary);"></div>
|
|
</div>
|
|
|
|
|
|
<!-- Metrics Grid -->
|
|
<div class="tx-metrics-grid">
|
|
<div class="metric-card">
|
|
<label class="metric-label">Value</label>
|
|
<div
|
|
id="tx_value"
|
|
class="metric-value sol-value"
|
|
data-sol="${value}"
|
|
data-timestamp="${tx.blockTime || ""}"
|
|
></div>
|
|
</div>
|
|
|
|
<div class="metric-card">
|
|
<label class="metric-label">Slot</label>
|
|
<div id="tx_slot" class="metric-value"></div>
|
|
</div>
|
|
|
|
<div class="metric-card">
|
|
<label class="metric-label">Transaction Fee</label>
|
|
<div
|
|
id="tx_fee"
|
|
class="metric-value sol-value"
|
|
data-sol="${fee}"
|
|
data-timestamp="${tx.blockTime || ""}"
|
|
></div>
|
|
</div>
|
|
|
|
<div class="metric-card">
|
|
<label class="metric-label">Timestamp</label>
|
|
<div id="tx_timestamp" class="metric-value"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
);
|
|
|
|
// Update UI elements with transaction data
|
|
if (getRef("tx_status_indicator")) {
|
|
const statusIndicator = getRef("tx_status_indicator");
|
|
if (status === "confirmed") {
|
|
statusIndicator.style.backgroundColor = "var(--color-success)";
|
|
statusIndicator.style.boxShadow =
|
|
"0 0 0 4px rgba(var(--color-success-rgb), 0.2)";
|
|
} else {
|
|
statusIndicator.style.backgroundColor = "var(--color-warning)";
|
|
statusIndicator.style.boxShadow =
|
|
"0 0 0 4px rgba(var(--color-warning-rgb), 0.2)";
|
|
}
|
|
}
|
|
|
|
if (getRef("tx_status_title")) {
|
|
getRef("tx_status_title").textContent =
|
|
status === "confirmed"
|
|
? "Transaction Confirmed"
|
|
: "Transaction Pending";
|
|
}
|
|
|
|
if (getRef("tx_status_subtext")) {
|
|
getRef("tx_status_subtext").textContent =
|
|
status === "confirmed" ? `Included in Slot #${tx.slot}` : "";
|
|
}
|
|
|
|
|
|
// Extract sender and receiver from transaction
|
|
let sender = "Unknown";
|
|
let receiver = "Unknown";
|
|
let transactionType = "transfer"; // transfer, swap, nft_mint, other
|
|
|
|
const accountKeys = tx.transaction?.message?.accountKeys || [];
|
|
const instructions = tx.transaction?.message?.instructions || [];
|
|
const logMessages = tx.meta?.logMessages || [];
|
|
|
|
// Check if this is an NFT mint by looking at log messages
|
|
const isNFTMint = logMessages.some(log =>
|
|
log.includes("MintToCollectionV1") ||
|
|
log.includes("MintV1") ||
|
|
log.includes("Bubblegum") ||
|
|
(log.includes("Instruction: Mint") && !log.includes("MintTo") && !log.includes("JUP"))
|
|
);
|
|
|
|
if (isNFTMint) {
|
|
transactionType = "nft_mint";
|
|
sender = "NFT Mint";
|
|
receiver = accountKeys[0]?.toString() || "Unknown";
|
|
}
|
|
// Check if this is a token swap by looking at token balance changes
|
|
else if (tx.meta?.preTokenBalances && tx.meta?.postTokenBalances) {
|
|
const preTokens = tx.meta.preTokenBalances;
|
|
const postTokens = tx.meta.postTokenBalances;
|
|
|
|
// Find tokens that decreased (sent)
|
|
let tokenSent = null;
|
|
let tokenReceived = null;
|
|
|
|
for (const preTok of preTokens) {
|
|
const postTok = postTokens.find(p => p.accountIndex === preTok.accountIndex);
|
|
if (postTok) {
|
|
const preAmount = parseFloat(preTok.uiTokenAmount.amount);
|
|
const postAmount = parseFloat(postTok.uiTokenAmount.amount);
|
|
|
|
// Token amount decreased significantly (sent/swapped)
|
|
if (preAmount > postAmount && (preAmount - postAmount) > 0.000001) {
|
|
tokenSent = {
|
|
mint: preTok.mint,
|
|
amount: preTok.uiTokenAmount.uiAmountString,
|
|
symbol: preTok.mint === "So11111111111111111111111111111111111111112" ? "SOL" : preTok.mint
|
|
};
|
|
}
|
|
|
|
// Token amount increased significantly (received)
|
|
if (postAmount > preAmount && (postAmount - preAmount) > 0.000001) {
|
|
tokenReceived = {
|
|
mint: postTok.mint,
|
|
amount: postTok.uiTokenAmount.uiAmountString,
|
|
symbol: postTok.mint === "So11111111111111111111111111111111111111112" ? "SOL" : postTok.mint
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we found a token swap, use that instead of FROM/TO
|
|
if (tokenSent && tokenReceived) {
|
|
transactionType = "swap";
|
|
|
|
// Map of common token mint addresses to symbols
|
|
const tokenSymbolMap = {
|
|
"So11111111111111111111111111111111111111112": "SOL",
|
|
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": "USDC",
|
|
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB": "USDT",
|
|
"DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263": "Bonk",
|
|
"7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs": "Ether (Wormhole)",
|
|
"mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So": "mSOL",
|
|
"7dHbWXmci3dT8UFYWYZweBLXgycu7Y3iL6trKn1Y7ARj": "stSOL",
|
|
"SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt": "SRM",
|
|
"kinXdEcpDQeHPEuQnqmUgtYykqKGVFq6CeVX5iAHJq6": "KIN",
|
|
"orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE": "ORCA",
|
|
"RLBxxFkseAZ4RgJH3Sqn8jXxhmGoz9jWxDNJMh8pL7a": "RLB",
|
|
"MangoCzJ36AjZyKwVj3VnYU4GTonjfVEnJmvvWaxLac": "MNGO",
|
|
"SHDWyBxihqiCj6YekG2GUr7wqKLeLAMK1gHZck9pL6y": "SHDW",
|
|
"HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3": "PYTH",
|
|
"JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN": "JUP",
|
|
};
|
|
|
|
// Get token symbols or use truncated addresses
|
|
const getTokenSymbol = (mint) => {
|
|
if (tokenSymbolMap[mint]) {
|
|
return tokenSymbolMap[mint];
|
|
}
|
|
// Fallback to truncated address
|
|
if (mint.length > 20) {
|
|
return mint.substring(0, 8) + "..." + mint.substring(mint.length - 4);
|
|
}
|
|
return mint;
|
|
};
|
|
|
|
sender = getTokenSymbol(tokenSent.mint);
|
|
receiver = getTokenSymbol(tokenReceived.mint);
|
|
}
|
|
}
|
|
|
|
// If not a swap or NFT mint, try to find the transfer instruction
|
|
if (transactionType === "transfer") {
|
|
// First priority: Check for parsed instructions (most reliable)
|
|
for (const ix of instructions) {
|
|
if (ix.parsed && ix.parsed.info) {
|
|
// System transfer
|
|
if (ix.parsed.type === "transfer" && ix.parsed.info.source && ix.parsed.info.destination) {
|
|
sender = ix.parsed.info.source;
|
|
receiver = ix.parsed.info.destination;
|
|
break;
|
|
}
|
|
// Token transfer
|
|
if (ix.parsed.info.authority && ix.parsed.info.destination) {
|
|
sender = ix.parsed.info.authority;
|
|
receiver = ix.parsed.info.destination;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Second priority: Try raw instruction decoding
|
|
if (sender === "Unknown") {
|
|
for (const ix of instructions) {
|
|
if (ix.accounts && ix.accounts.length >= 2) {
|
|
if (ix.data) {
|
|
try {
|
|
const dataBytes = bs58.decode(ix.data);
|
|
if (dataBytes[0] === 2) {
|
|
sender = accountKeys[ix.accounts[0]]?.toString() || "Unknown";
|
|
receiver = accountKeys[ix.accounts[1]]?.toString() || "Unknown";
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
// If decode fails, continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: use balance changes to determine sender/receiver
|
|
if (sender === "Unknown" && tx.meta?.preBalances && tx.meta?.postBalances) {
|
|
for (let i = 0; i < accountKeys.length; i++) {
|
|
const balanceChange = tx.meta.postBalances[i] - tx.meta.preBalances[i];
|
|
if (balanceChange < -100000 && sender === "Unknown") {
|
|
sender = accountKeys[i]?.toString() || "Unknown";
|
|
}
|
|
if (balanceChange > 100000 && receiver === "Unknown") {
|
|
receiver = accountKeys[i]?.toString() || "Unknown";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final fallback: use first account as sender
|
|
if (sender === "Unknown" && accountKeys[0]) {
|
|
sender = accountKeys[0]?.toString() || "Unknown";
|
|
}
|
|
}
|
|
|
|
if (getRef("tx_from")) {
|
|
getRef("tx_from").value = sender;
|
|
getRef("tx_from").addEventListener("click", function (e) {
|
|
if (
|
|
!e.target.closest("sm-copy button") &&
|
|
sender !== "Unknown"
|
|
) {
|
|
window.location.hash = `#/balance/${sender}`;
|
|
}
|
|
});
|
|
getRef("tx_from").style.cursor =
|
|
sender !== "Unknown" ? "pointer" : "not-allowed";
|
|
getRef("tx_from").title =
|
|
sender !== "Unknown"
|
|
? "View address details"
|
|
: "Address unknown";
|
|
}
|
|
|
|
if (getRef("tx_to")) {
|
|
getRef("tx_to").value = receiver;
|
|
getRef("tx_to").addEventListener("click", function (e) {
|
|
if (
|
|
!e.target.closest("sm-copy button") &&
|
|
receiver !== "Unknown"
|
|
) {
|
|
window.location.hash = `#/balance/${receiver}`;
|
|
}
|
|
});
|
|
getRef("tx_to").style.cursor =
|
|
receiver !== "Unknown" ? "pointer" : "not-allowed";
|
|
getRef("tx_to").title =
|
|
receiver !== "Unknown"
|
|
? "View address details"
|
|
: "Address unknown";
|
|
}
|
|
|
|
// Hide FROM/TO section for swaps and NFT mints
|
|
const addressSection = document.querySelector(".tx-address-section");
|
|
if (addressSection) {
|
|
if (transactionType === "swap" || transactionType === "nft_mint") {
|
|
addressSection.style.display = "none";
|
|
} else {
|
|
addressSection.style.display = "flex";
|
|
}
|
|
}
|
|
|
|
if (getRef("tx_hash")) getRef("tx_hash").value = txId;
|
|
|
|
// Set transaction type label
|
|
if (getRef("tx_type")) {
|
|
const typeLabels = {
|
|
"swap": "Swap",
|
|
"nft_mint": "NFT Mint",
|
|
"transfer": "Transfer"
|
|
};
|
|
getRef("tx_type").textContent = typeLabels[transactionType] || "Transfer";
|
|
}
|
|
|
|
// Set explorer link
|
|
if (getRef("tx_explorer_link_header")) {
|
|
getRef("tx_explorer_link_header").href = `https://solscan.io/tx/${txId}`;
|
|
}
|
|
|
|
if (getRef("tx_value")) {
|
|
getRef("tx_value").dataset.sol = value;
|
|
getRef("tx_value").dataset.timestamp = tx.blockTime || "";
|
|
getRef("tx_value").textContent = formatCurrency(value);
|
|
}
|
|
|
|
if (getRef("tx_slot"))
|
|
getRef("tx_slot").textContent = tx.slot || "Pending";
|
|
if (getRef("tx_fee")) {
|
|
getRef("tx_fee").dataset.sol = fee;
|
|
getRef("tx_fee").dataset.timestamp = tx.blockTime || "";
|
|
getRef("tx_fee").textContent = formatCurrency(fee);
|
|
}
|
|
|
|
if (getRef("tx_timestamp"))
|
|
getRef("tx_timestamp").textContent = timestamp;
|
|
|
|
if (getRef("tx_valuation_toggle")) {
|
|
const toggle = getRef("tx_valuation_toggle");
|
|
const toggleParent = toggle.parentElement;
|
|
const newToggle = toggle.cloneNode(true);
|
|
|
|
newToggle.addEventListener("change", async (e) => {
|
|
showCurrentValue = e.target.checked;
|
|
localStorage.setItem(
|
|
"sol-wallet-show-current-value",
|
|
showCurrentValue
|
|
);
|
|
|
|
const valueElement = getRef("tx_value");
|
|
const feeElement = getRef("tx_fee");
|
|
|
|
if (valueElement && valueElement.dataset.sol) {
|
|
const solAmount = parseFloat(valueElement.dataset.sol);
|
|
const timestamp = valueElement.dataset.timestamp;
|
|
valueElement.textContent = await getConvertedAmount(
|
|
solAmount,
|
|
timestamp
|
|
);
|
|
}
|
|
|
|
if (feeElement && feeElement.dataset.sol) {
|
|
const solFee = parseFloat(feeElement.dataset.sol);
|
|
const timestamp = feeElement.dataset.timestamp;
|
|
feeElement.textContent = await getConvertedAmount(
|
|
solFee,
|
|
timestamp
|
|
);
|
|
}
|
|
});
|
|
|
|
toggleParent.replaceChild(newToggle, toggle);
|
|
}
|
|
|
|
convertAllDisplayedCurrency();
|
|
|
|
if (currentCurrency !== "sol" && !isHistoricApiAvailable) {
|
|
testHistoricalAPIAndRenderToggle();
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error:", error);
|
|
renderErrorState();
|
|
});
|
|
|
|
// Helper functions
|
|
function renderNotFound() {
|
|
renderElem(
|
|
getRef("tx_details_container"),
|
|
html`
|
|
<div class="error-state">
|
|
<div class="error-icon">!</div>
|
|
<h3>Transaction Not Found</h3>
|
|
<p>
|
|
The requested transaction could not be found on the network.
|
|
</p>
|
|
<button class="button" onclick="router.routeTo('#/balance')">
|
|
Back to Balance
|
|
</button>
|
|
</div>
|
|
`
|
|
);
|
|
}
|
|
|
|
function renderErrorState() {
|
|
renderElem(
|
|
getRef("tx_details_container"),
|
|
html`
|
|
<div class="error-state">
|
|
<div class="error-icon">⚠️</div>
|
|
<h3>Error Loading Transaction</h3>
|
|
<p>Failed to fetch transaction details. Please try again later.</p>
|
|
<button class="button" onclick="location.reload()">
|
|
Retry
|
|
</button>
|
|
</div>
|
|
`
|
|
);
|
|
}
|
|
}
|
|
async function fetchSolanaTransactionDetails(signature) {
|
|
try {
|
|
// Check for valid signature format
|
|
if (!signature || signature.length < 87 || signature.length > 89) {
|
|
console.error("Invalid signature format:", signature);
|
|
return null;
|
|
}
|
|
|
|
// Use maxSupportedTransactionVersion parameter to handle newer transaction formats
|
|
const tx = await connection.getTransaction(signature, {
|
|
maxSupportedTransactionVersion: 0,
|
|
});
|
|
|
|
// Log success or failure
|
|
if (tx) {
|
|
console.log("Transaction details fetched successfully");
|
|
} else {
|
|
console.warn("Transaction not found:", signature);
|
|
}
|
|
|
|
return tx;
|
|
} catch (error) {
|
|
console.error("Error fetching Solana transaction details:", error);
|
|
notify(`Failed to fetch transaction: ${error.message}`, "error");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
const getWalletBalance = async (walletAddressString) => {
|
|
try {
|
|
const walletPublicKey = new solanaWeb3.PublicKey(walletAddressString);
|
|
|
|
const walletBalance = await connection.getBalance(walletPublicKey);
|
|
console.log(
|
|
`Wallet balance: ${walletBalance / solanaWeb3.LAMPORTS_PER_SOL} SOL`
|
|
);
|
|
return walletBalance;
|
|
} catch (err) {
|
|
notify(err, "error");
|
|
}
|
|
};
|
|
const connection = new solanaWeb3.Connection(
|
|
"https://mainnet.helius-rpc.com/?api-key=e6f2d1ac-b6cb-4b03-8cb0-79f1d91d6805"
|
|
);
|
|
function determineInputType(input) {
|
|
try {
|
|
// Check if it's a valid Solana address (44 characters)
|
|
new solanaWeb3.PublicKey(input);
|
|
|
|
// If it's 88-89 characters, it's likely a signature
|
|
if (input.length >= 87 && input.length <= 89) {
|
|
return {
|
|
type: "signature",
|
|
value: input,
|
|
};
|
|
}
|
|
|
|
// Otherwise it's an address
|
|
return {
|
|
type: "address",
|
|
value: input,
|
|
};
|
|
} catch (error) {
|
|
// If it's not a valid address, check if it could be a signature
|
|
if (input.length >= 87 && input.length <= 89) {
|
|
return {
|
|
type: "signature",
|
|
value: input,
|
|
};
|
|
}
|
|
|
|
// If we can't determine, default to address
|
|
return {
|
|
type: "unknown",
|
|
value: input,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Modify the checkBalance function to handle both addresses and signatures
|
|
const checkBalance = async (walletAddressOrSignature) => {
|
|
if (!walletAddressOrSignature) {
|
|
walletAddressOrSignature = getRef("wallet_address_input").value;
|
|
}
|
|
|
|
// Determine if input is an address or signature
|
|
const inputType = determineInputType(walletAddressOrSignature);
|
|
|
|
if (inputType.type === "signature") {
|
|
// Redirect to transaction details page
|
|
window.location.hash = `#/tdx/${inputType.value}`;
|
|
return;
|
|
}
|
|
|
|
walletAddressOrSignature = inputType.value;
|
|
|
|
// Continue with your existing address handling logic
|
|
const currentHash = window.location.hash;
|
|
const basePath = "#/balance";
|
|
|
|
if (currentHash !== `${basePath}/${walletAddressOrSignature}`) {
|
|
history.replaceState(
|
|
null,
|
|
null,
|
|
`${basePath}/${walletAddressOrSignature}`
|
|
);
|
|
}
|
|
currentWalletAddress = walletAddressOrSignature;
|
|
|
|
// Rest of your existing checkBalance function...
|
|
const convert2Address = walletAddressOrSignature.startsWith("R")
|
|
? floCrypto.getAddress(walletAddressOrSignature)
|
|
: floCrypto.getAddress(walletAddressOrSignature, true);
|
|
const isfloaddr =
|
|
walletAddressOrSignature.length <= 44 ? "" : convert2Address;
|
|
|
|
walletAddressOrSignature =
|
|
walletAddressOrSignature.length <= 44
|
|
? walletAddressOrSignature
|
|
: floSolana.wif2SolanaAddress(walletAddressOrSignature);
|
|
|
|
// Continue with existing code...
|
|
|
|
buttonLoader("check_balance_button", true);
|
|
|
|
try {
|
|
const walletBalance = await getWalletBalance(
|
|
walletAddressOrSignature
|
|
);
|
|
const solBalance =
|
|
(await walletBalance) / solanaWeb3.LAMPORTS_PER_SOL;
|
|
|
|
const Keys = { walletAddressOrSignature, isfloaddr };
|
|
// Interact with IndexedDB
|
|
|
|
const contactExists = await compactIDB.readData(
|
|
"solanaContacts",
|
|
walletAddressOrSignature
|
|
);
|
|
if (!contactExists) {
|
|
await compactIDB.addData(
|
|
"solanaContacts",
|
|
Keys,
|
|
walletAddressOrSignature
|
|
);
|
|
await renderSearchedAddressList();
|
|
}
|
|
|
|
// Render the balances
|
|
// document.getElementById("sol_balance_wrapper").innerHTML =
|
|
renderElem(
|
|
getRef("sol_balance_wrapper"),
|
|
html`
|
|
<div class="grid">
|
|
<div class="label">Solana Wallet Address</div>
|
|
<sm-copy
|
|
id="sol_address"
|
|
value="${walletAddressOrSignature}"
|
|
></sm-copy>
|
|
</div>
|
|
${solBalance >= 0
|
|
? html`
|
|
<div class="grid gap-1">
|
|
<h4>Balance</h4>
|
|
<ul
|
|
id="sol_address_balance"
|
|
class="flex flex-direction-column gap-0-5"
|
|
>
|
|
<li class="flex align-center space-between">
|
|
<div class="flex align-center gap-0-3">
|
|
<p>SOL</p>
|
|
</div>
|
|
<b
|
|
id="sol_balance"
|
|
class="sol-value"
|
|
data-sol="${solBalance}"
|
|
>${solBalance} SOL</b
|
|
>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
`
|
|
: ""}
|
|
`
|
|
);
|
|
|
|
getRef("sol_balance_wrapper").classList.remove("hidden");
|
|
getRef("sol_balance_wrapper").animate(
|
|
[
|
|
{
|
|
transform: "translateY(-1rem)",
|
|
opacity: 0,
|
|
},
|
|
{
|
|
transform: "none",
|
|
opacity: 1,
|
|
},
|
|
],
|
|
{
|
|
easing: "ease",
|
|
duration: 300,
|
|
fill: "forwards",
|
|
}
|
|
);
|
|
|
|
render.transactions(walletAddressOrSignature);
|
|
} catch (error) {
|
|
notify(error, "error");
|
|
} finally {
|
|
buttonLoader("check_balance_button", false);
|
|
}
|
|
};
|
|
async function deleteContact(floAddress) {
|
|
const confirmed = await getConfirmation("Delete contact", {
|
|
message: "Are you sure you want to delete this contact?",
|
|
});
|
|
if (!confirmed) return;
|
|
compactIDB
|
|
.removeData("solanaContacts", floAddress)
|
|
.then(() => {
|
|
renderSearchedAddressList();
|
|
})
|
|
.catch((error) => {
|
|
console.error(error);
|
|
});
|
|
}
|
|
|
|
router.addRoute("send", (state) => {
|
|
getRef("page_container").dataset.page = "send";
|
|
renderElem(
|
|
getRef("page_container"),
|
|
html`
|
|
<sm-form id="send_tx_form" style="width: min(32rem, 100%)">
|
|
<fieldset class="flex flex-direction-column gap-0.5">
|
|
<div class="flex space-between align-center">
|
|
<div class="flex flex-direction-column gap-0.5">
|
|
<h4>Sender</h4>
|
|
<p>
|
|
Amount will be deducted from equivalent Solana address
|
|
</p>
|
|
</div>
|
|
<button
|
|
id="check_balance_button"
|
|
class="button button--small button--colored"
|
|
onclick="checkSenderBalance()"
|
|
disabled
|
|
>
|
|
Check balance
|
|
</button>
|
|
</div>
|
|
<sm-input
|
|
id="private_key_input"
|
|
placeholder="Sender's Solana Seed OR Bitcoin Private Key OR FLO Private Key"
|
|
oninput=${handleSenderInput}
|
|
data-private-key
|
|
class="password-field"
|
|
type="password"
|
|
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>
|
|
<div
|
|
id="sender_balance_container"
|
|
class="flex align-center gap-0-3 hidden"
|
|
></div>
|
|
</fieldset>
|
|
<fieldset class="flex flex-direction-column gap-1">
|
|
<div class="flex flex-direction-column gap-0.5">
|
|
<h4>Receiver</h4>
|
|
<div class="grid gap-0.5">
|
|
<sm-input
|
|
class="receiver-address"
|
|
placeholder="Receiver's Solana address"
|
|
data-sol-address
|
|
animate
|
|
required
|
|
></sm-input>
|
|
<div class="flex flex-direction-column gap-0.5">
|
|
<sm-input
|
|
class="receiver-amount amount-shown flex-1"
|
|
placeholder="SOL Amount"
|
|
type="number"
|
|
step="0.000001"
|
|
min="0.000001"
|
|
error-text="Amount should be grater than 0.000001 SOL"
|
|
animate
|
|
required
|
|
>
|
|
<div class="asset-symbol flex" slot="icon">
|
|
<svg
|
|
class="icon"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
viewBox="0 0 32 32"
|
|
fill="#000000"
|
|
>
|
|
<path
|
|
d="M16 0c8.837 0 16 7.163 16 16s-7.163 16-16 16S0 24.837 0 16 7.163 0 16 0zm8.706 19.517H10.34a.59.59 0 00-.415.17l-2.838 2.815a.291.291 0 00.207.498H21.66a.59.59 0 00.415-.17l2.838-2.816a.291.291 0 00-.207-.497zm-3.046-5.292H7.294l-.068.007a.291.291 0 00-.14.49l2.84 2.816.07.06c.1.07.22.11.344.11h14.366l.068-.007a.291.291 0 00.14-.49l-2.84-2.816-.07-.06a.59.59 0 00-.344-.11zM24.706 9H10.34a.59.59 0 00-.415.17l-2.838 2.816a.291.291 0 00.207.497H21.66a.59.59 0 00.415-.17l2.838-2.815A.291.291 0 0024.706 9z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</sm-input>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="multi-state-button">
|
|
<button
|
|
id="send_tx_button"
|
|
class="button button--primary"
|
|
type="submit"
|
|
disabled
|
|
onclick="sendTx()"
|
|
>
|
|
Send SOL
|
|
</button>
|
|
</div>
|
|
</fieldset>
|
|
</sm-form>
|
|
`
|
|
);
|
|
if (
|
|
window.ethereum &&
|
|
!(window.currentChainId && window.currentChainId === "0x1")
|
|
) {
|
|
renderError("Please switch MetaMask to Ethereum Mainnet");
|
|
}
|
|
});
|
|
|
|
function togglePrivateKeyVisibility(input) {
|
|
const target = input.closest("sm-input");
|
|
target.type = target.type === "password" ? "text" : "password";
|
|
target.focusIn();
|
|
}
|
|
|
|
function checkSenderBalance() {
|
|
let address;
|
|
const secret = getRef("private_key_input").value;
|
|
const pKey =
|
|
secret.length < 64
|
|
? floSolana.wif2SolanaSecret(secret)
|
|
: floSolana.solanaSeed2SolanaSecret(secret);
|
|
const privateKey = floSolana.solanaSecret2UsableInCode(pKey);
|
|
if (!privateKey)
|
|
return notify(`Please enter sender's private key to check balance`);
|
|
if (privateKey) {
|
|
const privateKeyArray = Uint8Array.from(
|
|
privateKey.toString().split(",").map(Number)
|
|
);
|
|
console.log(privateKeyArray);
|
|
const keypair = solanaWeb3.Keypair.fromSecretKey(privateKeyArray);
|
|
address = keypair.publicKey.toString();
|
|
console.log("solana address", address);
|
|
}
|
|
getRef("sender_balance_container").classList.remove("hidden");
|
|
renderElem(
|
|
getRef("sender_balance_container"),
|
|
html` Loading balance...<sm-spinner></sm-spinner> `
|
|
);
|
|
const promises = [getWalletBalance(address)];
|
|
|
|
Promise.all(promises)
|
|
.then(([solBalance, tokenBalance]) => {
|
|
renderElem(
|
|
getRef("sender_balance_container"),
|
|
html`
|
|
<div
|
|
class="grid gap-1 w-100"
|
|
style="padding: 1rem; border-radius: 0.5rem; border: solid thin rgba(var(--text-color),0.3)"
|
|
>
|
|
<div class="grid">
|
|
<p class="label">Sender address</p>
|
|
<sm-copy value=${address}
|
|
><p>${address}</p>
|
|
<p></p
|
|
></sm-copy>
|
|
</div>
|
|
<p>
|
|
Balance:
|
|
<b class="amount-shown">${solBalance} SOL</b>
|
|
${selectedAsset !== "sol"
|
|
? html`|
|
|
<b class="amount-shown"
|
|
>${tokenBalance} ${selectedAsset.toUpperCase()}</b
|
|
>`
|
|
: ""}
|
|
</p>
|
|
</div>
|
|
`
|
|
);
|
|
})
|
|
.catch((err) => {
|
|
notify(err, "error");
|
|
});
|
|
}
|
|
function handleSenderInput(e) {
|
|
getRef("check_balance_button").disabled = !e.target.isValid;
|
|
if (!e.target.isValid) {
|
|
getRef("sender_balance_container").classList.add("hidden");
|
|
}
|
|
}
|
|
function handleAssetChange(e) {
|
|
const asset = e.target.value;
|
|
const amountInput =
|
|
getRef("send_tx_form").querySelector(".receiver-amount");
|
|
amountInput.value = "";
|
|
amountInput.setAttribute(
|
|
"error-text",
|
|
`Amount should be grater than 0.000001 ${asset.toUpperCase()}`
|
|
);
|
|
}
|
|
async function sendTx() {
|
|
const receiver = getRef("send_tx_form")
|
|
.querySelector(".receiver-address")
|
|
.value.trim();
|
|
const amount = parseFloat(
|
|
getRef("send_tx_form").querySelector(".receiver-amount").value.trim()
|
|
);
|
|
const asset = "sol";
|
|
try {
|
|
const confirmation = await getConfirmation("Send transaction", {
|
|
message: `You are about to send ${amount} ${asset.toUpperCase()} to ${receiver}`,
|
|
confirmText: "Send",
|
|
});
|
|
buttonLoader("send_tx_button", true);
|
|
if (!confirmation) return;
|
|
|
|
const secret = getRef("private_key_input").value.trim();
|
|
const pKey =
|
|
secret.length < 64
|
|
? floSolana.wif2SolanaSecret(secret)
|
|
: floSolana.solanaSeed2SolanaSecret(secret);
|
|
let privateKey = floSolana.solanaSecret2UsableInCode(pKey);
|
|
|
|
console.log("private key", privateKey);
|
|
const senderKeypair = solanaWeb3.Keypair.fromSecretKey(privateKey);
|
|
|
|
// if (/^[0-9a-fA-F]{64}$/.test(privateKey)) {
|
|
// privateKey = coinjs.privkey2wif(privateKey);
|
|
// }
|
|
// privateKey = coinjs.wif2privkey(privateKey).privkey;
|
|
|
|
const receiverPublicKey = new solanaWeb3.PublicKey(receiver);
|
|
console.log("receiver", receiverPublicKey);
|
|
Buffer = ethereumjs.Buffer.Buffer;
|
|
const transaction = new solanaWeb3.Transaction().add(
|
|
solanaWeb3.SystemProgram.transfer({
|
|
fromPubkey: senderKeypair.publicKey,
|
|
toPubkey: receiverPublicKey,
|
|
lamports: amount * solanaWeb3.LAMPORTS_PER_SOL, // Convert SOL to lamports
|
|
})
|
|
);
|
|
|
|
const signature = await solanaWeb3.sendAndConfirmTransaction(
|
|
connection,
|
|
transaction,
|
|
[senderKeypair]
|
|
);
|
|
showTransactionResult("pending", { txHash: signature });
|
|
try {
|
|
await connection.confirmTransaction(signature, "confirmed");
|
|
showTransactionResult("confirmed", { txHash: signature });
|
|
} catch (error) {
|
|
showTransactionResult("failed", { description: error.message });
|
|
}
|
|
showTransactionResult("confirmed", { txHash: signature });
|
|
|
|
getRef("send_tx_form").reset();
|
|
getRef("sender_balance_container").classList.add("hidden");
|
|
|
|
const addressOfKey = floSolana.solanaSeed2SolanaAddress(secret);
|
|
const balance = await walletBalance(addressOfKey);
|
|
console.log("balance", balance);
|
|
} catch (e) {
|
|
console.error(e.message);
|
|
if (e instanceof solanaWeb3.SendTransactionError) {
|
|
console.error("Transaction simulation failed:", e.message);
|
|
|
|
// Attempt to get logs
|
|
const logs = e.getLogs();
|
|
if (logs && logs.length > 0) {
|
|
console.error("Transaction logs:", logs);
|
|
} else {
|
|
console.error("No logs available.");
|
|
}
|
|
notify(
|
|
"Transaction failed. Please check your account balance and try again.",
|
|
"error"
|
|
);
|
|
showTransactionResult("failed", {
|
|
description:
|
|
"Transaction failed. Please check your account balance and try again.",
|
|
});
|
|
}
|
|
if (e.message) {
|
|
const regex = /\(error=({.*?}),/;
|
|
const match = e.message.match(regex);
|
|
if (match && match[1]) {
|
|
const { code } = JSON.parse(match[1]);
|
|
if (code === -32000) {
|
|
showTransactionResult("failed", {
|
|
description: `Insufficient ${asset.toUpperCase()} balance`,
|
|
});
|
|
} else {
|
|
showTransactionResult("failed", { description: e.message });
|
|
}
|
|
} else {
|
|
showTransactionResult("failed", { description: e.message });
|
|
}
|
|
} else {
|
|
showTransactionResult("failed", { description: e.message });
|
|
}
|
|
} finally {
|
|
buttonLoader("send_tx_button", false);
|
|
}
|
|
}
|
|
|
|
function showTransactionResult(status, { txHash, description = "" }) {
|
|
switch (status) {
|
|
case "pending":
|
|
renderElem(
|
|
getRef("transaction_result_popup__content"),
|
|
html`
|
|
<ul>
|
|
<li class="transaction__phase">
|
|
<svg
|
|
class="icon confirmed"
|
|
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>
|
|
<h4>Transaction sent</h4>
|
|
</li>
|
|
<li class="transaction__phase">
|
|
<sm-spinner
|
|
class="justify-self-center margin-top-1-5"
|
|
></sm-spinner>
|
|
<p>Waiting for transaction to be confirmed</p>
|
|
</li>
|
|
</ul>
|
|
<div class="grid">
|
|
<span class="label">Transaction ID</span>
|
|
<sm-copy value=${txHash}></sm-copy>
|
|
</div>
|
|
<a
|
|
class="button button--primary"
|
|
target="_blank"
|
|
href=${`https://explorer.solana.com/tx/${txHash}`}
|
|
>Check transaction status</a
|
|
>
|
|
`
|
|
);
|
|
break;
|
|
case "confirmed":
|
|
renderElem(
|
|
getRef("transaction_result_popup__content"),
|
|
html`
|
|
<svg
|
|
class="icon user-action-result__icon confirmed"
|
|
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 confirmed</h4>
|
|
<p>Transaction has been confirmed on the blockchain.</p>
|
|
</div>
|
|
<div class="grid">
|
|
<span class="label">Transaction ID</span>
|
|
<sm-copy value=${txHash}></sm-copy>
|
|
</div>
|
|
<a
|
|
class="button button--primary"
|
|
target="_blank"
|
|
href=${`https://explorer.solana.com/tx/${txHash}`}
|
|
>Check transaction status</a
|
|
>
|
|
`
|
|
);
|
|
break;
|
|
case "failed":
|
|
renderElem(
|
|
getRef("transaction_result_popup__content"),
|
|
html`
|
|
<svg
|
|
class="icon user-action-result__icon failed"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
height="24px"
|
|
viewBox="0 0 24 24"
|
|
width="24px"
|
|
fill="#000000"
|
|
>
|
|
<path
|
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"
|
|
/>
|
|
</svg>
|
|
<div class="grid gap-0-5 justify-center text-center">
|
|
<h4>Transaction failed</h4>
|
|
<p>${description}</p>
|
|
</div>
|
|
`
|
|
);
|
|
break;
|
|
}
|
|
openPopup("transaction_result_popup");
|
|
}
|
|
|
|
router.addRoute("create", (state) => {
|
|
getRef("page_container").dataset.page = "create";
|
|
renderElem(
|
|
getRef("page_container"),
|
|
html`
|
|
<div class="grid gap-1">
|
|
<h2>
|
|
Don't have an Solana Address OR Bitcoin Private Key OR FLO
|
|
Private Key? Create one
|
|
</h2>
|
|
<section class="create-buttons">
|
|
<button
|
|
class="button button--primary interactive gap-0-5 margin-right-auto"
|
|
onclick=${generateNewID}
|
|
>
|
|
<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 new address
|
|
</button>
|
|
<button
|
|
class="button button--primary interactive gap-0-5 margin-right-auto"
|
|
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 Solana address
|
|
</button>
|
|
</section>
|
|
</div>
|
|
<div id="created_address_wrapper" class="grid gap-1"></div>
|
|
`
|
|
);
|
|
});
|
|
function bnObjectToUint8(bnObject) {
|
|
const bn = bnObject._bn; // Extract the BN instance from the object
|
|
const words = bn.words; // Get the words array from the BN instance
|
|
|
|
// Convert each word to its corresponding bytes
|
|
const byteArray = [];
|
|
for (let i = 0; i < words.length; i++) {
|
|
const word = words[i];
|
|
byteArray.push((word >> 24) & 0xff); // Extract first byte
|
|
byteArray.push((word >> 16) & 0xff); // Extract second byte
|
|
byteArray.push((word >> 8) & 0xff); // Extract third byte
|
|
byteArray.push(word & 0xff); // Extract fourth byte
|
|
}
|
|
|
|
// Create Uint8Array from the byte array
|
|
const uint8Array = new Uint8Array(byteArray);
|
|
|
|
return uint8Array;
|
|
}
|
|
|
|
function retrieveSolanaAddr() {
|
|
function retrieve() {
|
|
let seed = getRef("retrieve_btc_addr_field").value.trim();
|
|
getRef("recovered_btc_addr_wrapper").classList.remove("hidden");
|
|
getRef("recovered_btc_addr").value =
|
|
floSolana.solanaSeed2SolanaAddress(seed);
|
|
}
|
|
if (document.startViewTransition) {
|
|
document.startViewTransition(() => {
|
|
retrieve();
|
|
});
|
|
} else retrieve();
|
|
}
|
|
|
|
function generateNewID() {
|
|
const solanaKeyPair = floSolana.generateSolanaKeyPair();
|
|
renderElem(
|
|
getRef("created_address_wrapper"),
|
|
html`
|
|
<ul id="generated_addresses" class="grid gap-1-5">
|
|
<li class="grid gap-0-5">
|
|
<div>
|
|
<h5>Solana Seed</h5>
|
|
<sm-copy value="${solanaKeyPair.seed}"></sm-copy>
|
|
</div>
|
|
<div>
|
|
<h5>Solana Address</h5>
|
|
<sm-copy value="${solanaKeyPair.publicKey}"></sm-copy>
|
|
</div>
|
|
</li>
|
|
<li class="grid gap-0-5">
|
|
<div>
|
|
<h5>Bitcoin Private Key</h5>
|
|
<sm-copy value="${solanaKeyPair.bitcoinWif}"></sm-copy>
|
|
</div>
|
|
<div>
|
|
<h5>Bitcoin Address</h5>
|
|
<sm-copy value="${solanaKeyPair.bitcoinAddress}"></sm-copy>
|
|
</div>
|
|
</li>
|
|
<li class="grid gap-0-5">
|
|
<div>
|
|
<h5>Flo Private Key</h5>
|
|
<sm-copy value="${solanaKeyPair.floWif}"></sm-copy>
|
|
</div>
|
|
<div>
|
|
<h5>Flo Address</h5>
|
|
<sm-copy value="${solanaKeyPair.floAddress}"></sm-copy>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
`
|
|
);
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |