Files
dchain/node/explorer/address.js
vsecoder 7e7393e4f8 chore: initial commit for v0.0.1
DChain single-node blockchain + React Native messenger client.

Core:
- PBFT consensus with multi-sig validator admission + equivocation slashing
- BadgerDB + schema migration scaffold (CurrentSchemaVersion=0)
- libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1)
- Native Go contracts (username_registry) alongside WASM (wazero)
- WebSocket gateway with topic-based fanout + Ed25519-nonce auth
- Relay mailbox with NaCl envelope encryption (X25519 + Ed25519)
- Prometheus /metrics, per-IP rate limit, body-size cap

Deployment:
- Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus
- 3-node dev compose (docker-compose.yml) with mocked internet topology
- 3-validator prod compose (deploy/prod/) for federation
- Auto-update from Gitea via /api/update-check + systemd timer
- Build-time version injection (ldflags → node --version)
- UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER)

Client (client-app/):
- Expo / React Native / NativeWind
- E2E NaCl encryption, typing indicator, contact requests
- Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch

Documentation:
- README.md, CHANGELOG.md, CONTEXT.md
- deploy/single/README.md with 6 operator scenarios
- deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design
- docs/contracts/*.md per contract
2026-04-17 14:16:44 +03:00

415 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function() {
var C = window.ExplorerCommon;
var state = {
currentAddress: '',
currentPubKey: '',
nextOffset: 0,
hasMore: false,
limit: 50
};
/* ── Helpers ─────────────────────────────────────────────────────────────── */
function linkAddress(value, label) {
return '<a href="/address?address=' + encodeURIComponent(value) + '">' +
C.esc(label || C.shortAddress(value)) + '</a>';
}
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 '<span class="addr-badge addr-badge-' + variant + '">' + C.esc(text) + '</span>';
}
/* ── 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)) : '<span class="text-muted">—</span>';
var toCell = tx.to ? linkAddress(tx.to, tx.to_addr || C.shortAddress(tx.to)) : '<span class="text-muted">—</span>';
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
? '<span class="tx-block-ref">block #' + tx.block_index + '</span>'
: '';
var tr = document.createElement('tr');
tr.className = 'tx-row';
tr.dataset.txid = tx.id || '';
tr.innerHTML =
'<td title="' + C.esc(C.fmtTime(tx.time)) + '">' +
'<div class="tx-time">' + C.esc(C.timeAgo(tx.time)) + '</div>' +
'</td>' +
'<td>' +
'<div class="tx-type-cell">' +
'<span class="tx-type-icon tx-icon-' + C.esc(dir.cls) + '">' +
'<i data-lucide="' + txTypeIcon(tx.type) + '"></i>' +
'</span>' +
'<div>' +
'<div class="tx-type-name">' + C.esc(C.txLabel(tx.type)) + '</div>' +
'<div class="tx-type-raw">' + C.esc(tx.type || '') + '</div>' +
'</div>' +
'</div>' +
'</td>' +
'<td>' +
'<div class="tx-route">' +
'<span class="tx-route-item">' + fromCell + '</span>' +
'<i data-lucide="arrow-right" class="tx-route-arrow"></i>' +
'<span class="tx-route-item">' + toCell + '</span>' +
'</div>' +
'</td>' +
'<td>' +
(memo ? '<div class="tx-memo-block">' + memo + '</div>' : '') +
blockRef +
'</td>' +
'<td style="text-align:right">' +
'<div class="tx-amount ' + amountCls + '">' + amountSign + C.esc(amountStr) + '</div>' +
(tx.fee_ut ? '<div class="tx-fee">fee ' + C.esc(C.toToken(tx.fee_ut)) + '</div>' : '') +
'</td>';
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 += '<tr>' +
'<td><a href="/token?id=' + encodeURIComponent(t.token_id) + '" class="token-sym">' + C.esc(t.symbol) + '</a></td>' +
'<td>' + C.esc(t.name) + '</td>' +
'<td style="text-align:right" class="mono">' + C.esc(balFmt) + '</td>' +
'<td><a href="/token?id=' + encodeURIComponent(t.token_id) + '" class="pill-link" style="font-size:12px;padding:4px 10px">Details</a></td>' +
'</tr>';
});
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))
? '<img class="nft-card-img" src="' + C.esc(n.uri) + '" alt="" loading="lazy">'
: '<div class="nft-card-img nft-card-placeholder"><i data-lucide="image"></i></div>';
cards += '<a class="nft-card" href="/token?nft=' + encodeURIComponent(n.nft_id) + '">' +
imgHtml +
'<div class="nft-card-body">' +
'<div class="nft-card-name">' + C.esc(n.name) + '</div>' +
'<div class="nft-card-id mono">' + C.short(n.nft_id, 16) + '</div>' +
'</div>' +
'</a>';
});
document.getElementById('addrNFTGrid').innerHTML = cards;
document.getElementById('nftPanel').style.display = '';
}
var txs = addrData.transactions || [];
if (!txs.length) {
document.getElementById('walletTxBody').innerHTML =
'<tr><td colspan="5" class="tbl-empty">No transactions for this wallet.</td></tr>';
} 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();
}
})();