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
296 lines
13 KiB
JavaScript
296 lines
13 KiB
JavaScript
(function() {
|
|
var C = window.ExplorerCommon;
|
|
|
|
var currentContractID = '';
|
|
|
|
/* ── Tab switching ───────────────────────────────────────────────────────── */
|
|
|
|
function switchTab(active) {
|
|
var tabs = ['Overview', 'State', 'Logs', 'Raw'];
|
|
tabs.forEach(function(name) {
|
|
var btn = document.getElementById('tab' + name);
|
|
var pane = document.getElementById('pane' + name);
|
|
if (!btn || !pane) return;
|
|
var isActive = (name === active);
|
|
btn.className = 'tx-tab' + (isActive ? ' tx-tab-active' : '');
|
|
pane.style.display = isActive ? '' : 'none';
|
|
});
|
|
// Lazy-load logs when tab is opened
|
|
if (active === 'Logs' && currentContractID) {
|
|
loadLogs(currentContractID);
|
|
}
|
|
}
|
|
|
|
document.getElementById('tabOverview').addEventListener('click', function() { switchTab('Overview'); });
|
|
document.getElementById('tabState').addEventListener('click', function() { switchTab('State'); });
|
|
document.getElementById('tabLogs').addEventListener('click', function() { switchTab('Logs'); });
|
|
document.getElementById('tabRaw').addEventListener('click', function() { switchTab('Raw'); });
|
|
|
|
/* ── ABI rendering ───────────────────────────────────────────────────────── */
|
|
|
|
function renderABI(abiJson) {
|
|
var abi = null;
|
|
try {
|
|
abi = typeof abiJson === 'string' ? JSON.parse(abiJson) : abiJson;
|
|
} catch (e) { return; }
|
|
if (!abi || !Array.isArray(abi.methods) || abi.methods.length === 0) return;
|
|
|
|
var tbody = document.getElementById('abiMethodsBody');
|
|
tbody.innerHTML = '';
|
|
abi.methods.forEach(function(m) {
|
|
var args = Array.isArray(m.args) && m.args.length > 0
|
|
? m.args.map(function(a) { return C.esc(a.name || '') + ': ' + C.esc(a.type || '?'); }).join(', ')
|
|
: '<span class="text-muted">none</span>';
|
|
var tr = document.createElement('tr');
|
|
tr.innerHTML =
|
|
'<td><span class="tx-type-badge">' + C.esc(m.name || '?') + '</span></td>' +
|
|
'<td class="mono" style="font-size:0.82rem">' + args + '</td>';
|
|
tbody.appendChild(tr);
|
|
});
|
|
document.getElementById('abiSection').style.display = '';
|
|
}
|
|
|
|
/* ── Main render ─────────────────────────────────────────────────────────── */
|
|
|
|
function renderContract(contract) {
|
|
document.title = 'Contract ' + C.short(contract.contract_id, 16) + ' | DChain Explorer';
|
|
currentContractID = contract.contract_id;
|
|
|
|
// Banner
|
|
document.getElementById('bannerContractId').textContent = contract.contract_id || '—';
|
|
document.getElementById('bannerDeployedAt').textContent =
|
|
contract.deployed_at != null ? 'Block #' + contract.deployed_at : '—';
|
|
|
|
// Overview fields
|
|
document.getElementById('contractId').textContent = contract.contract_id || '—';
|
|
|
|
if (contract.deployer_pub) {
|
|
document.getElementById('contractDeployer').innerHTML =
|
|
'<a href="/address?address=' + encodeURIComponent(contract.deployer_pub) + '">' +
|
|
C.esc(C.shortAddress(contract.deployer_pub)) + '</a>';
|
|
document.getElementById('contractDeployerRaw').textContent = contract.deployer_pub;
|
|
} else {
|
|
document.getElementById('contractDeployer').textContent = '—';
|
|
}
|
|
|
|
document.getElementById('contractDeployedBlock').textContent =
|
|
contract.deployed_at != null ? '#' + contract.deployed_at : '—';
|
|
|
|
document.getElementById('contractWasmSize').textContent =
|
|
contract.wasm_size != null ? contract.wasm_size.toLocaleString() + ' bytes' : '—';
|
|
|
|
// ABI
|
|
if (contract.abi_json) {
|
|
renderABI(contract.abi_json);
|
|
}
|
|
|
|
// Raw JSON
|
|
document.getElementById('contractRaw').textContent = JSON.stringify(contract, null, 2);
|
|
|
|
document.getElementById('mainContent').style.display = '';
|
|
C.refreshIcons();
|
|
}
|
|
|
|
/* ── Contracts list ──────────────────────────────────────────────────────── */
|
|
|
|
async function loadContractsList() {
|
|
try {
|
|
var data = await C.fetchJSON('/api/contracts');
|
|
var contracts = data.contracts || [];
|
|
var loading = document.getElementById('contractsLoading');
|
|
var empty = document.getElementById('contractsEmpty');
|
|
var wrap = document.getElementById('contractsTableWrap');
|
|
var count = document.getElementById('contractsCount');
|
|
if (loading) loading.style.display = 'none';
|
|
count.textContent = contracts.length + ' contract' + (contracts.length !== 1 ? 's' : '');
|
|
if (contracts.length === 0) {
|
|
empty.style.display = '';
|
|
return;
|
|
}
|
|
var tbody = document.getElementById('contractsBody');
|
|
tbody.innerHTML = '';
|
|
contracts.forEach(function(c) {
|
|
var tr = document.createElement('tr');
|
|
var idShort = C.short(c.contract_id, 20);
|
|
var deployer = c.deployer_pub ? C.shortAddress(c.deployer_pub) : '—';
|
|
var deployerHref = c.deployer_pub
|
|
? '<a href="/address?address=' + encodeURIComponent(c.deployer_pub) + '" class="mono" style="font-size:0.82rem">' + C.esc(deployer) + '</a>'
|
|
: '<span class="text-muted">—</span>';
|
|
tr.innerHTML =
|
|
'<td><a href="/contract?id=' + encodeURIComponent(c.contract_id) + '" class="mono" style="font-size:0.82rem">' + C.esc(idShort) + '</a></td>' +
|
|
'<td>' + deployerHref + '</td>' +
|
|
'<td class="text-muted">' + (c.deployed_at != null ? '#' + c.deployed_at : '—') + '</td>' +
|
|
'<td class="text-muted" style="font-size:0.8rem">' + (c.wasm_size != null ? c.wasm_size.toLocaleString() + ' B' : '—') + '</td>';
|
|
tbody.appendChild(tr);
|
|
});
|
|
wrap.style.display = '';
|
|
C.refreshIcons();
|
|
} catch (e) {
|
|
var loading = document.getElementById('contractsLoading');
|
|
if (loading) loading.textContent = 'Failed to load contracts: ' + e.message;
|
|
}
|
|
}
|
|
|
|
/* ── Load contract ───────────────────────────────────────────────────────── */
|
|
|
|
async function loadContract(id) {
|
|
if (!id) return;
|
|
C.setStatus('Loading…', 'warn');
|
|
document.getElementById('contractsList').style.display = 'none';
|
|
document.getElementById('mainContent').style.display = 'none';
|
|
document.getElementById('abiSection').style.display = 'none';
|
|
switchTab('Overview');
|
|
try {
|
|
var contract = await C.fetchJSON('/api/contracts/' + encodeURIComponent(id));
|
|
renderContract(contract);
|
|
window.history.replaceState({}, '', '/contract?id=' + encodeURIComponent(id));
|
|
C.setStatus('', '');
|
|
} catch (e) {
|
|
C.setStatus('Contract not found: ' + e.message, 'err');
|
|
document.getElementById('contractsList').style.display = '';
|
|
}
|
|
}
|
|
|
|
/* ── State browser ───────────────────────────────────────────────────────── */
|
|
|
|
async function queryState(key) {
|
|
if (!currentContractID || !key) return;
|
|
C.setStatus('Querying state…', 'warn');
|
|
document.getElementById('stateResult').style.display = 'none';
|
|
document.getElementById('stateEmpty').style.display = 'none';
|
|
try {
|
|
var data = await C.fetchJSON(
|
|
'/api/contracts/' + encodeURIComponent(currentContractID) +
|
|
'/state/' + encodeURIComponent(key)
|
|
);
|
|
C.setStatus('', '');
|
|
|
|
document.getElementById('stateKey').textContent = data.key || key;
|
|
|
|
if (data.value_hex === null || data.value_hex === undefined) {
|
|
// Key not set
|
|
document.getElementById('stateU64Row').style.display = 'none';
|
|
document.getElementById('stateHex').textContent = '—';
|
|
document.getElementById('stateB64').textContent = '—';
|
|
document.getElementById('stateNullRow').style.display = '';
|
|
} else {
|
|
document.getElementById('stateNullRow').style.display = 'none';
|
|
document.getElementById('stateHex').textContent = data.value_hex || '—';
|
|
document.getElementById('stateB64').textContent = data.value_b64 || '—';
|
|
if (data.value_u64 !== null && data.value_u64 !== undefined) {
|
|
document.getElementById('stateU64').textContent = data.value_u64.toLocaleString();
|
|
document.getElementById('stateU64Row').style.display = '';
|
|
} else {
|
|
document.getElementById('stateU64Row').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
document.getElementById('stateResult').style.display = '';
|
|
C.refreshIcons();
|
|
} catch (e) {
|
|
C.setStatus('State query failed: ' + e.message, 'err');
|
|
document.getElementById('stateEmpty').style.display = '';
|
|
}
|
|
}
|
|
|
|
document.getElementById('stateLoadBtn').addEventListener('click', function() {
|
|
var key = (document.getElementById('stateKeyInput').value || '').trim();
|
|
if (key) queryState(key);
|
|
});
|
|
|
|
document.getElementById('stateKeyInput').addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') document.getElementById('stateLoadBtn').click();
|
|
});
|
|
|
|
/* ── Logs ────────────────────────────────────────────────────────────────── */
|
|
|
|
var logsLoaded = false;
|
|
|
|
function makeLogRow(entry) {
|
|
var tr = document.createElement('tr');
|
|
var txShort = entry.tx_id ? C.short(entry.tx_id, 12) : '—';
|
|
var txLink = entry.tx_id
|
|
? '<a href="/tx?id=' + encodeURIComponent(entry.tx_id) + '" class="mono" style="font-size:0.8rem">' + C.esc(txShort) + '</a>'
|
|
: '<span class="text-muted">—</span>';
|
|
tr.innerHTML =
|
|
'<td><a href="/?block=' + entry.block_height + '">#' + entry.block_height + '</a></td>' +
|
|
'<td>' + txLink + '</td>' +
|
|
'<td class="text-muted" style="font-size:0.8rem">' + entry.seq + '</td>' +
|
|
'<td class="mono" style="font-size:0.85rem;word-break:break-all">' + C.esc(entry.message) + '</td>';
|
|
return tr;
|
|
}
|
|
|
|
async function loadLogs(contractID) {
|
|
if (logsLoaded) return;
|
|
logsLoaded = true;
|
|
try {
|
|
var data = await C.fetchJSON('/api/contracts/' + encodeURIComponent(contractID) + '/logs?limit=100');
|
|
var logs = data.logs || [];
|
|
if (logs.length === 0) {
|
|
document.getElementById('logsEmpty').style.display = '';
|
|
document.getElementById('logsTableWrap').style.display = 'none';
|
|
return;
|
|
}
|
|
var tbody = document.getElementById('logsBody');
|
|
tbody.innerHTML = '';
|
|
logs.forEach(function(entry) { tbody.appendChild(makeLogRow(entry)); });
|
|
document.getElementById('logsEmpty').style.display = 'none';
|
|
document.getElementById('logsTableWrap').style.display = '';
|
|
} catch (e) {
|
|
document.getElementById('logsEmpty').textContent = 'Failed to load logs: ' + e.message;
|
|
document.getElementById('logsEmpty').style.display = '';
|
|
}
|
|
}
|
|
|
|
// SSE — prepend live contract_log entries for the currently viewed contract.
|
|
C.connectSSE({
|
|
contract_log: function(entry) {
|
|
if (!currentContractID || entry.contract_id !== currentContractID) return;
|
|
var tbody = document.getElementById('logsBody');
|
|
if (!tbody) return;
|
|
tbody.insertBefore(makeLogRow(entry), tbody.firstChild);
|
|
document.getElementById('logsEmpty').style.display = 'none';
|
|
document.getElementById('logsTableWrap').style.display = '';
|
|
}
|
|
});
|
|
|
|
/* ── Copy buttons ────────────────────────────────────────────────────────── */
|
|
|
|
document.addEventListener('click', function(e) {
|
|
var t = e.target;
|
|
if (!t) return;
|
|
var btn = t.closest ? t.closest('.copy-btn') : null;
|
|
if (btn) {
|
|
var src = document.getElementById(btn.dataset.copyId);
|
|
if (src) {
|
|
navigator.clipboard.writeText(src.textContent || '').catch(function() {});
|
|
btn.classList.add('copy-btn-done');
|
|
setTimeout(function() { btn.classList.remove('copy-btn-done'); }, 1200);
|
|
}
|
|
}
|
|
});
|
|
|
|
/* ── Wiring ──────────────────────────────────────────────────────────────── */
|
|
|
|
document.getElementById('contractBtn').addEventListener('click', function() {
|
|
var val = (document.getElementById('contractInput').value || '').trim();
|
|
if (val) {
|
|
logsLoaded = false;
|
|
loadContract(val);
|
|
}
|
|
});
|
|
|
|
document.getElementById('contractInput').addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') document.getElementById('contractBtn').click();
|
|
});
|
|
|
|
var initial = C.q('id');
|
|
if (initial) {
|
|
document.getElementById('contractInput').value = initial;
|
|
loadContract(initial);
|
|
} else {
|
|
loadContractsList();
|
|
}
|
|
|
|
})();
|