Contractless-GUI-Wallet/frontend/js/assets_page.js

257 lines
8.5 KiB
JavaScript
Raw Normal View History

2026-07-01 16:01:30 +00:00
let assetsCatalog = [];
let assetsLoadedForNode = "";
let assetsLoadInFlight = false;
let assetsRefreshId = 0;
let activeAssetFilter = "all";
let assetsEmptyStateMessage = "";
function invalidateAssetsCatalog() {
assetsLoadedForNode = "";
}
function assetCatalogBalance(asset) {
const ticker = String(asset || "").trim().toLowerCase();
const match = activeWalletBalances.find((balance) => {
return (
String(balance.asset || "").trim().toLowerCase() === ticker &&
Number(balance.nft_series || 0) === 0
);
});
return match ? String(match.balance || "0") : "0";
}
function formatAssetSupply(value) {
return formatAtomicBalance(value || 0);
}
function assetCreatorLabel(asset, isCreator) {
const creator = String(asset.creator || "").trim();
if (isCreator) {
return "You";
}
return creator ? shortenAddress(creator) : "Unknown";
}
function formatAssetDate(timestamp) {
const numeric = Number(timestamp || 0);
if (!numeric) {
return "--";
}
return new Date(numeric * 1000).toLocaleDateString();
}
function escapeAssetText(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function compareAssetBalance(left, right) {
const leftBalance = BigInt(assetCatalogBalance(left.ticker));
const rightBalance = BigInt(assetCatalogBalance(right.ticker));
if (rightBalance !== leftBalance) {
return rightBalance > leftBalance ? 1 : -1;
}
return String(left.ticker || "").localeCompare(String(right.ticker || ""));
}
function sortedAssetCards() {
const query = String(assetsSearch?.value || "").trim().toLowerCase();
const sortMode = assetsSort?.value || "default";
const activeShortAddress = String(activeWalletAddressShort || "").toLowerCase();
const sourceRows = assetsCatalog;
let rows = sourceRows.filter((asset) => {
const ticker = String(asset.ticker || "").trim();
if (!ticker) {
return false;
}
const balance = BigInt(assetCatalogBalance(ticker));
const creator = String(asset.creator || "").toLowerCase();
const searchText = `${ticker} ${asset.origin_txid || ""} ${asset.creator || ""}`.toLowerCase();
if (activeAssetFilter === "holding" && balance <= 0n) {
return false;
}
if (activeAssetFilter === "created" && creator !== activeShortAddress) {
return false;
}
return !query || searchText.includes(query);
});
if (sortMode === "balance" || sortMode === "default") {
rows.sort(compareAssetBalance);
if (sortMode === "default") {
rows.sort((left, right) => {
const leftHolding = BigInt(assetCatalogBalance(left.ticker)) > 0n ? 0 : 1;
const rightHolding = BigInt(assetCatalogBalance(right.ticker)) > 0n ? 0 : 1;
return leftHolding - rightHolding || compareAssetBalance(left, right);
});
}
} else if (sortMode === "created") {
rows.sort((left, right) => Number(right.created_timestamp || 0) - Number(left.created_timestamp || 0));
} else {
rows.sort((left, right) => String(left.ticker || "").localeCompare(String(right.ticker || "")));
}
return rows;
}
function renderAssetCard(asset) {
const ticker = String(asset.ticker || "").trim();
const balance = assetCatalogBalance(ticker);
const isHolding = BigInt(balance || "0") > 0n;
const isCreator = String(asset.creator || "").toLowerCase() === String(activeWalletAddressShort || "").toLowerCase();
const policy = asset.hard_limit ? "Fixed Supply" : "Issuable";
const escapedTicker = escapeAssetText(ticker);
const escapedPolicy = escapeAssetText(policy);
const creatorLabel = assetCreatorLabel(asset, isCreator);
return `
<article class="portfolio-card">
<div class="portfolio-top">
<div class="asset-mark">${escapeAssetText(assetInitial(ticker))}</div>
<div>
<strong>${escapedTicker}</strong>
<span>${escapedPolicy}</span>
</div>
</div>
<b>${escapeAssetText(formatAtomicBalance(balance))} ${escapedTicker}</b>
<div class="asset-card-tags">
${isHolding ? '<span class="asset-card-tag holding">Holding</span>' : ""}
${isCreator ? '<span class="asset-card-tag created">Created by me</span>' : ""}
<span class="asset-card-tag ${asset.hard_limit ? "fixed" : ""}">${escapedPolicy}</span>
</div>
<div class="portfolio-meta">
<div><span>Current Supply</span><strong>${escapeAssetText(formatAssetSupply(asset.current_supply))}</strong></div>
<div><span>Total Burned</span><strong>${escapeAssetText(formatAssetSupply(asset.total_burned))}</strong></div>
<div><span>Holders</span><strong>${escapeAssetText(formatNumber(asset.holder_count))}</strong></div>
<div><span>Created</span><strong>${escapeAssetText(formatAssetDate(asset.created_timestamp))}</strong></div>
<div><span>Initial Supply</span><strong>${escapeAssetText(formatAssetSupply(asset.initial_supply))}</strong></div>
<div><span>Creator</span><strong>${escapeAssetText(creatorLabel)}</strong></div>
</div>
<code class="asset-origin">${escapeAssetText(asset.origin_txid || "")}</code>
</article>
`;
}
function setAssetsState(message) {
if (assetsState) {
assetsState.textContent = message || "";
}
}
function renderAssetsPage() {
if (!assetsCards) {
return;
}
const rows = sortedAssetCards();
assetsCards.innerHTML = rows.map(renderAssetCard).join("");
if (!walletLoaded) {
setAssetsState("Load a wallet to view assets.");
} else if (assetsLoadInFlight && !assetsCatalog.length) {
setAssetsState("Loading assets...");
} else if (!assetsCatalog.length) {
setAssetsState(assetsEmptyStateMessage || "Token catalog returned no rows.");
} else if (!rows.length) {
setAssetsState("No assets match the current filter.");
} else {
setAssetsState(`${rows.length} token${rows.length === 1 ? "" : "s"} shown`);
}
}
async function loadAssetsCatalog({ force = false } = {}) {
const invoke = window.__TAURI__?.core?.invoke;
const initialBroadcastNode = activeBroadcastNode();
if (!walletLoaded) {
assetsCatalog = [];
assetsEmptyStateMessage = "Asset catalog skipped: no wallet is loaded.";
renderAssetsPage();
return;
}
if (!invoke) {
assetsCatalog = [];
assetsEmptyStateMessage = "Asset catalog skipped: Tauri invoke is unavailable.";
renderAssetsPage();
return;
}
if (!initialBroadcastNode) {
assetsCatalog = [];
assetsEmptyStateMessage = "Asset catalog skipped: no broadcast node is selected.";
renderAssetsPage();
return;
}
const broadcastNode = initialBroadcastNode;
if (!force && assetsCatalog.length && assetsLoadedForNode === broadcastNode) {
renderAssetsPage();
return;
}
if (assetsLoadInFlight) {
return;
}
const refreshId = ++assetsRefreshId;
assetsLoadInFlight = true;
assetsEmptyStateMessage = "Calling token catalog RPC command 48...";
renderAssetsPage();
try {
const catalog = await invokeWalletRpcWithBackoff(
"asset catalog",
() => invoke("token_catalog", { broadcastNode }),
() => refreshId === assetsRefreshId && activeBroadcastNode() === broadcastNode
);
if (refreshId !== assetsRefreshId || activeBroadcastNode() !== broadcastNode) {
return;
}
assetsCatalog = Array.isArray(catalog) ? catalog : [];
assetsEmptyStateMessage = assetsCatalog.length
? ""
: "Token catalog command 48 returned 0 rows from the selected node.";
assetsLoadedForNode = broadcastNode;
} catch (error) {
if (refreshId === assetsRefreshId) {
assetsCatalog = [];
assetsEmptyStateMessage = `Token catalog command 48 failed: ${String(error)}`;
setAssetsState(String(error));
}
} finally {
if (refreshId === assetsRefreshId) {
assetsLoadInFlight = false;
renderAssetsPage();
}
}
}
function prepareAssetsPage() {
renderAssetsPage();
loadAssetsCatalog({ force: true });
}
function resetAssetsPage() {
assetsRefreshId += 1;
assetsCatalog = [];
assetsEmptyStateMessage = "";
assetsLoadedForNode = "";
assetsLoadInFlight = false;
renderAssetsPage();
}
assetsSearch?.addEventListener("input", renderAssetsPage);
assetsSort?.addEventListener("change", renderAssetsPage);
assetsFilterButtons.forEach((button) => {
button.addEventListener("click", () => {
activeAssetFilter = button.dataset.assetsFilter || "all";
assetsFilterButtons.forEach((item) => {
item.classList.toggle("active", item === button);
});
renderAssetsPage();
});
});