(function() { var C = window.ExplorerCommon; var state = { currentAddress: '', currentPubKey: '', nextOffset: 0, hasMore: false, limit: 50 }; /* ── Helpers ─────────────────────────────────────────────────────────────── */ function linkAddress(value, label) { return '' + C.esc(label || C.shortAddress(value)) + ''; } function direction(tx, pubKey) { if (tx.type === 'BLOCK_REWARD' || tx.type === 'HEARTBEAT') return { cls: 'recv', arrow: '↓' }; if (tx.type === 'RELAY_PROOF') return { cls: 'recv', arrow: '↓' }; if (tx.from === pubKey && (!tx.to || tx.to !== pubKey)) return { cls: 'sent', arrow: '↑' }; if (tx.to === pubKey && tx.from !== pubKey) return { cls: 'recv', arrow: '↓' }; return { cls: 'neutral', arrow: '·' }; } /* ── Badge builder ───────────────────────────────────────────────────────── */ function badge(text, variant) { return '' + C.esc(text) + ''; } /* ── Profile render ──────────────────────────────────────────────────────── */ function renderProfile(addrData, identData, nodeData) { var pubKey = addrData.pub_key || ''; var address = addrData.address || ''; state.currentPubKey = pubKey; // Avatar initials (first letter of nickname or first char of address) var name = (identData && identData.nickname) || ''; var avatarChar = name ? name[0].toUpperCase() : (address ? address[2] || '◆' : '◆'); document.getElementById('addrAvatar').textContent = avatarChar; // Name document.getElementById('addrNickname').textContent = name || 'Unknown Wallet'; // Badges var badges = ''; if (identData && identData.registered) badges += badge('Registered', 'ok'); if (nodeData && nodeData.blocks_produced > 0) badges += badge('Validator', 'accent'); if (nodeData && nodeData.relay_proofs > 0) badges += badge('Relay Node', 'relay'); document.getElementById('addrBadges').innerHTML = badges || badge('Unregistered', 'muted'); // Balance document.getElementById('walletBalance').textContent = addrData.balance || C.toToken(addrData.balance_ut); document.getElementById('walletBalanceSub').textContent = addrData.balance_ut != null ? addrData.balance_ut + ' µT' : ''; // Address field document.getElementById('walletAddress').textContent = address; document.getElementById('walletAddress').title = address; // Public key field document.getElementById('walletPubKey').textContent = pubKey; document.getElementById('walletPubKey').title = pubKey; // X25519 key if (identData && identData.x25519_pub) { document.getElementById('walletX25519').textContent = identData.x25519_pub; document.getElementById('walletX25519').title = identData.x25519_pub; document.getElementById('x25519Row').style.display = ''; } else { document.getElementById('x25519Row').style.display = 'none'; } // Node page link (if address is a node itself) if (nodeData && (nodeData.blocks_produced > 0 || nodeData.relay_proofs > 0 || nodeData.heartbeats > 0)) { var nodeLink = document.getElementById('nodePageLink'); nodeLink.href = '/node?node=' + encodeURIComponent(pubKey); document.getElementById('nodeLinkRow').style.display = ''; } else { document.getElementById('nodeLinkRow').style.display = 'none'; } // Bound wallet (only for pure node addresses) if (nodeData && nodeData.wallet_binding_address && nodeData.wallet_binding_address !== address) { var wbl = document.getElementById('walletBindingLink'); wbl.href = '/address?address=' + encodeURIComponent(nodeData.wallet_binding_address); wbl.textContent = nodeData.wallet_binding_address; document.getElementById('walletBindingRow').style.display = ''; } else { document.getElementById('walletBindingRow').style.display = 'none'; } // Node stats panel if (nodeData && (nodeData.blocks_produced > 0 || nodeData.relay_proofs > 0 || nodeData.heartbeats > 0)) { document.getElementById('nodeBlocks').textContent = nodeData.blocks_produced || 0; document.getElementById('nodeRelayProofs').textContent = nodeData.relay_proofs || 0; document.getElementById('nodeRepScore').textContent = nodeData.reputation_score || 0; document.getElementById('nodeRepRank').textContent = nodeData.reputation_rank || '—'; document.getElementById('nodeHeartbeats').textContent = nodeData.heartbeats || 0; document.getElementById('nodeSlashes').textContent = nodeData.slash_count || 0; document.getElementById('nodeRecentRewards').textContent = nodeData.recent_rewards || '—'; var window_ = nodeData.recent_window_blocks || 0; var produced = nodeData.recent_blocks_produced || 0; document.getElementById('nodeRecentWindow').textContent = 'last ' + window_ + ' blocks, produced ' + produced; document.getElementById('nodeStatsPanel').style.display = ''; } else { document.getElementById('nodeStatsPanel').style.display = 'none'; } } /* ── Transaction row builder ─────────────────────────────────────────────── */ function txTypeIcon(type) { var icons = { TRANSFER: 'arrow-left-right', REGISTER_KEY: 'user-check', RELAY_PROOF: 'zap', HEARTBEAT: 'activity', BLOCK_REWARD: 'layers-3', BIND_WALLET: 'link', REGISTER_RELAY: 'radio', SLASH: 'alert-triangle', ADD_VALIDATOR: 'shield-plus', REMOVE_VALIDATOR: 'shield-minus', }; return icons[type] || 'circle'; } function appendTxRows(txs) { var body = document.getElementById('walletTxBody'); for (var i = 0; i < txs.length; i++) { var tx = txs[i]; var dir = direction(tx, state.currentPubKey); var fromAddr = tx.from_addr || (tx.from ? C.shortAddress(tx.from) : '—'); var toAddr = tx.to_addr || (tx.to ? C.shortAddress(tx.to) : '—'); var fromCell = tx.from ? linkAddress(tx.from, tx.from_addr || C.shortAddress(tx.from)) : ''; var toCell = tx.to ? linkAddress(tx.to, tx.to_addr || C.shortAddress(tx.to)) : ''; var amount = Number(tx.amount_ut || 0); var amountStr = amount > 0 ? C.toToken(amount) : '—'; var amountCls = dir.cls === 'recv' ? 'pos' : (dir.cls === 'sent' ? 'neg' : 'neutral'); var amountSign = dir.cls === 'recv' ? '+' : (dir.cls === 'sent' ? '−' : ''); var memo = tx.memo ? C.esc(tx.memo) : ''; var blockRef = tx.block_index != null ? 'block #' + tx.block_index + '' : ''; var tr = document.createElement('tr'); tr.className = 'tx-row'; tr.dataset.txid = tx.id || ''; tr.innerHTML = '' + '
' + C.esc(C.timeAgo(tx.time)) + '
' + '' + '' + '
' + '' + '' + '' + '
' + '
' + C.esc(C.txLabel(tx.type)) + '
' + '
' + C.esc(tx.type || '') + '
' + '
' + '
' + '' + '' + '
' + '' + fromCell + '' + '' + '' + toCell + '' + '
' + '' + '' + (memo ? '
' + memo + '
' : '') + blockRef + '' + '' + '
' + amountSign + C.esc(amountStr) + '
' + (tx.fee_ut ? '
fee ' + C.esc(C.toToken(tx.fee_ut)) + '
' : '') + ''; body.appendChild(tr); } C.refreshIcons(); } /* ── Load wallet ─────────────────────────────────────────────────────────── */ function showEmpty() { var es = document.getElementById('emptyState'); var mc = document.getElementById('mainContent'); if (es) es.style.display = ''; if (mc) mc.style.display = 'none'; } function showError(msg) { var es = document.getElementById('emptyState'); var mc = document.getElementById('mainContent'); var eb = document.getElementById('errorBanner'); var em = document.getElementById('errorMsg'); var ps = document.getElementById('profileSection'); if (es) es.style.display = 'none'; if (eb) { eb.style.display = ''; if (em) em.textContent = msg; } if (ps) ps.style.display = 'none'; if (mc) mc.style.display = ''; C.refreshIcons(); } async function loadWallet(address) { if (!address) return; state.currentAddress = address; state.nextOffset = 0; state.hasMore = false; // Hide empty state, hide main, show loading var es = document.getElementById('emptyState'); if (es) es.style.display = 'none'; document.getElementById('mainContent').style.display = 'none'; document.getElementById('errorBanner').style.display = 'none'; document.getElementById('profileSection').style.display = ''; document.getElementById('walletTxBody').innerHTML = ''; document.getElementById('walletTxCount').textContent = ''; C.setStatus('Loading…', 'warn'); try { // Parallel fetch: address data + identity + node info var results = await Promise.all([ C.fetchJSON('/api/address/' + encodeURIComponent(address) + '?limit=' + state.limit + '&offset=0'), C.fetchJSON('/api/identity/' + encodeURIComponent(address)).catch(function() { return null; }), C.fetchJSON('/api/node/' + encodeURIComponent(address)).catch(function() { return null; }), C.fetchJSON('/api/tokens').catch(function() { return null; }), C.fetchJSON('/api/nfts/owner/' + encodeURIComponent(address)).catch(function() { return null; }), ]); var addrData = results[0]; var identData = results[1]; var nodeData = results[2]; var tokensAll = results[3]; var nftsOwned = results[4]; renderProfile(addrData, identData, nodeData); // ── Token balances ────────────────────────────────────────────────── var pubKey = addrData.pub_key || address; var allTokens = (tokensAll && Array.isArray(tokensAll.tokens)) ? tokensAll.tokens : []; if (allTokens.length) { var balFetches = allTokens.map(function(t) { return C.fetchJSON('/api/tokens/' + t.token_id + '/balance/' + encodeURIComponent(pubKey)) .then(function(b) { return { token: t, balance: b && b.balance != null ? b.balance : 0 }; }) .catch(function() { return { token: t, balance: 0 }; }); }); var balResults = await Promise.all(balFetches); var nonZero = balResults.filter(function(r) { return r.balance > 0; }); if (nonZero.length) { var rows = ''; nonZero.forEach(function(r) { var t = r.token; var d = t.decimals || 0; var sup = r.balance; var whole = d > 0 ? Math.floor(sup / Math.pow(10, d)) : sup; var frac = d > 0 ? sup % Math.pow(10, d) : 0; var balFmt = whole.toLocaleString() + (frac > 0 ? '.' + String(frac).padStart(d, '0').replace(/0+$/, '') : ''); rows += '' + '' + C.esc(t.symbol) + '' + '' + C.esc(t.name) + '' + '' + C.esc(balFmt) + '' + 'Details' + ''; }); document.getElementById('tokenBalBody').innerHTML = rows; document.getElementById('tokenBalPanel').style.display = ''; } } // ── NFTs owned ────────────────────────────────────────────────────── var ownedNFTs = (nftsOwned && Array.isArray(nftsOwned.nfts)) ? nftsOwned.nfts : []; if (ownedNFTs.length) { document.getElementById('nftOwnedCount').textContent = ownedNFTs.length + ' NFT' + (ownedNFTs.length !== 1 ? 's' : ''); var cards = ''; ownedNFTs.forEach(function(n) { var imgHtml = (n.uri && /\.(png|jpg|jpeg|gif|svg|webp)/i.test(n.uri)) ? '' : '
'; cards += '' + imgHtml + '
' + '
' + C.esc(n.name) + '
' + '
' + C.short(n.nft_id, 16) + '
' + '
' + '
'; }); document.getElementById('addrNFTGrid').innerHTML = cards; document.getElementById('nftPanel').style.display = ''; } var txs = addrData.transactions || []; if (!txs.length) { document.getElementById('walletTxBody').innerHTML = 'No transactions for this wallet.'; } else { appendTxRows(txs); } state.nextOffset = addrData.next_offset || txs.length; state.hasMore = !!addrData.has_more; setLoadMoreVisible(state.hasMore); var countText = txs.length + (state.hasMore ? '+' : '') + ' transaction' + (txs.length !== 1 ? 's' : ''); document.getElementById('walletTxCount').textContent = countText; // Update page title and URL var displayName = (identData && identData.nickname) || addrData.address || address; document.title = displayName + ' | DChain Explorer'; window.history.replaceState({}, '', '/address?address=' + encodeURIComponent(addrData.pub_key || address)); document.getElementById('mainContent').style.display = ''; C.setStatus('', ''); C.refreshIcons(); } catch (e) { showError(e.message || 'Unknown error'); C.setStatus('', ''); } } async function loadMore() { if (!state.currentAddress || !state.hasMore) return; C.setStatus('Loading more…', 'warn'); setLoadMoreVisible(false); try { var data = await C.fetchJSON('/api/address/' + encodeURIComponent(state.currentAddress) + '?limit=' + state.limit + '&offset=' + state.nextOffset); var txs = data.transactions || []; appendTxRows(txs); state.nextOffset = data.next_offset || (state.nextOffset + txs.length); state.hasMore = !!data.has_more; var prev = parseInt((document.getElementById('walletTxCount').textContent || '0'), 10) || 0; var total = prev + txs.length; document.getElementById('walletTxCount').textContent = total + (state.hasMore ? '+' : '') + ' transactions'; setLoadMoreVisible(state.hasMore); C.setStatus('', ''); } catch (e) { C.setStatus('Load failed: ' + e.message, 'err'); setLoadMoreVisible(true); } } function setLoadMoreVisible(v) { var btn = document.getElementById('loadMoreBtn'); if (btn) btn.style.display = v ? '' : 'none'; } /* ── Copy buttons ────────────────────────────────────────────────────────── */ document.addEventListener('click', function(e) { var t = e.target; if (!t) return; // Copy button var btn = t.closest ? t.closest('.copy-btn') : null; if (btn) { var src = document.getElementById(btn.dataset.copyId); if (src) { navigator.clipboard.writeText(src.textContent || src.title || '').catch(function() {}); btn.classList.add('copy-btn-done'); setTimeout(function() { btn.classList.remove('copy-btn-done'); }, 1200); } return; } // TX row click → tx detail page var link = t.closest ? t.closest('a') : null; if (link) return; var row = t.closest ? t.closest('tr.tx-row') : null; if (row && row.dataset && row.dataset.txid) { window.location.href = '/tx?id=' + encodeURIComponent(row.dataset.txid); } }); /* ── Event wiring ────────────────────────────────────────────────────────── */ document.getElementById('addressBtn').addEventListener('click', function() { var val = (document.getElementById('addressInput').value || '').trim(); if (val) loadWallet(val); }); document.getElementById('addressInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') document.getElementById('addressBtn').click(); }); document.getElementById('loadMoreBtn').addEventListener('click', loadMore); /* ── Auto-load from URL param ────────────────────────────────────────────── */ var initial = C.q('address'); if (initial) { document.getElementById('addressInput').value = initial; loadWallet(initial); } else { showEmpty(); } })();