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