solanawallet/index.html

3812 lines
147 KiB
HTML

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