209 lines
7.1 KiB
JavaScript
Raw Normal View History

2025-10-30 11:48:34 +01:00
// static/pages/TransactionsTable.js
2025-10-17 14:46:44 +02:00
import { h } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
2025-10-30 15:00:06 +01:00
import { API } from '../lib/api.js';
import { TABLE_SIZE } from '../lib/constants.js';
2025-10-20 15:42:12 +02:00
import {
streamNdjson,
ensureFixedRowCount,
2025-10-30 11:48:34 +01:00
shortenHex, // (kept in case you want to use later)
2025-10-20 15:42:12 +02:00
withBenignFilter,
2025-10-30 15:00:06 +01:00
} from '../lib/utils.js';
2025-10-20 15:42:12 +02:00
const OPERATIONS_PREVIEW_LIMIT = 2;
2025-10-30 11:48:34 +01:00
// ---------- small DOM helpers ----------
2025-10-20 15:42:12 +02:00
function createSpan(className, text, title) {
2025-10-30 11:48:34 +01:00
const el = document.createElement('span');
if (className) el.className = className;
if (title) el.title = title;
el.textContent = text;
return el;
2025-10-20 15:42:12 +02:00
}
function createLink(href, text, title) {
2025-10-30 11:48:34 +01:00
const el = document.createElement('a');
el.className = 'linkish mono';
el.href = href;
if (title) el.title = title;
el.textContent = text;
return el;
2025-10-20 15:42:12 +02:00
}
2025-10-30 11:48:34 +01:00
// ---------- coercion / formatting helpers ----------
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;
};
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 formatOperationsPreview(ops) {
if (!ops?.length) return '—';
const labels = ops.map(opLabel);
if (labels.length <= OPERATIONS_PREVIEW_LIMIT) return labels.join(', ');
const head = labels.slice(0, OPERATIONS_PREVIEW_LIMIT).join(', ');
const remainder = labels.length - OPERATIONS_PREVIEW_LIMIT;
return `${head} +${remainder}`;
}
2025-10-20 15:42:12 +02:00
2025-10-30 11:48:34 +01:00
// ---------- normalize API → view model ----------
function normalizeTransaction(raw) {
// { id, block_id, hash, operations:[Operation], inputs:[HexBytes], outputs:[Note], proof, execution_gas_price, storage_gas_price, created_at? }
const ops = Array.isArray(raw?.operations) ? raw.operations : Array.isArray(raw?.ops) ? raw.ops : [];
2025-10-20 15:42:12 +02:00
2025-10-30 11:48:34 +01:00
const outputs = Array.isArray(raw?.outputs) ? raw.outputs : [];
const totalOutputValue = outputs.reduce((sum, note) => sum + toNumber(note?.value), 0);
2025-10-20 15:42:12 +02:00
return {
id: raw?.id ?? '',
hash: raw?.hash ?? '',
2025-10-30 11:48:34 +01:00
operations: ops,
executionGasPrice: toNumber(raw?.execution_gas_price),
storageGasPrice: toNumber(raw?.storage_gas_price),
numberOfOutputs: outputs.length,
2025-10-20 15:42:12 +02:00
totalOutputValue,
};
}
2025-10-30 11:48:34 +01:00
// ---------- row builder ----------
function buildTransactionRow(tx) {
const tr = document.createElement('tr');
2025-10-20 15:42:12 +02:00
// Hash (replaces ID)
2025-10-30 11:48:34 +01:00
const tdId = document.createElement('td');
tdId.className = 'mono';
tdId.appendChild(createLink(`/transactions/${tx.hash}`, shortenHex(tx.hash), tx.hash));
2025-10-30 11:48:34 +01:00
// Operations (preview)
const tdOps = document.createElement('td');
const preview = formatOperationsPreview(tx.operations);
tdOps.appendChild(
createSpan('', preview, Array.isArray(tx.operations) ? tx.operations.map(opLabel).join(', ') : ''),
2025-10-20 15:42:12 +02:00
);
2025-10-30 11:48:34 +01:00
// Outputs (count / total)
const tdOut = document.createElement('td');
tdOut.className = 'amount';
tdOut.textContent = `${tx.numberOfOutputs} / ${tx.totalOutputValue.toLocaleString(undefined, { maximumFractionDigits: 8 })}`;
2025-10-20 15:42:12 +02:00
// Gas (execution / storage)
2025-10-30 11:48:34 +01:00
const tdGas = document.createElement('td');
tdGas.className = 'mono';
tdGas.textContent = `${tx.executionGasPrice.toLocaleString()} / ${tx.storageGasPrice.toLocaleString()}`;
2025-10-20 15:42:12 +02:00
2025-10-30 11:48:34 +01:00
tr.append(tdId, tdOps, tdOut, tdGas);
return tr;
2025-10-20 15:42:12 +02:00
}
2025-10-17 14:46:44 +02:00
2025-10-30 11:48:34 +01:00
// ---------- component ----------
2025-10-17 14:46:44 +02:00
export default function TransactionsTable() {
2025-10-30 11:48:34 +01:00
const bodyRef = useRef(null);
const countRef = useRef(null);
const abortRef = useRef(null);
const totalCountRef = useRef(0);
2025-10-17 14:46:44 +02:00
useEffect(() => {
2025-10-30 11:48:34 +01:00
const body = bodyRef.current;
const counter = countRef.current;
// 4 columns: Hash | Operations | Outputs | Gas
2025-10-30 11:48:34 +01:00
ensureFixedRowCount(body, 4, TABLE_SIZE);
2025-10-30 11:48:34 +01:00
abortRef.current?.abort();
abortRef.current = new AbortController();
2025-10-20 15:42:12 +02:00
const url = `${API.TRANSACTIONS_STREAM}?prefetch-limit=${encodeURIComponent(TABLE_SIZE)}`;
2025-10-20 15:42:12 +02:00
streamNdjson(
url,
2025-10-30 11:48:34 +01:00
(raw) => {
2025-10-20 15:42:12 +02:00
try {
2025-10-30 11:48:34 +01:00
const tx = normalizeTransaction(raw);
const row = buildTransactionRow(tx);
body.insertBefore(row, body.firstChild);
while (body.rows.length > TABLE_SIZE) body.deleteRow(-1);
counter.textContent = String(++totalCountRef.current);
} catch (err) {
console.error('Failed to render transaction row:', err, raw);
2025-10-20 15:42:12 +02:00
}
},
{
2025-10-30 11:48:34 +01:00
signal: abortRef.current.signal,
onError: withBenignFilter(
2025-10-30 11:48:34 +01:00
(err) => console.error('Transactions stream error:', err),
abortRef.current.signal,
2025-10-20 15:42:12 +02:00
),
},
2025-10-30 11:48:34 +01:00
).catch((err) => {
if (!abortRef.current.signal.aborted) {
console.error('Transactions stream connection error:', err);
2025-10-20 15:42:12 +02:00
}
2025-10-17 14:46:44 +02:00
});
2025-10-30 11:48:34 +01:00
return () => abortRef.current?.abort();
2025-10-17 14:46:44 +02:00
}, []);
return h(
'div',
{ class: 'card' },
h(
'div',
{ class: 'card-header' },
2025-10-30 11:48:34 +01:00
h('div', null, h('strong', null, 'Transactions '), h('span', { class: 'pill', ref: countRef }, '0')),
2025-10-20 15:42:12 +02:00
h('div', { style: 'color:var(--muted); font-size:12px;' }),
2025-10-17 14:46:44 +02:00
),
h(
'div',
{ class: 'table-wrapper' },
h(
'table',
{ class: 'table--transactions' },
h(
'colgroup',
null,
h('col', { style: 'width:240px' }), // Hash
2025-10-20 15:42:12 +02:00
h('col', null), // Operations
2025-10-30 11:48:34 +01:00
h('col', { style: 'width:200px' }), // Outputs (count / total)
h('col', { style: 'width:200px' }), // Gas (execution / storage)
2025-10-17 14:46:44 +02:00
),
h(
'thead',
null,
2025-10-20 15:42:12 +02:00
h(
'tr',
null,
h('th', null, 'Hash'),
2025-10-20 15:42:12 +02:00
h('th', null, 'Operations'),
h('th', null, 'Outputs (count / total)'),
h('th', null, 'Gas (execution / storage)'),
),
2025-10-17 14:46:44 +02:00
),
2025-10-30 11:48:34 +01:00
h('tbody', { ref: bodyRef }),
2025-10-17 14:46:44 +02:00
),
),
);
}