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
122 lines
5.2 KiB
JavaScript
122 lines
5.2 KiB
JavaScript
(function() {
|
|
var C = window.ExplorerCommon;
|
|
|
|
var state = { tab: 'fungible' };
|
|
|
|
function switchTab(name) {
|
|
state.tab = name;
|
|
document.getElementById('paneFungible').style.display = name === 'fungible' ? '' : 'none';
|
|
document.getElementById('paneNFT').style.display = name === 'nft' ? '' : 'none';
|
|
document.getElementById('tabFungible').className = 'tx-tab' + (name === 'fungible' ? ' tx-tab-active' : '');
|
|
document.getElementById('tabNFT').className = 'tx-tab' + (name === 'nft' ? ' tx-tab-active' : '');
|
|
}
|
|
|
|
/* ── Fungible tokens ─────────────────────────────────────────────────────── */
|
|
|
|
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+$/, '');
|
|
}
|
|
|
|
async function loadTokens() {
|
|
C.setStatus('Loading…', 'warn');
|
|
try {
|
|
var data = await C.fetchJSON('/api/tokens');
|
|
var tokens = (data && Array.isArray(data.tokens)) ? data.tokens : [];
|
|
document.getElementById('tokenCount').textContent = tokens.length;
|
|
|
|
var tbody = document.getElementById('tokenBody');
|
|
if (!tokens.length) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="tbl-empty">No fungible tokens issued yet.</td></tr>';
|
|
} else {
|
|
var rows = '';
|
|
tokens.forEach(function(t, i) {
|
|
var supply = formatSupply(t.total_supply || 0, t.decimals || 0);
|
|
rows +=
|
|
'<tr>' +
|
|
'<td class="text-muted" style="font-size:12px">' + (i+1) + '</td>' +
|
|
'<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 class="mono text-muted">' + (t.decimals || 0) + '</td>' +
|
|
'<td class="mono">' + C.esc(supply) + '</td>' +
|
|
'<td>' +
|
|
'<a href="/address?address=' + encodeURIComponent(t.issuer) + '" class="mono" style="font-size:12px">' +
|
|
C.short(t.issuer, 20) +
|
|
'</a>' +
|
|
'</td>' +
|
|
'<td class="mono text-muted" style="font-size:12px">' + (t.issued_at || 0) + '</td>' +
|
|
'</tr>';
|
|
});
|
|
tbody.innerHTML = rows;
|
|
}
|
|
C.refreshIcons();
|
|
} catch(e) {
|
|
C.setStatus('Load failed: ' + e.message, 'err');
|
|
}
|
|
}
|
|
|
|
/* ── NFTs ────────────────────────────────────────────────────────────────── */
|
|
|
|
async function loadNFTs() {
|
|
try {
|
|
var data = await C.fetchJSON('/api/nfts');
|
|
var nfts = (data && Array.isArray(data.nfts)) ? data.nfts : [];
|
|
document.getElementById('nftCount').textContent = nfts.length;
|
|
|
|
var grid = document.getElementById('nftGrid');
|
|
var empty = document.getElementById('nftEmpty');
|
|
if (!nfts.length) {
|
|
grid.innerHTML = '';
|
|
empty.style.display = '';
|
|
return;
|
|
}
|
|
empty.style.display = 'none';
|
|
|
|
var cards = '';
|
|
nfts.forEach(function(n) {
|
|
var burned = n.burned ? ' nft-card-burned' : '';
|
|
var imgHtml = '';
|
|
if (n.uri && /\.(png|jpg|jpeg|gif|svg|webp)/i.test(n.uri)) {
|
|
imgHtml = '<img class="nft-card-img" src="' + C.esc(n.uri) + '" alt="" loading="lazy">';
|
|
} else {
|
|
imgHtml = '<div class="nft-card-img nft-card-placeholder"><i data-lucide="image"></i></div>';
|
|
}
|
|
cards +=
|
|
'<a class="nft-card' + burned + '" href="/token?nft=' + encodeURIComponent(n.nft_id) + '">' +
|
|
imgHtml +
|
|
'<div class="nft-card-body">' +
|
|
'<div class="nft-card-name">' + C.esc(n.name) + (n.burned ? ' <span class="badge-burned">BURNED</span>' : '') + '</div>' +
|
|
'<div class="nft-card-id mono">' + C.short(n.nft_id, 16) + '</div>' +
|
|
(n.owner ? '<div class="nft-card-owner text-muted">Owner: ' + C.short(n.owner, 16) + '</div>' : '') +
|
|
'</div>' +
|
|
'</a>';
|
|
});
|
|
grid.innerHTML = cards;
|
|
C.refreshIcons();
|
|
} catch(e) {
|
|
document.getElementById('nftCount').textContent = '?';
|
|
}
|
|
}
|
|
|
|
/* ── Boot ────────────────────────────────────────────────────────────────── */
|
|
|
|
document.getElementById('tabFungible').addEventListener('click', function() { switchTab('fungible'); });
|
|
document.getElementById('tabNFT').addEventListener('click', function() { switchTab('nft'); });
|
|
document.getElementById('refreshBtn').addEventListener('click', function() { loadTokens(); loadNFTs(); });
|
|
|
|
// Check URL hash for tab.
|
|
if (window.location.hash === '#nft') switchTab('nft');
|
|
|
|
Promise.all([loadTokens(), loadNFTs()]).then(function() {
|
|
C.setStatus('', '');
|
|
});
|
|
})();
|