Built-in transaction viewer: every ledger row verifiable at /tx/<hash>, explorer link when configured

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 05:37:29 -05:00
parent 1c31679b6c
commit 718d7f2d0e
12 changed files with 124 additions and 26 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ window.IAP = (function () {
div.innerHTML = '<span>' + describeEvent(ev, c) + '</span>'
+ (c.explorer
? '<span class="tx"><a target="_blank" rel="noopener" href="' + c.explorer + '/tx/' + ev.tx + '">verify ↗</a></span>'
: '<span class="tx mono">' + ev.tx.slice(0, 12) + '…</span>');
: '<span class="tx"><a href="/tx/' + ev.tx + '">verify ↗</a></span>'); // built-in viewer when the chain has no public explorer
return div;
}
// Render one served ad into #<elId>. Silent if no inventory.
+1 -1
View File
@@ -2,7 +2,7 @@
(async function () {
await IAP.renderNav('ledger');
const c = await IAP.getConfig();
IAP.$('contractLink').href = c.explorer + '/address/' + c.contract;
IAP.$('contractLink').href = c.explorer ? c.explorer + '/address/' + c.contract : '/contract';
const feed = IAP.$('feed');
const { events } = await (await fetch('/api/feed?n=150')).json();
+42
View File
@@ -0,0 +1,42 @@
// Transaction viewer: renders one tx from /api/tx/<hash> (the server-side RPC
// relay, so the browser never talks to the chain directly and CSP stays 'self').
// This is the "verify it yourself" page for chains without a public explorer;
// when the config carries an explorer URL, feed links point there instead.
(async function () {
await IAP.renderNav('ledger');
const c = await IAP.getConfig();
const hash = location.pathname.split('/').pop();
IAP.$('txHash').textContent = hash;
IAP.$('txChain').textContent = 'on ' + (c.chainName || 'the settlement chain');
let r = null;
try { r = await (await fetch('/api/tx/' + hash)).json(); } catch (e) {}
const stat = IAP.$('txStatus');
if (!r || !r.found) {
stat.textContent = 'not found';
stat.className = 'badge amber';
IAP.$('txHint').hidden = false;
return;
}
const ok = r.status === '0x1';
stat.textContent = ok ? '✓ confirmed' : '✗ reverted';
stat.className = 'badge' + (ok ? '' : ' amber');
const hx = v => { try { return parseInt(v, 16); } catch (e) { return 0; } };
const rows = [
['Block', '#' + hx(r.blockNumber).toLocaleString()],
['Time', r.ts ? new Date(hx(r.ts) * 1000).toLocaleString() : 'pending'],
['From', r.from || ''],
['To (contract)', r.to || ''],
['Value sent', IAP.fmtPol(r.valueWei || '0') + ' POL'],
['Gas used', hx(r.gasUsed).toLocaleString()]
];
const tb = IAP.$('txTable');
tb.hidden = false;
tb.querySelector('tbody').innerHTML = rows.map(x =>
'<tr><td class="muted small" style="white-space:nowrap">' + x[0] + '</td>'
+ '<td class="mono small" style="overflow-wrap:anywhere">' + String(x[1]).replace(/[&<>]/g, '') + '</td></tr>').join('');
if (r.events && r.events.length) {
IAP.$('evCard').hidden = false;
const feed = IAP.$('evFeed');
for (const ev of r.events) feed.appendChild(IAP.feedRow(ev, c));
}
})();