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
198 lines
5.8 KiB
JavaScript
198 lines
5.8 KiB
JavaScript
(function() {
|
|
function esc(v) {
|
|
return String(v === undefined || v === null ? '' : v)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function short(v, n) {
|
|
if (!v) return '-';
|
|
return v.length > n ? v.slice(0, n) + '...' : v;
|
|
}
|
|
|
|
function fmtTime(iso) {
|
|
if (!iso) return '-';
|
|
return iso.replace('T', ' ').replace('Z', ' UTC');
|
|
}
|
|
|
|
function timeAgo(iso) {
|
|
if (!iso) return '-';
|
|
var now = Date.now();
|
|
var ts = new Date(iso).getTime();
|
|
if (!isFinite(ts)) return '-';
|
|
var diffSec = Math.max(0, Math.floor((now - ts) / 1000));
|
|
if (diffSec < 10) return 'a few seconds ago';
|
|
if (diffSec < 60) return diffSec + ' seconds ago';
|
|
var diffMin = Math.floor(diffSec / 60);
|
|
if (diffMin < 60) return diffMin === 1 ? 'a minute ago' : diffMin + ' minutes ago';
|
|
var diffHour = Math.floor(diffMin / 60);
|
|
if (diffHour < 24) return diffHour === 1 ? 'an hour ago' : diffHour + ' hours ago';
|
|
var diffDay = Math.floor(diffHour / 24);
|
|
if (diffDay === 1) return 'yesterday';
|
|
if (diffDay < 30) return diffDay + ' days ago';
|
|
var diffMonth = Math.floor(diffDay / 30);
|
|
if (diffMonth < 12) return diffMonth === 1 ? 'a month ago' : diffMonth + ' months ago';
|
|
var diffYear = Math.floor(diffMonth / 12);
|
|
return diffYear === 1 ? 'a year ago' : diffYear + ' years ago';
|
|
}
|
|
|
|
function shortAddress(addr) {
|
|
if (!addr) return '-';
|
|
if (addr.length <= 9) return addr;
|
|
return addr.slice(0, 3) + '...' + addr.slice(-3);
|
|
}
|
|
|
|
function txLabel(eventType) {
|
|
var map = {
|
|
TRANSFER: 'Transfer',
|
|
REGISTER_KEY: 'Register',
|
|
CREATE_CHANNEL: 'Create Channel',
|
|
ADD_MEMBER: 'Add Member',
|
|
OPEN_PAY_CHAN: 'Open Channel',
|
|
CLOSE_PAY_CHAN: 'Close Channel',
|
|
RELAY_PROOF: 'Relay Proof',
|
|
BIND_WALLET: 'Bind Wallet',
|
|
SLASH: 'Slash',
|
|
HEARTBEAT: 'Heartbeat',
|
|
BLOCK_REWARD: 'Reward'
|
|
};
|
|
return map[eventType] || eventType || 'Transaction';
|
|
}
|
|
|
|
function toToken(micro) {
|
|
if (micro === undefined || micro === null) return '-';
|
|
var n = Number(micro);
|
|
if (isNaN(n)) return String(micro);
|
|
return (n / 1000000).toFixed(6) + ' T';
|
|
}
|
|
|
|
// Compact token display: 21000000 T → "21M T", 1234 T → "1.23k T"
|
|
function _fmtNum(n) {
|
|
if (n >= 100) return Math.round(n).toString();
|
|
if (n >= 10) return n.toFixed(1).replace(/\.0$/, '');
|
|
return n.toFixed(2).replace(/\.?0+$/, '');
|
|
}
|
|
function toTokenShort(micro) {
|
|
if (micro === undefined || micro === null) return '-';
|
|
var t = Number(micro) / 1000000;
|
|
if (isNaN(t)) return String(micro);
|
|
if (t >= 1e9) return _fmtNum(t / 1e9) + 'B T';
|
|
if (t >= 1e6) return _fmtNum(t / 1e6) + 'M T';
|
|
if (t >= 1e3) return _fmtNum(t / 1e3) + 'k T';
|
|
if (t >= 0.01) return _fmtNum(t) + ' T';
|
|
var ut = Number(micro);
|
|
if (ut >= 1000) return _fmtNum(ut / 1000) + 'k µT';
|
|
return ut + ' µT';
|
|
}
|
|
|
|
function isPubKey(s) {
|
|
return /^[0-9a-fA-F]{64}$/.test(s || '');
|
|
}
|
|
|
|
function setStatus(text, cls) {
|
|
var el = document.getElementById('status');
|
|
if (!el) return;
|
|
el.className = 'status' + (cls ? ' ' + cls : '');
|
|
el.textContent = text;
|
|
}
|
|
|
|
async function fetchJSON(url) {
|
|
var resp = await fetch(url);
|
|
var json = await resp.json().catch(function() { return {}; });
|
|
if (!resp.ok) {
|
|
throw new Error(json && json.error ? json.error : 'request failed');
|
|
}
|
|
if (json && json.error) {
|
|
throw new Error(json.error);
|
|
}
|
|
return json;
|
|
}
|
|
|
|
function q(name) {
|
|
return new URLSearchParams(window.location.search).get(name) || '';
|
|
}
|
|
|
|
function navAddress(address) {
|
|
window.location.href = '/address?address=' + encodeURIComponent(address);
|
|
}
|
|
|
|
function navTx(id) {
|
|
window.location.href = '/tx?id=' + encodeURIComponent(id);
|
|
}
|
|
|
|
function navNode(node) {
|
|
window.location.href = '/node?node=' + encodeURIComponent(node);
|
|
}
|
|
|
|
function refreshIcons() {
|
|
if (window.lucide && typeof window.lucide.createIcons === 'function') {
|
|
window.lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
// ── SSE live event stream ─────────────────────────────────────────────────
|
|
//
|
|
// connectSSE(handlers) opens a connection to GET /api/events and dispatches
|
|
// events to the supplied handler map, e.g.:
|
|
//
|
|
// C.connectSSE({
|
|
// block: function(data) { ... },
|
|
// tx: function(data) { ... },
|
|
// contract_log: function(data) { ... },
|
|
// connected: function() { /* SSE connection established */ },
|
|
// error: function() { /* connection lost */ },
|
|
// });
|
|
//
|
|
// Returns the EventSource instance so the caller can close it if needed.
|
|
function connectSSE(handlers) {
|
|
if (!window.EventSource) return null; // browser doesn't support SSE
|
|
|
|
var es = new EventSource('/api/events');
|
|
|
|
es.addEventListener('open', function() {
|
|
if (handlers.connected) handlers.connected();
|
|
});
|
|
|
|
es.addEventListener('error', function() {
|
|
if (handlers.error) handlers.error();
|
|
});
|
|
|
|
['block', 'tx', 'contract_log'].forEach(function(type) {
|
|
if (!handlers[type]) return;
|
|
es.addEventListener(type, function(e) {
|
|
try {
|
|
var data = JSON.parse(e.data);
|
|
handlers[type](data);
|
|
} catch (_) {}
|
|
});
|
|
});
|
|
|
|
return es;
|
|
}
|
|
|
|
window.ExplorerCommon = {
|
|
esc: esc,
|
|
short: short,
|
|
fmtTime: fmtTime,
|
|
timeAgo: timeAgo,
|
|
shortAddress: shortAddress,
|
|
txLabel: txLabel,
|
|
toToken: toToken,
|
|
toTokenShort: toTokenShort,
|
|
isPubKey: isPubKey,
|
|
setStatus: setStatus,
|
|
fetchJSON: fetchJSON,
|
|
q: q,
|
|
navAddress: navAddress,
|
|
navTx: navTx,
|
|
navNode: navNode,
|
|
refreshIcons: refreshIcons,
|
|
connectSSE: connectSSE
|
|
};
|
|
|
|
refreshIcons();
|
|
})();
|