Files
dchain/node/explorer/contract.js
vsecoder 7e7393e4f8 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
2026-04-17 14:16:44 +03:00

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();
}
})();