(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();
}
})();