(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() : '?') +
'
' +
'
' +
'
Sender
' +
'
';
}
if (toPub) {
toNode =
'' +
'
' +
(toAddr ? toAddr[0].toUpperCase() : '?') +
'
' +
'
' +
'
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);
}
})();