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
181 lines
8.2 KiB
JavaScript
181 lines
8.2 KiB
JavaScript
(function() {
|
|
var C = window.ExplorerCommon;
|
|
|
|
var params = new URLSearchParams(window.location.search);
|
|
var tokenID = params.get('id');
|
|
var nftID = params.get('nft');
|
|
|
|
function kv(icon, label, valHtml) {
|
|
return '<div class="addr-kv-row">' +
|
|
'<div class="addr-kv-key"><i data-lucide="' + icon + '"></i> ' + label + '</div>' +
|
|
'<div class="addr-kv-val">' + valHtml + '</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function copyBtn(id) {
|
|
return '<button class="copy-btn" data-copy-id="' + id + '" title="Copy"><i data-lucide="copy"></i></button>';
|
|
}
|
|
|
|
function hiddenSpan(id, val) {
|
|
return '<span id="' + id + '" style="display:none">' + C.esc(val) + '</span>';
|
|
}
|
|
|
|
/* ── Fungible token ──────────────────────────────────────────────────────── */
|
|
|
|
async function loadToken() {
|
|
C.setStatus('Loading token…', 'warn');
|
|
try {
|
|
var data = await C.fetchJSON('/api/tokens/' + tokenID);
|
|
if (!data || !data.token_id) { C.setStatus('Token not found.', 'err'); return; }
|
|
|
|
document.title = data.symbol + ' | DChain Explorer';
|
|
document.getElementById('bannerType').textContent = 'Fungible Token';
|
|
document.getElementById('bannerName').textContent = data.name + ' (' + data.symbol + ')';
|
|
document.getElementById('bannerBlock').textContent = 'block ' + (data.issued_at || 0);
|
|
document.getElementById('bannerIcon').querySelector('i').setAttribute('data-lucide', 'coins');
|
|
|
|
var d = data.decimals || 0;
|
|
var supplyFmt = formatSupply(data.total_supply || 0, d);
|
|
|
|
var rows = '';
|
|
rows += kv('hash', 'Token ID',
|
|
'<span class="mono addr-pubkey-text" id="tid">' + C.esc(data.token_id) + '</span>' + copyBtn('tid'));
|
|
rows += kv('type', 'Symbol',
|
|
'<span class="token-sym">' + C.esc(data.symbol) + '</span>');
|
|
rows += kv('tag', 'Name', C.esc(data.name));
|
|
rows += kv('layers-3', 'Decimals', '<span class="mono">' + d + '</span>');
|
|
rows += kv('bar-chart-2', 'Total Supply',
|
|
'<span class="mono">' + C.esc(supplyFmt) + '</span>');
|
|
rows += kv('user', 'Issuer',
|
|
'<a href="/address?address=' + encodeURIComponent(data.issuer) + '" class="mono">' +
|
|
C.short(data.issuer, 20) +
|
|
'</a>' +
|
|
hiddenSpan('issuerRaw', data.issuer) + copyBtn('issuerRaw'));
|
|
rows += kv('blocks', 'Issued at block',
|
|
'<span class="mono">' + (data.issued_at || 0) + '</span>');
|
|
|
|
document.getElementById('kvList').innerHTML = rows;
|
|
document.getElementById('rawJSON').textContent = JSON.stringify(data, null, 2);
|
|
document.getElementById('tabRaw').style.display = '';
|
|
|
|
document.getElementById('mainContent').style.display = '';
|
|
C.setStatus('', '');
|
|
C.refreshIcons();
|
|
C.wireClipboard();
|
|
} catch(e) {
|
|
C.setStatus('Load failed: ' + e.message, 'err');
|
|
}
|
|
}
|
|
|
|
/* ── NFT ─────────────────────────────────────────────────────────────────── */
|
|
|
|
async function loadNFT() {
|
|
C.setStatus('Loading NFT…', 'warn');
|
|
try {
|
|
var data = await C.fetchJSON('/api/nfts/' + nftID);
|
|
var n = data && data.nft ? data.nft : data;
|
|
if (!n || !n.nft_id) { C.setStatus('NFT not found.', 'err'); return; }
|
|
|
|
document.title = n.name + ' | DChain Explorer';
|
|
document.getElementById('bannerType').textContent = n.burned ? 'NFT (Burned)' : 'NFT';
|
|
document.getElementById('bannerName').textContent = n.name;
|
|
document.getElementById('bannerBlock').textContent = 'minted block ' + (n.minted_at || 0);
|
|
document.getElementById('bannerIcon').querySelector('i').setAttribute('data-lucide', 'image');
|
|
if (n.burned) document.getElementById('banner').classList.add('tx-banner-err');
|
|
|
|
// Show image if URI looks like an image
|
|
if (n.uri && /\.(png|jpg|jpeg|gif|svg|webp)/i.test(n.uri)) {
|
|
document.getElementById('nftImage').src = n.uri;
|
|
document.getElementById('nftImageWrap').style.display = '';
|
|
}
|
|
|
|
var ownerAddr = data.owner_address || '';
|
|
var rows = '';
|
|
rows += kv('hash', 'NFT ID',
|
|
'<span class="mono addr-pubkey-text" id="nid">' + C.esc(n.nft_id) + '</span>' + copyBtn('nid'));
|
|
rows += kv('tag', 'Name', C.esc(n.name));
|
|
if (n.description)
|
|
rows += kv('file-text', 'Description', C.esc(n.description));
|
|
if (n.uri)
|
|
rows += kv('link', 'Metadata URI',
|
|
'<a href="' + C.esc(n.uri) + '" target="_blank" class="mono" style="word-break:break-all">' + C.esc(n.uri) + '</a>');
|
|
if (!n.burned && n.owner) {
|
|
rows += kv('user', 'Owner',
|
|
'<a href="/address?address=' + encodeURIComponent(n.owner) + '" class="mono">' +
|
|
C.short(ownerAddr || n.owner, 20) +
|
|
'</a>' +
|
|
hiddenSpan('ownerRaw', n.owner) + copyBtn('ownerRaw'));
|
|
} else if (n.burned) {
|
|
rows += kv('flame', 'Status', '<span style="color:var(--err)">Burned</span>');
|
|
}
|
|
rows += kv('user-check', 'Issuer',
|
|
'<a href="/address?address=' + encodeURIComponent(n.issuer) + '" class="mono">' +
|
|
C.short(n.issuer, 20) +
|
|
'</a>');
|
|
rows += kv('blocks', 'Minted at block',
|
|
'<span class="mono">' + (n.minted_at || 0) + '</span>');
|
|
|
|
document.getElementById('kvList').innerHTML = rows;
|
|
|
|
// Attributes
|
|
if (n.attributes) {
|
|
try {
|
|
var attrs = JSON.parse(n.attributes);
|
|
var keys = Object.keys(attrs);
|
|
if (keys.length) {
|
|
var html = '';
|
|
keys.forEach(function(k) {
|
|
html += '<div class="attr-chip"><div class="attr-chip-key">' + C.esc(k) + '</div>' +
|
|
'<div class="attr-chip-val">' + C.esc(String(attrs[k])) + '</div></div>';
|
|
});
|
|
document.getElementById('attrsGrid').innerHTML = html;
|
|
document.getElementById('attrsSection').style.display = '';
|
|
}
|
|
} catch(_) {}
|
|
}
|
|
|
|
document.getElementById('rawJSON').textContent = JSON.stringify(data, null, 2);
|
|
document.getElementById('tabRaw').style.display = '';
|
|
document.getElementById('mainContent').style.display = '';
|
|
C.setStatus('', '');
|
|
C.refreshIcons();
|
|
C.wireClipboard();
|
|
} catch(e) {
|
|
C.setStatus('Load failed: ' + e.message, 'err');
|
|
}
|
|
}
|
|
|
|
/* ── Helpers ─────────────────────────────────────────────────────────────── */
|
|
|
|
function formatSupply(supply, decimals) {
|
|
if (decimals === 0) return supply.toLocaleString();
|
|
var d = Math.pow(10, decimals);
|
|
var whole = Math.floor(supply / d);
|
|
var frac = supply % d;
|
|
if (frac === 0) return whole.toLocaleString();
|
|
return whole.toLocaleString() + '.' + String(frac).padStart(decimals, '0').replace(/0+$/, '');
|
|
}
|
|
|
|
/* ── Tabs ────────────────────────────────────────────────────────────────── */
|
|
|
|
document.getElementById('tabOverview').addEventListener('click', function() {
|
|
document.getElementById('paneOverview').style.display = '';
|
|
document.getElementById('paneRaw').style.display = 'none';
|
|
document.getElementById('tabOverview').classList.add('tx-tab-active');
|
|
document.getElementById('tabRaw').classList.remove('tx-tab-active');
|
|
});
|
|
document.getElementById('tabRaw').addEventListener('click', function() {
|
|
document.getElementById('paneOverview').style.display = 'none';
|
|
document.getElementById('paneRaw').style.display = '';
|
|
document.getElementById('tabRaw').classList.add('tx-tab-active');
|
|
document.getElementById('tabOverview').classList.remove('tx-tab-active');
|
|
});
|
|
|
|
/* ── Boot ────────────────────────────────────────────────────────────────── */
|
|
|
|
if (nftID) loadNFT();
|
|
else if (tokenID) loadToken();
|
|
else C.setStatus('No token or NFT ID provided.', 'err');
|
|
|
|
})();
|