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:
vsecoder
2026-04-17 14:16:44 +03:00
commit 7e7393e4f8
196 changed files with 55947 additions and 0 deletions

197
node/explorer/common.js Normal file
View File

@@ -0,0 +1,197 @@
(function() {
function esc(v) {
return String(v === undefined || v === null ? '' : v)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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();
})();