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
This commit is contained in:
197
node/explorer/common.js
Normal file
197
node/explorer/common.js
Normal file
@@ -0,0 +1,197 @@
|
||||
(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();
|
||||
})();
|
||||
Reference in New Issue
Block a user