solanawallet/index.html
void-57 5666fe5d34 Add transaction details and transaction history features
Implemented transaction details view for Solana transactions, including sender, receiver, value, fee, slot, and status.
Added transaction history fetching, pagination, and filtering (all/sent/received).
Improved UI for transaction navigation and error handling.
2026-01-13 22:14:40 +05:30

3600 lines
136 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;
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() {
if (!hasMoreTransactions) return;
// Show loading state
this.container.innerHTML =
'<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
try {
const addressInput = currentWalletAddress;
if (!addressInput) {
this.container.innerHTML =
'<p class="text-center margin-top-2">No address input found</p>';
return;
}
const address = addressInput;
if (!address) {
this.container.innerHTML =
'<p class="text-center margin-top-2">No address specified</p>';
return;
}
console.log("Fetching next page for address:", address);
try {
new solanaWeb3.PublicKey(address);
} catch (error) {
this.container.innerHTML =
'<p class="text-center margin-top-2">Invalid address</p>';
return;
}
if (!lastFetchedSignature) {
this.container.innerHTML =
'<p class="text-center margin-top-2">No more transactions to load</p>';
hasMoreTransactions = false;
return;
}
console.log(
"Fetching next page with signature:",
lastFetchedSignature
);
// Get next batch of signatures using the last signature from previous batch
const newSignatures = await getSolanaTransactionHistory(
address,
lastFetchedSignature,
false
);
console.log("Fetched new signatures:", newSignatures?.length);
if (!newSignatures || newSignatures.length === 0) {
hasMoreTransactions = false;
this.container.innerHTML =
'<p class="text-center margin-top-2">No more transactions to load</p>';
// Update button states
getRef("next_page_button").disabled = true;
return;
}
// Process the new transactions
const newTransactions = await processTransactions(newSignatures);
// Format and filter the transactions using the specific formatTransaction function
const formattedTxs = newTransactions.map((tx) => {
return formatTransaction(address, tx);
});
// Save this page for instant backward navigation
transactionPageCache.set(currentPage + 1, {
signatures: newSignatures,
transactions: newTransactions,
formatted: formattedTxs
});
// Apply current filter
const filterSelector = document.getElementById("filter_selector");
const filter = filterSelector ? filterSelector.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"
);
}
// Clear previous content and show new transactions
this.container.innerHTML = "";
// Add new transactions to the display
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>';
}
currentPage++;
// Update page indicator
const pageInfo = document.getElementById("page_info");
if (pageInfo) pageInfo.textContent = `Page ${currentPage}`;
// Update button states
const prevButton = document.querySelector(
".pagination-controls button:first-child"
);
if (prevButton) prevButton.disabled = false;
const nextButton = document.getElementById("next_page_button");
if (nextButton) nextButton.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 = 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
currentPage = targetPage;
// Update page indicator
const pageInfo = document.getElementById("page_info");
if (pageInfo) pageInfo.textContent = `Page ${currentPage}`;
// Update button states
const prevButton = document.querySelector(
".pagination-controls button:first-child"
);
if (prevButton) prevButton.disabled = 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 with proper null checks
let fromAddress = "N/A";
let toAddress = "N/A";
// Add null checks before accessing transaction properties
if (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);
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,
};
} 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 (postBalance - preBalance) / solanaWeb3.LAMPORTS_PER_SOL;
} 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();
}
// Handle filter changes
getRef("filter_selector").addEventListener("change", async (e) => {
const address = currentWalletAddress;
if (!address) return;
// Show loading state
getRef("transactions_list").innerHTML =
'<sm-spinner class="justify-self-center margin-top-1-5"></sm-spinner>';
try {
// Don't refetch transactions - use the already processed ones stored in memory
// Just filter what we already have
const filter = e.target.value || "all";
let filteredTransactions;
if (filter === "sent") {
filteredTransactions = formattedTxs.filter(
(tx) => tx.type === "out"
);
} else if (filter === "received") {
filteredTransactions = formattedTxs.filter(
(tx) => tx.type === "in"
);
} else {
// "all" filter - use all transactions
filteredTransactions = formattedTxs;
}
// Clear container
getRef("transactions_list").innerHTML = "";
// Render filtered transactions
if (filteredTransactions.length > 0) {
for (const tx of filteredTransactions) {
try {
const card = await render.transactionCard(tx);
if (card) getRef("transactions_list").appendChild(card);
} catch (error) {
console.error("Error rendering transaction card:", error);
}
}
} else {
getRef("transactions_list").innerHTML =
'<p class="text-center margin-top-2">No transactions found for this filter</p>';
}
// Update pagination info
const pageInfo = document.getElementById("page_info");
if (pageInfo) pageInfo.textContent = `Page ${currentPage}`;
// Update button states
const prevButton = document.querySelector(
".pagination-controls button:first-child"
);
if (prevButton) prevButton.disabled = currentPage <= 1;
const nextButton = document.getElementById("next_page_button");
if (nextButton) nextButton.disabled = !hasMoreTransactions;
} catch (error) {
console.error("Error applying filter:", error);
getRef("transactions_list").innerHTML =
'<p class="text-center margin-top-2">Error filtering transactions</p>';
}
});
// 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">
<div class="transaction__type" style="font-weight: 500; margin-right: 8px;">
${transactionDetails.type ===
"out"
? "Sent"
: "Received"
}
</div>
<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.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.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 [];
}
}
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=${(e) =>
render.transactions(
getRef("wallet_address_input").value
)}
>
<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);
// Extract transaction information
const value =
tx.meta?.postBalances && tx.meta?.preBalances
? Math.abs(tx.meta.postBalances[0] - tx.meta.preBalances[0]) /
solanaWeb3.LAMPORTS_PER_SOL
: 0;
const fee = tx.meta?.fee
? tx.meta.fee / solanaWeb3.LAMPORTS_PER_SOL
: 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">
<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>
<!-- 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>
<!-- 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}` : "";
}
// Fill in transaction details
if (getRef("tx_from")) {
const sender =
tx.transaction?.message?.accountKeys?.[0]?.toString() ||
"Unknown";
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")) {
const receiver =
tx.transaction?.message?.accountKeys?.[1]?.toString() ||
"Unknown";
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";
}
if (getRef("tx_hash")) getRef("tx_hash").value = 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();
}
let sender = "Unknown";
let receiver = "Unknown";
const message = tx.transaction?.message;
const accountKeys =
message?.staticAccountKeys || message?.accountKeys || [];
const instructions =
message?.compiledInstructions || message?.instructions || [];
// Try to extract from parsed instructions (for parsed transactions)
if (instructions && instructions.length > 0) {
// Try to find a transfer instruction (SPL or System)
for (const ix of instructions) {
// For parsed transactions (if available)
if (ix.parsed && ix.parsed.info) {
if (ix.parsed.info.source && ix.parsed.info.destination) {
sender = ix.parsed.info.source;
receiver = ix.parsed.info.destination;
break;
}
if (ix.parsed.info.authority && ix.parsed.info.destination) {
sender = ix.parsed.info.authority;
receiver = ix.parsed.info.destination;
break;
}
}
// For raw instructions (like in your prompt)
if (ix.accountKeyIndexes && ix.accountKeyIndexes.length >= 2) {
sender = accountKeys[ix.accountKeyIndexes[0]] || "Unknown";
receiver = accountKeys[ix.accountKeyIndexes[1]] || "Unknown";
break;
}
}
}
// Fallback: just use first two account keys
if (sender === "Unknown" && accountKeys[0]) sender = accountKeys[0];
if (receiver === "Unknown" && accountKeys[1])
receiver = accountKeys[1];
if (getRef("tx_from")) getRef("tx_from").value = sender;
if (getRef("tx_to")) getRef("tx_to").value = receiver;
})
.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></p>
<button
<button
</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 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>