2025-10-20 15:42:12 +02:00
|
|
|
|
// static/pages/TransactionDetail.js
|
|
|
|
|
|
import { h, Fragment } from 'preact';
|
|
|
|
|
|
import { useEffect, useMemo, useState } from 'preact/hooks';
|
|
|
|
|
|
import { API } from '../lib/api.js?dev=1';
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-20 15:42:12 +02:00
|
|
|
|
// ————— helpers —————
|
|
|
|
|
|
const isNumber = (v) => typeof v === 'number' && !Number.isNaN(v);
|
|
|
|
|
|
const toLocaleNum = (n, opts = {}) => Number(n ?? 0).toLocaleString(undefined, { maximumFractionDigits: 8, ...opts });
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
// Best-effort pretty bytes/hex/string
|
2025-10-20 15:42:12 +02:00
|
|
|
|
function renderBytes(value) {
|
2025-10-30 11:48:34 +01:00
|
|
|
|
if (value == null) return '';
|
|
|
|
|
|
if (typeof value === 'string') return value; // hex/base64/plain
|
2025-10-20 15:42:12 +02:00
|
|
|
|
if (Array.isArray(value) && value.every((x) => Number.isInteger(x) && x >= 0 && x <= 255)) {
|
|
|
|
|
|
return '0x' + value.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.stringify(value);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return String(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
const opLabel = (op) => {
|
|
|
|
|
|
if (op == null) return 'op';
|
|
|
|
|
|
if (typeof op === 'string' || typeof op === 'number') return String(op);
|
|
|
|
|
|
if (typeof op !== 'object') return String(op);
|
|
|
|
|
|
if (typeof op.type === 'string') return op.type;
|
|
|
|
|
|
if (typeof op.kind === 'string') return op.kind;
|
|
|
|
|
|
if (op.content) {
|
|
|
|
|
|
if (typeof op.content.type === 'string') return op.content.type;
|
|
|
|
|
|
if (typeof op.content.kind === 'string') return op.content.kind;
|
|
|
|
|
|
}
|
|
|
|
|
|
const keys = Object.keys(op);
|
|
|
|
|
|
return keys.length ? keys[0] : 'op';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function opsToPills(ops, limit = 6) {
|
|
|
|
|
|
const arr = Array.isArray(ops) ? ops : [];
|
|
|
|
|
|
if (!arr.length) return h('span', { style: 'color:var(--muted); whiteSpace: "nowrap";' }, '—');
|
|
|
|
|
|
const labels = arr.map(opLabel);
|
|
|
|
|
|
const shown = labels.slice(0, limit);
|
|
|
|
|
|
const extra = labels.length - shown.length;
|
|
|
|
|
|
return h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
{ style: 'display:flex; gap:6px; flexWrap:"wrap"; alignItems:"center"' },
|
|
|
|
|
|
...shown.map((label, i) =>
|
|
|
|
|
|
h('span', { key: `${label}-${i}`, class: 'pill', title: label, style: 'flex:0 0 auto;' }, label),
|
|
|
|
|
|
),
|
|
|
|
|
|
extra > 0 && h('span', { class: 'pill', title: `${extra} more`, style: 'flex:0 0 auto;' }, `+${extra}`),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const toNumber = (v) => {
|
|
|
|
|
|
if (v == null) return 0;
|
|
|
|
|
|
if (typeof v === 'number') return v;
|
|
|
|
|
|
if (typeof v === 'bigint') return Number(v);
|
|
|
|
|
|
if (typeof v === 'string') {
|
|
|
|
|
|
const s = v.trim();
|
|
|
|
|
|
if (/^0x[0-9a-f]+$/i.test(s)) return Number(BigInt(s));
|
|
|
|
|
|
const n = Number(s);
|
|
|
|
|
|
return Number.isFinite(n) ? n : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof v === 'object' && v !== null && 'value' in v) return toNumber(v.value);
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function CopyPill({ text, label = 'Copy' }) {
|
|
|
|
|
|
const onCopy = async (e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
try {
|
|
|
|
|
|
await navigator.clipboard.writeText(String(text ?? ''));
|
|
|
|
|
|
} catch {}
|
|
|
|
|
|
};
|
|
|
|
|
|
return h(
|
|
|
|
|
|
'a',
|
|
|
|
|
|
{
|
|
|
|
|
|
class: 'pill linkish mono',
|
|
|
|
|
|
style: 'cursor:pointer; user-select:none;',
|
|
|
|
|
|
href: '#',
|
|
|
|
|
|
onClick: onCopy,
|
|
|
|
|
|
onKeyDown: (e) => {
|
|
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
onCopy(e);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
tabIndex: 0,
|
|
|
|
|
|
role: 'button',
|
|
|
|
|
|
},
|
|
|
|
|
|
label,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ————— normalizer for new TransactionRead —————
|
|
|
|
|
|
// { id, block_id, hash, operations:[Operation], inputs:[HexBytes], outputs:[Note{public_key:HexBytes,value:int}],
|
|
|
|
|
|
// proof, execution_gas_price, storage_gas_price }
|
2025-10-20 15:42:12 +02:00
|
|
|
|
function normalizeTransaction(raw) {
|
2025-10-30 11:48:34 +01:00
|
|
|
|
const ops = Array.isArray(raw?.operations) ? raw.operations : Array.isArray(raw?.ops) ? raw.ops : [];
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
const inputs = Array.isArray(raw?.inputs) ? raw.inputs : [];
|
|
|
|
|
|
const outputs = Array.isArray(raw?.outputs) ? raw.outputs : [];
|
|
|
|
|
|
|
|
|
|
|
|
const totalOutputValue = outputs.reduce((sum, note) => sum + toNumber(note?.value), 0);
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-20 15:42:12 +02:00
|
|
|
|
return {
|
|
|
|
|
|
id: raw?.id ?? '',
|
|
|
|
|
|
blockId: raw?.block_id ?? null,
|
2025-10-30 11:48:34 +01:00
|
|
|
|
hash: renderBytes(raw?.hash),
|
|
|
|
|
|
proof: renderBytes(raw?.proof),
|
|
|
|
|
|
operations: ops, // keep objects, we’ll label in UI
|
2025-10-20 15:42:12 +02:00
|
|
|
|
executionGasPrice: isNumber(raw?.execution_gas_price)
|
|
|
|
|
|
? raw.execution_gas_price
|
2025-10-30 11:48:34 +01:00
|
|
|
|
: toNumber(raw?.execution_gas_price),
|
|
|
|
|
|
storageGasPrice: isNumber(raw?.storage_gas_price) ? raw.storage_gas_price : toNumber(raw?.storage_gas_price),
|
|
|
|
|
|
ledger: {
|
|
|
|
|
|
inputs: inputs.map((v) => renderBytes(v)),
|
|
|
|
|
|
outputs: outputs.map((n) => ({
|
|
|
|
|
|
public_key: renderBytes(n?.public_key),
|
|
|
|
|
|
value: toNumber(n?.value),
|
|
|
|
|
|
})),
|
|
|
|
|
|
totalOutputValue,
|
|
|
|
|
|
},
|
2025-10-20 15:42:12 +02:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-20 15:42:12 +02:00
|
|
|
|
// ————— UI bits —————
|
|
|
|
|
|
function SectionCard({ title, children, style }) {
|
2025-10-17 14:46:44 +02:00
|
|
|
|
return h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'div',
|
|
|
|
|
|
{ class: 'card', style: `margin-top:12px; ${style ?? ''}` },
|
|
|
|
|
|
h('div', { class: 'card-header' }, h('strong', null, title)),
|
|
|
|
|
|
h('div', { style: 'padding:12px 14px' }, children),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-20 15:42:12 +02:00
|
|
|
|
function Summary({ tx }) {
|
|
|
|
|
|
return h(
|
|
|
|
|
|
SectionCard,
|
|
|
|
|
|
{ title: 'Summary' },
|
2025-10-17 14:46:44 +02:00
|
|
|
|
h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'div',
|
|
|
|
|
|
{ style: 'display:grid; gap:8px;' },
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
// Block link
|
2025-10-20 15:42:12 +02:00
|
|
|
|
tx.blockId != null &&
|
2025-10-17 14:46:44 +02:00
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
2025-10-20 15:42:12 +02:00
|
|
|
|
null,
|
|
|
|
|
|
h('b', null, 'Block: '),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'a',
|
|
|
|
|
|
{ class: 'linkish mono', href: API.BLOCK_DETAIL_BY_ID(tx.blockId), title: String(tx.blockId) },
|
|
|
|
|
|
String(tx.blockId),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
),
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
|
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
// Hash + copy
|
|
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h('b', null, 'Hash: '),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'span',
|
|
|
|
|
|
{ class: 'pill mono', title: tx.hash, style: 'max-width:100%; overflow-wrap:anywhere;' },
|
|
|
|
|
|
String(tx.hash || ''),
|
|
|
|
|
|
),
|
|
|
|
|
|
h(CopyPill, { text: tx.hash }),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Proof + copy (if present)
|
|
|
|
|
|
tx.proof &&
|
|
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h('b', null, 'Proof: '),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'span',
|
|
|
|
|
|
{ class: 'pill mono', title: tx.proof, style: 'max-width:100%; overflow-wrap:anywhere;' },
|
|
|
|
|
|
String(tx.proof),
|
|
|
|
|
|
),
|
|
|
|
|
|
h(CopyPill, { text: tx.proof }),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Gas
|
2025-10-20 15:42:12 +02:00
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h('b', null, 'Execution Gas: '),
|
|
|
|
|
|
h('span', { class: 'mono' }, toLocaleNum(tx.executionGasPrice)),
|
|
|
|
|
|
),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h('b', null, 'Storage Gas: '),
|
|
|
|
|
|
h('span', { class: 'mono' }, toLocaleNum(tx.storageGasPrice)),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
// Operations (labels as pills)
|
|
|
|
|
|
h('div', null, h('b', null, 'Operations: '), opsToPills(tx.operations)),
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function InputsTable({ inputs }) {
|
2025-10-30 11:48:34 +01:00
|
|
|
|
if (!inputs?.length) return h('div', { style: 'color:var(--muted)' }, '—');
|
2025-10-20 15:42:12 +02:00
|
|
|
|
|
|
|
|
|
|
return h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
{ class: 'table-wrapper', style: 'margin-top:6px;' },
|
|
|
|
|
|
h(
|
|
|
|
|
|
'table',
|
|
|
|
|
|
{ class: 'table--transactions' },
|
|
|
|
|
|
h(
|
|
|
|
|
|
'colgroup',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h('col', { style: 'width:80px' }), // #
|
2025-10-30 11:48:34 +01:00
|
|
|
|
h('col', null), // Value
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
|
|
|
|
|
h('thead', null, h('tr', null, h('th', { style: 'text-align:center;' }, '#'), h('th', null, 'Value'))),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'tbody',
|
|
|
|
|
|
null,
|
|
|
|
|
|
...inputs.map((fr, i) =>
|
2025-10-17 14:46:44 +02:00
|
|
|
|
h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'tr',
|
|
|
|
|
|
{ key: i },
|
|
|
|
|
|
h('td', { style: 'text-align:center;' }, String(i)),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'td',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h(
|
|
|
|
|
|
'span',
|
|
|
|
|
|
{ class: 'mono', style: 'overflow-wrap:anywhere; word-break:break-word;' },
|
|
|
|
|
|
String(fr),
|
|
|
|
|
|
),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function OutputsTable({ outputs }) {
|
2025-10-30 11:48:34 +01:00
|
|
|
|
if (!outputs?.length) return h('div', { style: 'color:var(--muted)' }, '—');
|
2025-10-20 15:42:12 +02:00
|
|
|
|
|
|
|
|
|
|
return h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
{ class: 'table-wrapper', style: 'margin-top:6px;' },
|
|
|
|
|
|
h(
|
|
|
|
|
|
'table',
|
|
|
|
|
|
{ class: 'table--transactions' },
|
|
|
|
|
|
h(
|
|
|
|
|
|
'colgroup',
|
|
|
|
|
|
null,
|
2025-10-30 11:48:34 +01:00
|
|
|
|
h('col', { style: 'width:80px' }), // #
|
|
|
|
|
|
h('col', null), // Public Key
|
|
|
|
|
|
h('col', { style: 'width:180px' }), // Value
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'thead',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h(
|
|
|
|
|
|
'tr',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h('th', { style: 'text-align:center;' }, '#'),
|
2025-10-30 11:48:34 +01:00
|
|
|
|
h('th', null, 'Public Key'),
|
|
|
|
|
|
h('th', { style: 'text-align:right;' }, 'Value'),
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'tbody',
|
|
|
|
|
|
null,
|
|
|
|
|
|
...outputs.map((note, idx) =>
|
2025-10-17 14:46:44 +02:00
|
|
|
|
h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'tr',
|
|
|
|
|
|
{ key: idx },
|
|
|
|
|
|
h('td', { style: 'text-align:center;' }, String(idx)),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'td',
|
2025-10-17 14:46:44 +02:00
|
|
|
|
null,
|
|
|
|
|
|
h(
|
2025-10-20 15:42:12 +02:00
|
|
|
|
'span',
|
2025-10-30 11:48:34 +01:00
|
|
|
|
{ class: 'mono', style: 'display:inline-block; overflow-wrap:anywhere;' },
|
|
|
|
|
|
String(note.public_key ?? ''),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
),
|
2025-10-30 11:48:34 +01:00
|
|
|
|
h('span', { class: 'sr-only' }, ' '),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
),
|
2025-10-30 11:48:34 +01:00
|
|
|
|
h('td', { class: 'amount', style: 'text-align:right;' }, toLocaleNum(note.value)),
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
),
|
|
|
|
|
|
),
|
2025-10-20 15:42:12 +02:00
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function Ledger({ ledger }) {
|
2025-10-30 11:48:34 +01:00
|
|
|
|
const inputs = Array.isArray(ledger?.inputs) ? ledger.inputs : [];
|
|
|
|
|
|
const outputs = Array.isArray(ledger?.outputs) ? ledger.outputs : [];
|
|
|
|
|
|
const totalInputValue = inputs.reduce((s, v) => s + toNumber(v), 0);
|
|
|
|
|
|
const totalOutputValue = toNumber(ledger?.totalOutputValue);
|
2025-10-20 15:42:12 +02:00
|
|
|
|
|
|
|
|
|
|
return h(
|
|
|
|
|
|
SectionCard,
|
2025-10-30 11:48:34 +01:00
|
|
|
|
{ title: 'Ledger' },
|
2025-10-20 15:42:12 +02:00
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
{ style: 'display:grid; gap:16px;' },
|
|
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
// Inputs
|
2025-10-20 15:42:12 +02:00
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
2025-10-30 11:48:34 +01:00
|
|
|
|
{ style: 'display:flex; alignItems:center; gap:8px;' },
|
2025-10-20 15:42:12 +02:00
|
|
|
|
h('b', null, 'Inputs'),
|
|
|
|
|
|
h('span', { class: 'pill' }, String(inputs.length)),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'span',
|
|
|
|
|
|
{ class: 'amount', style: 'margin-left:auto;' },
|
|
|
|
|
|
`Total: ${toLocaleNum(totalInputValue)}`,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
h(InputsTable, { inputs }),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
|
// Outputs
|
2025-10-20 15:42:12 +02:00
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
|
|
|
|
|
null,
|
|
|
|
|
|
h(
|
|
|
|
|
|
'div',
|
2025-10-30 11:48:34 +01:00
|
|
|
|
{ style: 'display:flex; alignItems:center; gap:8px;' },
|
2025-10-20 15:42:12 +02:00
|
|
|
|
h('b', null, 'Outputs'),
|
|
|
|
|
|
h('span', { class: 'pill' }, String(outputs.length)),
|
|
|
|
|
|
h(
|
|
|
|
|
|
'span',
|
|
|
|
|
|
{ class: 'amount', style: 'margin-left:auto;' },
|
|
|
|
|
|
`Total: ${toLocaleNum(totalOutputValue)}`,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
h(OutputsTable, { outputs }),
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ————— page —————
|
|
|
|
|
|
export default function TransactionDetail({ parameters }) {
|
|
|
|
|
|
const idParam = parameters?.[0];
|
|
|
|
|
|
const id = Number.parseInt(String(idParam), 10);
|
|
|
|
|
|
const isValidId = Number.isInteger(id) && id >= 0;
|
|
|
|
|
|
|
|
|
|
|
|
const [tx, setTx] = useState(null);
|
|
|
|
|
|
const [err, setErr] = useState(null); // { kind: 'invalid'|'not-found'|'network', msg: string }
|
|
|
|
|
|
|
|
|
|
|
|
const pageTitle = useMemo(() => `Transaction ${String(idParam)}`, [idParam]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
document.title = pageTitle;
|
|
|
|
|
|
}, [pageTitle]);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setTx(null);
|
|
|
|
|
|
setErr(null);
|
|
|
|
|
|
|
|
|
|
|
|
if (!isValidId) {
|
|
|
|
|
|
setErr({ kind: 'invalid', msg: 'Invalid transaction id.' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let alive = true;
|
|
|
|
|
|
const controller = new AbortController();
|
|
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(API.TRANSACTION_DETAIL_BY_ID(id), {
|
|
|
|
|
|
cache: 'no-cache',
|
|
|
|
|
|
signal: controller.signal,
|
|
|
|
|
|
});
|
|
|
|
|
|
if (res.status === 404 || res.status === 410) {
|
|
|
|
|
|
if (alive) setErr({ kind: 'not-found', msg: 'Transaction not found.' });
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
|
|
|
|
const payload = await res.json();
|
|
|
|
|
|
if (!alive) return;
|
|
|
|
|
|
setTx(normalizeTransaction(payload));
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (!alive || e?.name === 'AbortError') return;
|
|
|
|
|
|
setErr({ kind: 'network', msg: e?.message ?? 'Failed to load transaction' });
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
alive = false;
|
|
|
|
|
|
controller.abort();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, [id, isValidId]);
|
|
|
|
|
|
|
|
|
|
|
|
return h(
|
|
|
|
|
|
'main',
|
|
|
|
|
|
{ class: 'wrap' },
|
|
|
|
|
|
|
|
|
|
|
|
h(
|
|
|
|
|
|
'header',
|
2025-10-30 11:48:34 +01:00
|
|
|
|
{ style: 'display:flex; gap:12px; alignItems:center; margin:12px 0;' },
|
2025-10-20 15:42:12 +02:00
|
|
|
|
h('a', { class: 'linkish', href: '/' }, '← Back'),
|
|
|
|
|
|
h('h1', { style: 'margin:0' }, pageTitle),
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
|
|
// Errors
|
|
|
|
|
|
err?.kind === 'invalid' && h('p', { style: 'color:#ff8a8a' }, err.msg),
|
|
|
|
|
|
err?.kind === 'not-found' &&
|
|
|
|
|
|
h(
|
|
|
|
|
|
SectionCard,
|
|
|
|
|
|
{ title: 'Transaction not found' },
|
|
|
|
|
|
h('p', null, 'We could not find a transaction with that identifier.'),
|
|
|
|
|
|
),
|
|
|
|
|
|
err?.kind === 'network' && h('p', { style: 'color:#ff8a8a' }, `Error: ${err.msg}`),
|
|
|
|
|
|
|
|
|
|
|
|
// Loading
|
|
|
|
|
|
!tx && !err && h('p', null, 'Loading…'),
|
|
|
|
|
|
|
|
|
|
|
|
// Success
|
|
|
|
|
|
tx && h(Fragment, null, h(Summary, { tx }), h(Ledger, { ledger: tx.ledger })),
|
2025-10-17 14:46:44 +02:00
|
|
|
|
);
|
|
|
|
|
|
}
|