(function() { var C = window.ExplorerCommon; /* ── Helpers ─────────────────────────────────────────────────────────────── */ function linkAddress(pubkey, addrDisplay) { var label = addrDisplay || C.shortAddress(pubkey); return '' + C.esc(label) + ''; } function txTypeIcon(type) { var map = { 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 map[type] || 'circle'; } // Color class for the banner icon based on tx type function txBannerClass(type) { var pos = { BLOCK_REWARD: 1, RELAY_PROOF: 1, HEARTBEAT: 1, REGISTER_KEY: 1, REGISTER_RELAY: 1 }; var warn = { SLASH: 1, REMOVE_VALIDATOR: 1 }; if (warn[type]) return 'tx-banner-warn'; if (pos[type]) return 'tx-banner-ok'; return 'tx-banner-ok'; // default — all confirmed txs are "ok" } // One-line human description of the tx function txDescription(tx) { var fromAddr = tx.from_addr ? C.shortAddress(tx.from_addr) : (tx.from ? C.shortAddress(tx.from) : '?'); var toAddr = tx.to_addr ? C.shortAddress(tx.to_addr) : (tx.to ? C.shortAddress(tx.to) : ''); var amt = tx.amount ? tx.amount : C.toToken(tx.amount_ut || 0); switch (String(tx.type)) { case 'TRANSFER': return fromAddr + ' sent ' + amt + (toAddr ? ' to ' + toAddr : ''); case 'BLOCK_REWARD': return 'Block #' + tx.block_index + ' fee reward' + (toAddr ? ' to ' + toAddr : ''); case 'RELAY_PROOF': return fromAddr + ' submitted relay proof, earned ' + amt; case 'HEARTBEAT': return fromAddr + ' submitted heartbeat'; case 'REGISTER_KEY': { var nick = ''; if (tx.payload && tx.payload.nickname) nick = ' as "' + tx.payload.nickname + '"'; return fromAddr + ' registered identity' + nick; } case 'REGISTER_RELAY': return fromAddr + ' registered as relay node'; case 'BIND_WALLET': return fromAddr + ' bound wallet' + (toAddr ? ' → ' + toAddr : ''); case 'ADD_VALIDATOR': return fromAddr + ' added ' + (toAddr || '?') + ' to validator set'; case 'REMOVE_VALIDATOR': return fromAddr + ' removed ' + (toAddr || '?') + ' from validator set'; case 'SLASH': return fromAddr + ' slashed ' + (toAddr || '?'); default: return C.txLabel(tx.type) + (fromAddr ? ' by ' + fromAddr : ''); } } /* ── Flow diagram builder ─────────────────────────────────────────────── */ function buildFlowDiagram(tx) { var fromPub = tx.from || ''; var fromAddr = tx.from_addr || (fromPub ? C.shortAddress(fromPub) : ''); var toPub = tx.to || ''; var toAddr = tx.to_addr || (toPub ? C.shortAddress(toPub) : ''); var amt = tx.amount ? tx.amount : (tx.amount_ut ? C.toToken(tx.amount_ut) : ''); var memo = tx.memo || ''; // Nodes var fromNode = ''; var toNode = ''; if (fromPub) { fromNode = '
' + '
' + (fromAddr ? fromAddr[0].toUpperCase() : '?') + '
' + '
' + (fromPub ? '' + C.esc(fromAddr || C.shortAddress(fromPub)) + '' : '—') + '
' + '
Sender
' + '
'; } if (toPub) { toNode = '
' + '
' + (toAddr ? toAddr[0].toUpperCase() : '?') + '
' + '
' + '' + C.esc(toAddr || C.shortAddress(toPub)) + '' + '
' + '
Recipient
' + '
'; } // No route for system txs with no To if (!fromPub && !toPub) return ''; var arrowLabel = amt || C.txLabel(tx.type); var arrowSub = memo || ''; var arrow = '
' + '
' + C.esc(arrowLabel) + '
' + '
' + '
' + '' + '
' + (arrowSub ? '
' + C.esc(arrowSub) + '
' : '') + '
'; if (fromNode && toNode) { return fromNode + arrow + toNode; } if (fromNode) return fromNode; return toNode; } /* ── Main render ─────────────────────────────────────────────────────────── */ function renderTx(tx) { // Page title document.title = C.txLabel(tx.type) + ' ' + C.short(tx.id, 12) + ' | DChain Explorer'; // ── Banner var bannerIcon = document.getElementById('txBannerIcon'); bannerIcon.innerHTML = ''; bannerIcon.className = 'tx-banner-icon ' + txBannerClass(tx.type); document.getElementById('txBannerTitle').textContent = 'Confirmed · ' + C.txLabel(tx.type); document.getElementById('txBannerDesc').textContent = txDescription(tx); document.getElementById('txBannerTime').textContent = C.fmtTime(tx.time) + ' (' + C.timeAgo(tx.time) + ')'; // ── Overview action table row var fromAddr = tx.from_addr || (tx.from ? C.shortAddress(tx.from) : '—'); var toAddr = tx.to_addr || (tx.to ? C.shortAddress(tx.to) : ''); var routeHtml = tx.from ? ('' + (tx.from ? linkAddress(tx.from, fromAddr) : '—') + '' + (tx.to ? '' + '' + linkAddress(tx.to, toAddr) + '' : '')) : '—'; var payloadNote = tx.memo || (tx.payload && tx.payload.nickname ? 'nickname: ' + tx.payload.nickname : '') || '—'; var amtHtml = tx.amount_ut ? '' + C.esc(tx.amount || C.toToken(tx.amount_ut)) + '' : ''; document.getElementById('txActionRow').innerHTML = '
' + '' + '' + C.esc(C.txLabel(tx.type)) + '' + '
' + '
' + routeHtml + '
' + '
' + C.esc(payloadNote) + '
' + '
' + amtHtml + '
'; // ── Flow diagram var flowHtml = buildFlowDiagram(tx); var flowEl = document.getElementById('txFlow'); if (flowHtml) { flowEl.innerHTML = flowHtml; flowEl.style.display = ''; } else { flowEl.style.display = 'none'; } // ── Details panel // TX ID document.getElementById('txId').textContent = tx.id || '—'; // Type with badge document.getElementById('txType').innerHTML = '' + C.esc(tx.type || '—') + '' + ' — ' + C.esc(C.txLabel(tx.type)) + ''; // Memo if (tx.memo) { document.getElementById('txMemo').textContent = tx.memo; document.getElementById('txMemoRow').style.display = ''; } else { document.getElementById('txMemoRow').style.display = 'none'; } // From if (tx.from) { document.getElementById('txFrom').innerHTML = linkAddress(tx.from, tx.from_addr || tx.from); document.getElementById('txFromRaw').textContent = tx.from; } else { document.getElementById('txFrom').textContent = '—'; } // To if (tx.to) { document.getElementById('txTo').innerHTML = linkAddress(tx.to, tx.to_addr || tx.to); document.getElementById('txToRaw').textContent = tx.to; document.getElementById('txToRow').style.display = ''; } else { document.getElementById('txToRow').style.display = 'none'; } // Amount var amountText = tx.amount || C.toToken(tx.amount_ut || 0); document.getElementById('txAmount').textContent = amountText; document.getElementById('txAmount').className = 'tx-amount-val' + (tx.amount_ut > 0 ? ' pos' : ''); // Fee document.getElementById('txFee').textContent = tx.fee || C.toToken(tx.fee_ut || 0); // Block var blockLink = document.getElementById('txBlockLink'); if (tx.block_index !== undefined) { blockLink.textContent = '#' + tx.block_index; blockLink.href = '#'; blockLink.onclick = function(e) { e.preventDefault(); window.location.href = '/?block=' + tx.block_index; }; } else { blockLink.textContent = '—'; } document.getElementById('txBlockHash').textContent = tx.block_hash ? ' ' + C.short(tx.block_hash, 20) : ''; // Time document.getElementById('txTime').textContent = C.fmtTime(tx.time); document.getElementById('txTimeAgo').textContent = tx.time ? '(' + C.timeAgo(tx.time) + ')' : ''; // Signature if (tx.signature_hex) { document.getElementById('txSig').textContent = tx.signature_hex; document.getElementById('txSigRow').style.display = ''; } else { document.getElementById('txSigRow').style.display = 'none'; } // ── Payload panel (only for rich payloads) var shouldShowPayload = tx.payload && typeof tx.payload === 'object' && tx.type !== 'TRANSFER'; // memo already shown for transfers if (shouldShowPayload) { document.getElementById('txPayloadPre').textContent = JSON.stringify(tx.payload, null, 2); document.getElementById('txPayloadPanel').style.display = ''; } else { document.getElementById('txPayloadPanel').style.display = 'none'; } // ── Raw JSON document.getElementById('txRaw').textContent = JSON.stringify(tx, null, 2); // Show content document.getElementById('mainContent').style.display = ''; C.refreshIcons(); } /* ── Load ────────────────────────────────────────────────────────────────── */ async function loadTx(id) { if (!id) return; C.setStatus('Loading…', 'warn'); document.getElementById('mainContent').style.display = 'none'; try { var tx = await C.fetchJSON('/api/tx/' + encodeURIComponent(id)); renderTx(tx); window.history.replaceState({}, '', '/tx?id=' + encodeURIComponent(id)); C.setStatus('', ''); } catch (e) { C.setStatus('Load failed: ' + e.message, 'err'); } } /* ── Tab switching ───────────────────────────────────────────────────────── */ function switchTab(active) { var tabs = ['Overview', 'Raw']; tabs.forEach(function(name) { var btn = document.getElementById('tab' + name); var pane = document.getElementById('pane' + name); if (!btn || !pane) return; var isActive = (name === active); btn.className = 'tx-tab' + (isActive ? ' tx-tab-active' : ''); pane.style.display = isActive ? '' : 'none'; }); } document.getElementById('tabOverview').addEventListener('click', function() { switchTab('Overview'); }); document.getElementById('tabRaw').addEventListener('click', function() { switchTab('Raw'); }); /* ── Copy buttons ────────────────────────────────────────────────────────── */ document.addEventListener('click', function(e) { var t = e.target; if (!t) return; 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 || '').catch(function() {}); btn.classList.add('copy-btn-done'); setTimeout(function() { btn.classList.remove('copy-btn-done'); }, 1200); } } }); /* ── Wiring ──────────────────────────────────────────────────────────────── */ document.getElementById('txBtn').addEventListener('click', function() { var val = (document.getElementById('txInput').value || '').trim(); if (val) loadTx(val); }); document.getElementById('txInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') document.getElementById('txBtn').click(); }); var initial = C.q('id'); if (initial) { document.getElementById('txInput').value = initial; loadTx(initial); } })();