let ownedNfts = []; let ownedNftRows = []; let selectedNftKey = ""; let nftsLoadId = 0; let nftsLoading = false; let nftsPage = 1; let nftsCachingVisible = false; const NFTS_PAGE_SIZE = 30; const nftMetadataCache = new Map(); function nftKey(nft) { return `${String(nft.nft_name || nft.asset || "").trim().toLowerCase()}|${Number(nft.series || nft.nft_series || 0)}`; } function nftCacheKey(nft) { return `${String(nft.nft_name || nft.asset || "").trim().toLowerCase()}_${Number(nft.series || nft.nft_series || 0)}`; } function escapeNftText(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function shortenNftTx(txid) { const value = String(txid || ""); return value.length > 16 ? `${value.slice(0, 6)}...${value.slice(-8)}` : value || "--"; } function nftDisplayName(nft) { const metadataName = String(nft.metadata?.name || "").trim(); if (metadataName) { return metadataName; } const name = String(nft.nft_name || nft.asset || "NFT").trim(); return Number(nft.series || 0) > 0 ? `${name} #${Number(nft.series)}` : name; } function nftImageUri(nft) { const cachedImage = String(nft.image_data_url || nft.imageDataUrl || "").trim(); if (cachedImage) { return cachedImage; } return String( nft.metadata?.image || nft.metadata?.image_url || nft.metadata?.animation_url || "" ).trim(); } function normalizeIpfsGateway(gateway) { const fallback = "https://gateway.pinata.cloud/ipfs/"; const clean = String(gateway || "").trim() || fallback; return clean.endsWith("/") ? clean : `${clean}/`; } function normalizeIpfsPath(value) { let path = String(value || "").trim(); let previous = ""; while (path && path !== previous) { previous = path; path = path .replace(/^ipfs:\/\//i, "") .replace(/^ipfs:\//i, "") .replace(/^ipfs:/i, "") .replace(/^\/?ipfs\//i, "") .replace(/^\/+/, ""); } return path; } function ipfsToGatewayUrl(uri, gateway) { const value = String(uri || "").trim(); if (!value) { return ""; } if (value.startsWith("http://") || value.startsWith("https://")) { return value; } if (value.startsWith("data:image/")) { return value; } const path = normalizeIpfsPath(value); return `${normalizeIpfsGateway(gateway)}${path}`; } function nftTypeLabel(nft) { const metadataType = String(nft.metadata?.contractless?.metadata_type || "").trim(); if (metadataType.toLowerCase() === "rwa") { return "RWA"; } if (nftImageUri(nft)) { return "Image NFT"; } return "NFT"; } function setNftsState(message) { if (nftsState) { nftsState.textContent = message || ""; } } function setNftDetailText(element, value) { if (element) { element.textContent = value || "--"; } } function resetNftDetails(message = "Select an owned NFT.") { if (nftDetailTitle) { nftDetailTitle.textContent = "NFT Details"; } if (nftDetailSubtitle) { nftDetailSubtitle.textContent = message; } setNftDetailText(nftDetailName, ""); setNftDetailText(nftDetailAsset, ""); setNftDetailText(nftDetailCreator, ""); setNftDetailText(nftDetailGenesis, ""); setNftDetailText(nftDetailMetadata, ""); if (nftMetadataSummary) { nftMetadataSummary.innerHTML = ""; } if (nftHistory) { nftHistory.innerHTML = ""; } } function hasOwnedBalance(balance) { try { return BigInt(String(balance?.balance || "0")) > 0n; } catch (_) { return Number(balance?.balance || 0) > 0; } } function ownedNftRowsFromBalances(balances, registry) { const normalizedBalances = Array.isArray(balances) ? balances : []; const registryRows = Array.isArray(registry) ? registry : []; const balanceByAsset = new Map(); normalizedBalances.forEach((balance) => { if (!hasOwnedBalance(balance)) { return; } const asset = String(balance.asset || "").trim().toLowerCase(); const series = Number(balance.nft_series || 0); balanceByAsset.set(`${asset}|${series}`, balance); }); return registryRows .map((nft) => { const nftName = String(nft.nft_name || "").trim(); const series = Number(nft.series || 0); const balance = balanceByAsset.get(`${nftName.toLowerCase()}|${series}`); if (!balance) { return null; } return { asset: nftName, nft_name: nftName, series, balance: String(balance.balance || "0"), origin_txid: nft.origin_txid || "" }; }) .filter(Boolean) .filter((nft) => nft.nft_name); } function legacyOwnedNftRowsFromBalances(balances) { return (Array.isArray(balances) ? balances : []) .filter((balance) => { const series = Number(balance.nft_series || 0); let owned = false; try { owned = BigInt(String(balance.balance || "0")) > 0n; } catch (_) { owned = Number(balance.balance || 0) > 0; } return series > 0 && owned; }) .map((balance) => ({ asset: String(balance.asset || "").trim(), nft_name: String(balance.asset || "").trim(), series: Number(balance.nft_series || 0), balance: String(balance.balance || "0") })) .filter((nft) => nft.nft_name); } function metadataDescription(metadata) { return String(metadata?.description || metadata?.summary || "").trim(); } function renderNftCard(nft) { const key = nftKey(nft); const name = nftDisplayName(nft); const image = nftImageUri(nft); const type = nftTypeLabel(nft); const active = key === selectedNftKey ? " active" : ""; const preview = image && nft.gateway ? `` : `
${escapeNftText(assetInitial(name))}
`; return `
${preview}
${escapeNftText(name)} ${escapeNftText(type)} - ${escapeNftText(nft.asset_name || `${nft.nft_name}_${nft.series}`)}
`; } function renderNftHistoryEntry(entry) { const asset = entry.received_asset ? `${entry.received_asset}${Number(entry.received_series || 0) > 0 ? ` #${entry.received_series}` : ""}` : ""; return `
${escapeNftText(entry.action || "Unknown")} Block ${escapeNftText(formatNumber(entry.block || 0))}
${escapeNftText(shortenNftTx(entry.txid || ""))} ${entry.from ? `
From${escapeNftText(shortenAddress(entry.from))}
` : ""} ${entry.to ? `
To${escapeNftText(shortenAddress(entry.to))}
` : ""} ${asset ? `
Received${escapeNftText(asset)}
` : ""}
`; } function renderNftMetadataSummary(nft) { const metadata = nft.metadata || {}; const description = metadataDescription(metadata); const attributes = Array.isArray(metadata.attributes) ? metadata.attributes : []; const contractless = metadata.contractless && typeof metadata.contractless === "object" ? metadata.contractless : null; const parts = []; if (description) { parts.push(`

${escapeNftText(description)}

`); } if (attributes.length) { parts.push(`
${attributes.slice(0, 8).map((attribute) => `
${escapeNftText(attribute.trait_type || attribute.type || "Trait")} ${escapeNftText(attribute.value ?? "")}
`).join("")}
`); } if (contractless) { parts.push(`
${Object.entries(contractless).slice(0, 6).map(([key, value]) => `
${escapeNftText(key)}${escapeNftText(value)}
`).join("")}
`); } return parts.join("") || "

No metadata summary available.

"; } function renderNftDetails(nft) { if (!nft) { resetNftDetails(); return; } const name = nftDisplayName(nft); if (nftDetailTitle) { nftDetailTitle.textContent = name; } if (nftDetailSubtitle) { nftDetailSubtitle.textContent = nftTypeLabel(nft); } setNftDetailText(nftDetailName, name); setNftDetailText(nftDetailAsset, nft.asset_name || `${nft.nft_name}_${nft.series}`); setNftDetailText(nftDetailCreator, nft.creator ? shortenAddress(nft.creator) : ""); setNftDetailText(nftDetailGenesis, nft.genesis_txid ? shortenNftTx(nft.genesis_txid) : ""); setNftDetailText(nftDetailMetadata, nft.metadata_uri || nft.item_ipfs || ""); if (nftMetadataSummary) { nftMetadataSummary.innerHTML = renderNftMetadataSummary(nft); } if (nftHistory) { nftHistory.innerHTML = Array.isArray(nft.history) && nft.history.length ? nft.history.map(renderNftHistoryEntry).join("") : '
No provenance records returned.
'; } } function renderNftsPage() { if (!nftsList) { return; } nftsList.innerHTML = ownedNfts.map(renderNftCard).join(""); const selected = ownedNfts.find((nft) => nftKey(nft) === selectedNftKey) || ownedNfts[0]; selectedNftKey = selected ? nftKey(selected) : ""; renderNftDetails(selected); if (!walletLoaded) { setNftsState("Load a wallet to view NFTs."); } else if (!ownedNftRows.length) { setNftsState(nftsLoading ? "Loading owned NFTs..." : "No owned NFTs found for this address."); } else if (!ownedNfts.length) { const start = (nftsPage - 1) * NFTS_PAGE_SIZE + 1; const end = Math.min(start + NFTS_PAGE_SIZE - 1, ownedNftRows.length); const suffix = nftsCachingVisible ? " - Caching media..." : ""; setNftsState(`Loading ${start}-${end} of ${ownedNftRows.length} NFTs${suffix}`); } else { const start = (nftsPage - 1) * NFTS_PAGE_SIZE + 1; const end = Math.min(start + ownedNfts.length - 1, ownedNftRows.length); const suffix = nftsCachingVisible ? " - Caching media..." : ""; setNftsState(`Showing ${start}-${end} of ${ownedNftRows.length} NFTs${suffix}`); } renderNftsPagination(); } async function loadNftMetadata(invoke, detail, gateway) { const metadataUri = detail.metadata_uri || detail.item_ipfs || ""; if (!metadataUri) { return null; } const cacheKey = `${gateway}|${metadataUri}`; if (nftMetadataCache.has(cacheKey)) { return nftMetadataCache.get(cacheKey); } const metadata = await invoke("fetch_ipfs_metadata", { ipfsPath: metadataUri, gateway }); nftMetadataCache.set(cacheKey, metadata); return metadata; } function nftFromCache(row, cached, gateway) { const detail = cached?.detail && typeof cached.detail === "object" ? cached.detail : {}; const metadata = cached?.metadata && typeof cached.metadata === "object" ? cached.metadata : {}; return { ...row, ...detail, metadata, image_data_url: cached?.image_data_url || "", gateway }; } function renderNftsPagination() { if (!nftsPagination) { return; } const totalPages = Math.max(1, Math.ceil(ownedNftRows.length / NFTS_PAGE_SIZE)); const showPagination = ownedNftRows.length > NFTS_PAGE_SIZE; nftsPagination.hidden = !showPagination; if (nftsPageTotal) { nftsPageTotal.textContent = String(totalPages); } if (nftsPageInput && Number(nftsPageInput.value) !== nftsPage) { nftsPageInput.value = String(nftsPage); } document.querySelectorAll("[data-nfts-page]").forEach((button) => { const action = button.dataset.nftsPage; button.disabled = !showPagination || (["first", "previous"].includes(action) && nftsPage <= 1) || (["next", "last"].includes(action) && nftsPage >= totalPages); }); } function clampNftsPage(page) { const totalPages = Math.max(1, Math.ceil(ownedNftRows.length / NFTS_PAGE_SIZE)); return Math.min(Math.max(1, Number(page) || 1), totalPages); } function visibleNftRows() { const start = (nftsPage - 1) * NFTS_PAGE_SIZE; return ownedNftRows.slice(start, start + NFTS_PAGE_SIZE); } async function hydrateVisibleNftPage({ loadId, gateway, broadcastNode, force }) { const invoke = window.__TAURI__?.core?.invoke; if (!invoke || loadId !== nftsLoadId) { return; } const rows = visibleNftRows(); const loaded = []; nftsCachingVisible = false; for (const row of rows) { if (loadId !== nftsLoadId || currentView !== "nfts") { return; } try { if (!force) { const cached = await invoke("read_nft_cache", { nftName: row.nft_name, series: row.series }); if (cached) { loaded.push(nftFromCache(row, cached, gateway)); ownedNfts = loaded.slice(); renderNftsPage(); continue; } } nftsCachingVisible = true; renderNftsPage(); const detail = await invoke("nft_details", { broadcastNode, nftName: row.nft_name, series: row.series }); let metadata = null; try { metadata = await loadNftMetadata(invoke, detail, gateway); } catch (error) { metadata = { description: `Metadata unavailable: ${String(error)}` }; } const imageUrl = ipfsToGatewayUrl( String(metadata?.image || metadata?.image_url || metadata?.animation_url || ""), gateway ); let cacheRecord = null; try { cacheRecord = await invoke("write_nft_cache", { nftName: row.nft_name, series: row.series, detailJson: JSON.stringify(detail), metadataJson: JSON.stringify(metadata || {}), imageUrl }); } catch (_) { cacheRecord = null; } loaded.push(cacheRecord ? nftFromCache(row, cacheRecord, gateway) : { ...row, ...detail, metadata, gateway }); ownedNfts = loaded.slice(); renderNftsPage(); } catch (error) { loaded.push({ ...row, metadata: { description: `NFT details unavailable: ${String(error)}` }, gateway }); ownedNfts = loaded.slice(); renderNftsPage(); } } nftsCachingVisible = false; ownedNfts = loaded; renderNftsPage(); } async function prepareNftsPage({ force = false } = {}) { const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = activeBroadcastNode(); if (!walletLoaded) { ownedNfts = []; ownedNftRows = []; renderNftsPage(); return; } if (!invoke || !broadcastNode) { ownedNfts = []; ownedNftRows = []; setNftsState("NFT lookup skipped: wallet bridge or broadcast node is unavailable."); renderNftsPage(); return; } if (nftsLoading && !force) { return; } const loadId = ++nftsLoadId; nftsLoading = true; nftsCachingVisible = false; renderNftsPage(); try { const [balances, settings, nftRegistry] = await Promise.all([ invoke("get_active_wallet_balances", { broadcastNode }), invoke("read_gui_settings"), invoke("nft_list", { broadcastNode }) ]); if (loadId !== nftsLoadId || currentView !== "nfts") { return; } const gateway = normalizeIpfsGateway(settings?.ipfs_gateway); ownedNftRows = ownedNftRowsFromBalances(balances, nftRegistry); if (!ownedNftRows.length && !Array.isArray(nftRegistry)) { ownedNftRows = legacyOwnedNftRowsFromBalances(balances); } nftsPage = clampNftsPage(nftsPage); invoke("prune_nft_cache", { keepKeys: ownedNftRows.map(nftCacheKey) }).catch(() => {}); ownedNfts = []; renderNftsPage(); await hydrateVisibleNftPage({ loadId, gateway, broadcastNode, force }); } catch (error) { ownedNfts = []; ownedNftRows = []; setNftsState(String(error)); } finally { if (loadId === nftsLoadId) { nftsLoading = false; renderNftsPage(); } } } function resetNftsPage() { nftsLoadId += 1; ownedNfts = []; ownedNftRows = []; selectedNftKey = ""; nftsPage = 1; nftsLoading = false; nftsCachingVisible = false; resetNftDetails(); renderNftsPage(); } nftsList?.addEventListener("click", (event) => { const button = event.target.closest("[data-nft-select]"); const card = event.target.closest("[data-nft-key]"); const key = button?.dataset.nftSelect || card?.dataset.nftKey || ""; if (!key) { return; } selectedNftKey = key; renderNftsPage(); }); nftsRefreshButton?.addEventListener("click", () => { nftMetadataCache.clear(); prepareNftsPage({ force: true }); }); function reloadNftPageFromControls(force = false) { nftsLoadId += 1; nftsLoading = false; nftsCachingVisible = false; ownedNfts = []; selectedNftKey = ""; prepareNftsPage({ force }); } document.querySelectorAll("[data-nfts-page]").forEach((button) => { button.addEventListener("click", () => { const totalPages = Math.max(1, Math.ceil(ownedNftRows.length / NFTS_PAGE_SIZE)); const action = button.dataset.nftsPage; if (action === "first") { nftsPage = 1; } else if (action === "previous") { nftsPage = clampNftsPage(nftsPage - 1); } else if (action === "next") { nftsPage = clampNftsPage(nftsPage + 1); } else if (action === "last") { nftsPage = totalPages; } reloadNftPageFromControls(); }); }); nftsPageInput?.addEventListener("change", () => { nftsPage = clampNftsPage(nftsPageInput.value); reloadNftPageFromControls(); });