257 lines
8.5 KiB
JavaScript
257 lines
8.5 KiB
JavaScript
|
|
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("<", "<")
|
||
|
|
.replaceAll(">", ">")
|
||
|
|
.replaceAll('"', """)
|
||
|
|
.replaceAll("'", "'");
|
||
|
|
}
|
||
|
|
|
||
|
|
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();
|
||
|
|
});
|
||
|
|
});
|