mirror of
https://github.com/logos-blockchain/logos-blockchain-block-explorer-template.git
synced 2026-01-02 05:03:07 +00:00
Use hash to handle blocks and transactions.
This commit is contained in:
parent
a997139155
commit
c3c357d09a
@ -1,13 +1,14 @@
|
||||
from http.client import NOT_FOUND
|
||||
from typing import TYPE_CHECKING, AsyncIterator, List
|
||||
|
||||
from fastapi import Path, Query
|
||||
from fastapi import Query
|
||||
from rusty_results import Empty, Option, Some
|
||||
from starlette.responses import JSONResponse, Response
|
||||
|
||||
from api.streams import into_ndjson_stream
|
||||
from api.v1.serializers.blocks import BlockRead
|
||||
from core.api import NBERequest, NDJsonStreamingResponse
|
||||
from core.types import dehexify
|
||||
from models.block import Block
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -30,8 +31,11 @@ async def stream(request: NBERequest, prefetch_limit: int = Query(0, alias="pref
|
||||
return NDJsonStreamingResponse(ndjson_blocks_stream)
|
||||
|
||||
|
||||
async def get(request: NBERequest, block_id: int = Path(ge=1)) -> Response:
|
||||
block = await request.app.state.block_repository.get_by_id(block_id)
|
||||
async def get(request: NBERequest, block_hash: str) -> Response:
|
||||
if not block_hash:
|
||||
return Response(status_code=NOT_FOUND)
|
||||
block_hash = dehexify(block_hash)
|
||||
block = await request.app.state.block_repository.get_by_hash(block_hash)
|
||||
return block.map(lambda _block: JSONResponse(BlockRead.from_block(_block).model_dump(mode="json"))).unwrap_or_else(
|
||||
lambda: Response(status_code=NOT_FOUND)
|
||||
)
|
||||
|
||||
@ -4,17 +4,20 @@ from . import blocks, health, index, transactions
|
||||
|
||||
|
||||
def create_v1_router() -> APIRouter:
|
||||
"""
|
||||
Route order must be preserved.
|
||||
"""
|
||||
router = APIRouter()
|
||||
|
||||
router.add_api_route("/", index.index, methods=["GET", "HEAD"])
|
||||
|
||||
router.add_api_route("/health", health.get, methods=["GET", "HEAD"])
|
||||
router.add_api_route("/health/stream", health.stream, methods=["GET", "HEAD"])
|
||||
router.add_api_route("/health", health.get, methods=["GET", "HEAD"])
|
||||
|
||||
router.add_api_route("/transactions/{transaction_id:int}", transactions.get, methods=["GET"])
|
||||
router.add_api_route("/transactions/stream", transactions.stream, methods=["GET"])
|
||||
router.add_api_route("/transactions/{transaction_hash:str}", transactions.get, methods=["GET"])
|
||||
|
||||
router.add_api_route("/blocks/{block_id:int}", blocks.get, methods=["GET"])
|
||||
router.add_api_route("/blocks/stream", blocks.stream, methods=["GET"])
|
||||
router.add_api_route("/blocks/{block_hash:str}", blocks.get, methods=["GET"])
|
||||
|
||||
return router
|
||||
|
||||
@ -10,7 +10,7 @@ from models.transactions.transaction import Transaction
|
||||
|
||||
class TransactionRead(NbeSchema):
|
||||
id: int
|
||||
block_id: int
|
||||
block_hash: HexBytes
|
||||
hash: HexBytes
|
||||
operations: List[Operation]
|
||||
inputs: List[HexBytes]
|
||||
@ -23,7 +23,7 @@ class TransactionRead(NbeSchema):
|
||||
def from_transaction(cls, transaction: Transaction) -> Self:
|
||||
return cls(
|
||||
id=transaction.id,
|
||||
block_id=transaction.block.id,
|
||||
block_hash=transaction.block.hash,
|
||||
hash=transaction.hash,
|
||||
operations=transaction.operations,
|
||||
inputs=transaction.inputs,
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
from http.client import NOT_FOUND
|
||||
from typing import TYPE_CHECKING, AsyncIterator, List
|
||||
|
||||
from fastapi import Path, Query
|
||||
from fastapi import Query
|
||||
from rusty_results import Empty, Option, Some
|
||||
from starlette.responses import JSONResponse, Response
|
||||
|
||||
from api.streams import into_ndjson_stream
|
||||
from api.v1.serializers.transactions import TransactionRead
|
||||
from core.api import NBERequest, NDJsonStreamingResponse
|
||||
from core.types import dehexify
|
||||
from models.transactions.transaction import Transaction
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -36,8 +37,11 @@ async def stream(request: NBERequest, prefetch_limit: int = Query(0, alias="pref
|
||||
return NDJsonStreamingResponse(ndjson_transactions_stream)
|
||||
|
||||
|
||||
async def get(request: NBERequest, transaction_id: int = Path(ge=1)) -> Response:
|
||||
transaction = await request.app.state.transaction_repository.get_by_id(transaction_id)
|
||||
async def get(request: NBERequest, transaction_hash: str) -> Response:
|
||||
if not transaction_hash:
|
||||
return Response(status_code=NOT_FOUND)
|
||||
transaction_hash = dehexify(transaction_hash)
|
||||
transaction = await request.app.state.transaction_repository.get_by_hash(transaction_hash)
|
||||
return transaction.map(
|
||||
lambda _transaction: JSONResponse(TransactionRead.from_transaction(_transaction).model_dump(mode="json"))
|
||||
).unwrap_or_else(lambda: Response(status_code=NOT_FOUND))
|
||||
|
||||
@ -7,6 +7,10 @@ def hexify(data: bytes) -> str:
|
||||
return data.hex()
|
||||
|
||||
|
||||
def dehexify(data: str) -> bytes:
|
||||
return bytes.fromhex(data)
|
||||
|
||||
|
||||
HexBytes = Annotated[
|
||||
bytes,
|
||||
PlainSerializer(hexify, return_type=str, when_used="json"),
|
||||
|
||||
@ -46,7 +46,7 @@ class BlockRepository:
|
||||
else:
|
||||
return Empty()
|
||||
|
||||
async def get_by_hash(self, block_hash: str) -> Option[Block]:
|
||||
async def get_by_hash(self, block_hash: bytes) -> Option[Block]:
|
||||
statement = select(Block).where(Block.hash == block_hash)
|
||||
|
||||
with self.client.session() as session:
|
||||
|
||||
@ -51,7 +51,7 @@ class TransactionRepository:
|
||||
else:
|
||||
return Empty()
|
||||
|
||||
async def get_by_hash(self, transaction_hash: str) -> Option[Transaction]:
|
||||
async def get_by_hash(self, transaction_hash: bytes) -> Option[Transaction]:
|
||||
statement = select(Transaction).where(Transaction.hash == transaction_hash)
|
||||
|
||||
with self.client.session() as session:
|
||||
|
||||
@ -15,8 +15,8 @@ export default function BlocksTable() {
|
||||
const body = bodyRef.current;
|
||||
const counter = countRef.current;
|
||||
|
||||
// 6 columns: ID | Slot | Hash | Parent | Block Root | Transactions
|
||||
ensureFixedRowCount(body, 6, TABLE_SIZE);
|
||||
// 5 columns: Hash | Slot | Parent | Block Root | Transactions
|
||||
ensureFixedRowCount(body, 5, TABLE_SIZE);
|
||||
|
||||
abortRef.current?.abort();
|
||||
abortRef.current = new AbortController();
|
||||
@ -33,14 +33,14 @@ export default function BlocksTable() {
|
||||
if (key) seenKeysRef.current.delete(key);
|
||||
body.deleteRow(-1);
|
||||
}
|
||||
// pad with placeholders to TABLE_SIZE (6 cols)
|
||||
ensureFixedRowCount(body, 6, TABLE_SIZE);
|
||||
// pad with placeholders to TABLE_SIZE (5 cols)
|
||||
ensureFixedRowCount(body, 5, TABLE_SIZE);
|
||||
const real = [...body.rows].filter((r) => !r.classList.contains('ph')).length;
|
||||
counter.textContent = String(real);
|
||||
};
|
||||
|
||||
const navigateToBlockDetail = (blockId) => {
|
||||
history.pushState({}, '', PAGE.BLOCK_DETAIL(blockId));
|
||||
const navigateToBlockDetail = (blockHash) => {
|
||||
history.pushState({}, '', PAGE.BLOCK_DETAIL(blockHash));
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
};
|
||||
|
||||
@ -48,15 +48,16 @@ export default function BlocksTable() {
|
||||
const tr = document.createElement('tr');
|
||||
tr.dataset.key = key;
|
||||
|
||||
// ID (clickable)
|
||||
// Hash (clickable, replaces ID)
|
||||
const tdId = document.createElement('td');
|
||||
const linkId = document.createElement('a');
|
||||
linkId.className = 'linkish mono';
|
||||
linkId.href = PAGE.BLOCK_DETAIL(b.id);
|
||||
linkId.textContent = String(b.id);
|
||||
linkId.href = PAGE.BLOCK_DETAIL(b.hash);
|
||||
linkId.textContent = shortenHex(b.hash);
|
||||
linkId.title = b.hash;
|
||||
linkId.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
navigateToBlockDetail(b.id);
|
||||
navigateToBlockDetail(b.hash);
|
||||
});
|
||||
tdId.appendChild(linkId);
|
||||
|
||||
@ -67,21 +68,18 @@ export default function BlocksTable() {
|
||||
spSlot.textContent = String(b.slot);
|
||||
tdSlot.appendChild(spSlot);
|
||||
|
||||
// Hash
|
||||
const tdHash = document.createElement('td');
|
||||
const spHash = document.createElement('span');
|
||||
spHash.className = 'mono';
|
||||
spHash.title = b.hash;
|
||||
spHash.textContent = shortenHex(b.hash);
|
||||
tdHash.appendChild(spHash);
|
||||
|
||||
// Parent (block.parent_block_hash)
|
||||
const tdParent = document.createElement('td');
|
||||
const spParent = document.createElement('span');
|
||||
spParent.className = 'mono';
|
||||
spParent.title = b.parent;
|
||||
spParent.textContent = shortenHex(b.parent);
|
||||
tdParent.appendChild(spParent);
|
||||
const linkParent = document.createElement('a');
|
||||
linkParent.className = 'linkish mono';
|
||||
linkParent.href = PAGE.BLOCK_DETAIL(b.parent);
|
||||
linkParent.textContent = shortenHex(b.parent);
|
||||
linkParent.title = b.parent;
|
||||
linkParent.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
navigateToBlockDetail(b.parent, e);
|
||||
});
|
||||
tdParent.appendChild(linkParent);
|
||||
|
||||
// Block Root
|
||||
const tdRoot = document.createElement('td');
|
||||
@ -98,7 +96,7 @@ export default function BlocksTable() {
|
||||
spCount.textContent = String(b.transactionCount);
|
||||
tdCount.appendChild(spCount);
|
||||
|
||||
tr.append(tdId, tdSlot, tdHash, tdParent, tdRoot, tdCount);
|
||||
tr.append(tdId, tdSlot, tdParent, tdRoot, tdCount);
|
||||
body.insertBefore(tr, body.firstChild);
|
||||
pruneAndPad();
|
||||
};
|
||||
@ -165,9 +163,8 @@ export default function BlocksTable() {
|
||||
h(
|
||||
'colgroup',
|
||||
null,
|
||||
h('col', { style: 'width:80px' }), // ID
|
||||
h('col', { style: 'width:90px' }), // Slot
|
||||
h('col', { style: 'width:240px' }), // Hash
|
||||
h('col', { style: 'width:90px' }), // Slot
|
||||
h('col', { style: 'width:240px' }), // Parent
|
||||
h('col', { style: 'width:240px' }), // Block Root
|
||||
h('col', { style: 'width:120px' }), // Transactions
|
||||
@ -178,9 +175,8 @@ export default function BlocksTable() {
|
||||
h(
|
||||
'tr',
|
||||
null,
|
||||
h('th', null, 'ID'),
|
||||
h('th', null, 'Slot'),
|
||||
h('th', null, 'Hash'),
|
||||
h('th', null, 'Slot'),
|
||||
h('th', null, 'Parent'),
|
||||
h('th', null, 'Block Root'),
|
||||
h('th', null, 'Transactions'),
|
||||
|
||||
@ -78,6 +78,7 @@ function normalizeTransaction(raw) {
|
||||
|
||||
return {
|
||||
id: raw?.id ?? '',
|
||||
hash: raw?.hash ?? '',
|
||||
operations: ops,
|
||||
executionGasPrice: toNumber(raw?.execution_gas_price),
|
||||
storageGasPrice: toNumber(raw?.storage_gas_price),
|
||||
@ -90,10 +91,10 @@ function normalizeTransaction(raw) {
|
||||
function buildTransactionRow(tx) {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// ID
|
||||
// Hash (replaces ID)
|
||||
const tdId = document.createElement('td');
|
||||
tdId.className = 'mono';
|
||||
tdId.appendChild(createLink(`/transactions/${tx.id}`, String(tx.id), String(tx.id)));
|
||||
tdId.appendChild(createLink(`/transactions/${tx.hash}`, shortenHex(tx.hash), tx.hash));
|
||||
|
||||
// Operations (preview)
|
||||
const tdOps = document.createElement('td');
|
||||
@ -127,7 +128,7 @@ export default function TransactionsTable() {
|
||||
const body = bodyRef.current;
|
||||
const counter = countRef.current;
|
||||
|
||||
// 4 columns: ID | Operations | Outputs | Gas
|
||||
// 4 columns: Hash | Operations | Outputs | Gas
|
||||
ensureFixedRowCount(body, 4, TABLE_SIZE);
|
||||
|
||||
abortRef.current?.abort();
|
||||
@ -183,7 +184,7 @@ export default function TransactionsTable() {
|
||||
h(
|
||||
'colgroup',
|
||||
null,
|
||||
h('col', { style: 'width:120px' }), // ID
|
||||
h('col', { style: 'width:240px' }), // Hash
|
||||
h('col', null), // Operations
|
||||
h('col', { style: 'width:200px' }), // Outputs (count / total)
|
||||
h('col', { style: 'width:200px' }), // Gas (execution / storage)
|
||||
@ -194,7 +195,7 @@ export default function TransactionsTable() {
|
||||
h(
|
||||
'tr',
|
||||
null,
|
||||
h('th', null, 'ID'),
|
||||
h('th', null, 'Hash'),
|
||||
h('th', null, 'Operations'),
|
||||
h('th', null, 'Outputs (count / total)'),
|
||||
h('th', null, 'Gas (execution / storage)'),
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
const API_PREFIX = '/api/v1';
|
||||
|
||||
const joinUrl = (...parts) => parts.join('/').replace(/\/{2,}/g, '/');
|
||||
const encodeId = (id) => encodeURIComponent(String(id));
|
||||
const encodeHash = (hash) => encodeURIComponent(String(hash));
|
||||
|
||||
const HEALTH_ENDPOINT = joinUrl(API_PREFIX, 'health/stream');
|
||||
|
||||
const TRANSACTION_DETAIL_BY_ID = (id) => joinUrl(API_PREFIX, 'transactions', encodeId(id));
|
||||
const TRANSACTION_DETAIL_BY_HASH = (hash) => joinUrl(API_PREFIX, 'transactions', encodeHash(hash));
|
||||
const TRANSACTIONS_STREAM = joinUrl(API_PREFIX, 'transactions/stream');
|
||||
|
||||
const BLOCK_DETAIL_BY_ID = (id) => joinUrl(API_PREFIX, 'blocks', encodeId(id));
|
||||
const BLOCK_DETAIL_BY_HASH = (hash) => joinUrl(API_PREFIX, 'blocks', encodeHash(hash));
|
||||
const BLOCKS_STREAM = joinUrl(API_PREFIX, 'blocks/stream');
|
||||
|
||||
export const API = {
|
||||
HEALTH_ENDPOINT,
|
||||
TRANSACTION_DETAIL_BY_ID,
|
||||
TRANSACTION_DETAIL_BY_HASH,
|
||||
TRANSACTIONS_STREAM,
|
||||
BLOCK_DETAIL_BY_ID,
|
||||
BLOCK_DETAIL_BY_HASH,
|
||||
BLOCKS_STREAM,
|
||||
};
|
||||
|
||||
const BLOCK_DETAIL = (id) => joinUrl('/blocks', encodeId(id));
|
||||
const TRANSACTION_DETAIL = (id) => joinUrl('/transactions', encodeId(id));
|
||||
const BLOCK_DETAIL = (hash) => joinUrl('/blocks', encodeHash(hash));
|
||||
const TRANSACTION_DETAIL = (hash) => joinUrl('/transactions', encodeHash(hash));
|
||||
|
||||
export const PAGE = {
|
||||
BLOCK_DETAIL,
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { API, PAGE } from '../lib/api.js';
|
||||
import { shortenHex } from '../lib/utils.js';
|
||||
|
||||
const OPERATIONS_PREVIEW_LIMIT = 2;
|
||||
|
||||
@ -73,15 +74,14 @@ function CopyPill({ text }) {
|
||||
}
|
||||
|
||||
export default function BlockDetailPage({ parameters }) {
|
||||
const blockIdParameter = parameters[0];
|
||||
const blockId = Number.parseInt(String(blockIdParameter), 10);
|
||||
const isValidId = Number.isInteger(blockId) && blockId >= 0;
|
||||
const blockHash = parameters[0];
|
||||
const isValidHash = typeof blockHash === 'string' && blockHash.length > 0;
|
||||
|
||||
const [block, setBlock] = useState(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [errorKind, setErrorKind] = useState(null); // 'invalid-id' | 'not-found' | 'network' | null
|
||||
const [errorKind, setErrorKind] = useState(null); // 'invalid-hash' | 'not-found' | 'network' | null
|
||||
|
||||
const pageTitle = useMemo(() => `Block ${String(blockIdParameter)}`, [blockIdParameter]);
|
||||
const pageTitle = useMemo(() => `Block ${shortenHex(blockHash)}`, [blockHash]);
|
||||
useEffect(() => {
|
||||
document.title = pageTitle;
|
||||
}, [pageTitle]);
|
||||
@ -91,9 +91,9 @@ export default function BlockDetailPage({ parameters }) {
|
||||
setErrorMessage('');
|
||||
setErrorKind(null);
|
||||
|
||||
if (!isValidId) {
|
||||
setErrorKind('invalid-id');
|
||||
setErrorMessage('Invalid block id.');
|
||||
if (!isValidHash) {
|
||||
setErrorKind('invalid-hash');
|
||||
setErrorMessage('Invalid block hash.');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -102,7 +102,7 @@ export default function BlockDetailPage({ parameters }) {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(API.BLOCK_DETAIL_BY_ID(blockId), {
|
||||
const res = await fetch(API.BLOCK_DETAIL_BY_HASH(blockHash), {
|
||||
cache: 'no-cache',
|
||||
signal: controller.signal,
|
||||
});
|
||||
@ -127,7 +127,7 @@ export default function BlockDetailPage({ parameters }) {
|
||||
alive = false;
|
||||
controller.abort();
|
||||
};
|
||||
}, [blockId, isValidId]);
|
||||
}, [blockHash, isValidHash]);
|
||||
|
||||
const header = block?.header ?? {}; // back-compat only
|
||||
const transactions = Array.isArray(block?.transactions) ? block.transactions : [];
|
||||
@ -135,8 +135,7 @@ export default function BlockDetailPage({ parameters }) {
|
||||
// Prefer new top-level fields; fallback to legacy header.*
|
||||
const slot = block?.slot ?? header?.slot ?? null;
|
||||
const blockRoot = block?.block_root ?? header?.block_root ?? '';
|
||||
const blockHash = block?.hash ?? header?.hash ?? '';
|
||||
const parentId = block?.parent_id ?? null;
|
||||
const currentBlockHash = block?.hash ?? header?.hash ?? '';
|
||||
const parentHash = block?.parent_block_hash ?? header?.parent_block ?? '';
|
||||
|
||||
return h(
|
||||
@ -152,7 +151,7 @@ export default function BlockDetailPage({ parameters }) {
|
||||
),
|
||||
|
||||
// Error states
|
||||
errorKind === 'invalid-id' && h('p', { style: 'color:#ff8a8a' }, errorMessage),
|
||||
errorKind === 'invalid-hash' && h('p', { style: 'color:#ff8a8a' }, errorMessage),
|
||||
errorKind === 'not-found' &&
|
||||
h(
|
||||
'div',
|
||||
@ -202,12 +201,12 @@ export default function BlockDetailPage({ parameters }) {
|
||||
'span',
|
||||
{
|
||||
class: 'pill mono',
|
||||
title: blockHash,
|
||||
title: currentBlockHash,
|
||||
style: 'max-width:100%; overflow-wrap:anywhere; word-break:break-word;',
|
||||
},
|
||||
String(blockHash),
|
||||
String(currentBlockHash),
|
||||
),
|
||||
h(CopyPill, { text: blockHash }),
|
||||
h(CopyPill, { text: currentBlockHash }),
|
||||
),
|
||||
|
||||
// Root (pill + copy)
|
||||
@ -227,32 +226,32 @@ export default function BlockDetailPage({ parameters }) {
|
||||
h(CopyPill, { text: blockRoot }),
|
||||
),
|
||||
|
||||
// Parent (id link OR parent hash) + copy
|
||||
// Parent (parent hash link) + copy
|
||||
h('div', null, h('b', null, 'Parent:')),
|
||||
h(
|
||||
'div',
|
||||
{ style: 'display:flex; gap:8px; flex-wrap:wrap; align-items:flex-start;' },
|
||||
parentId != null
|
||||
parentHash
|
||||
? h(
|
||||
'a',
|
||||
{
|
||||
class: 'pill mono linkish',
|
||||
href: PAGE.BLOCK_DETAIL(parentId),
|
||||
title: String(parentId),
|
||||
href: PAGE.BLOCK_DETAIL(parentHash),
|
||||
title: String(parentHash),
|
||||
style: 'max-width:100%; overflow-wrap:anywhere; word-break:break-word;',
|
||||
},
|
||||
String(parentId),
|
||||
shortenHex(parentHash),
|
||||
)
|
||||
: h(
|
||||
'span',
|
||||
{
|
||||
class: 'pill mono',
|
||||
title: parentHash,
|
||||
title: '—',
|
||||
style: 'max-width:100%; overflow-wrap:anywhere; word-break:break-word;',
|
||||
},
|
||||
String(parentHash || '—'),
|
||||
'—',
|
||||
),
|
||||
h(CopyPill, { text: parentId ?? parentHash }),
|
||||
h(CopyPill, { text: parentHash }),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -282,7 +281,11 @@ export default function BlockDetailPage({ parameters }) {
|
||||
h(
|
||||
'tr',
|
||||
null,
|
||||
h('th', { style: 'text-align:left; padding:8px 10px; white-space:nowrap;' }, 'ID'),
|
||||
h(
|
||||
'th',
|
||||
{ style: 'text-align:left; padding:8px 10px; white-space:nowrap;' },
|
||||
'Hash',
|
||||
),
|
||||
h(
|
||||
'th',
|
||||
{ style: 'text-align:center; padding:8px 10px; white-space:nowrap;' },
|
||||
@ -311,8 +314,8 @@ export default function BlockDetailPage({ parameters }) {
|
||||
|
||||
return h(
|
||||
'tr',
|
||||
{ key: t?.id ?? `${count}/${total}` },
|
||||
// ID (left)
|
||||
{ key: t?.hash ?? `${count}/${total}` },
|
||||
// Hash (left)
|
||||
h(
|
||||
'td',
|
||||
{ style: 'text-align:left; padding:8px 10px; white-space:nowrap;' },
|
||||
@ -320,10 +323,10 @@ export default function BlockDetailPage({ parameters }) {
|
||||
'a',
|
||||
{
|
||||
class: 'linkish mono',
|
||||
href: PAGE.TRANSACTION_DETAIL(t?.id ?? ''),
|
||||
title: String(t?.id ?? ''),
|
||||
href: PAGE.TRANSACTION_DETAIL(t?.hash ?? ''),
|
||||
title: String(t?.hash ?? ''),
|
||||
},
|
||||
String(t?.id ?? ''),
|
||||
shortenHex(t?.hash ?? ''),
|
||||
),
|
||||
),
|
||||
// Outputs (center)
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
// static/pages/TransactionDetail.js
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useEffect, useMemo, useState } from 'preact/hooks';
|
||||
import { API } from '../lib/api.js';
|
||||
import { API, PAGE } from '../lib/api.js';
|
||||
import { shortenHex } from '../lib/utils.js';
|
||||
|
||||
// ————— helpers —————
|
||||
const isNumber = (v) => typeof v === 'number' && !Number.isNaN(v);
|
||||
@ -105,7 +106,7 @@ function normalizeTransaction(raw) {
|
||||
|
||||
return {
|
||||
id: raw?.id ?? '',
|
||||
blockId: raw?.block_id ?? null,
|
||||
blockHash: raw?.block_hash ?? null,
|
||||
hash: renderBytes(raw?.hash),
|
||||
proof: renderBytes(raw?.proof),
|
||||
operations: ops, // keep objects, we’ll label in UI
|
||||
@ -143,15 +144,15 @@ function Summary({ tx }) {
|
||||
{ style: 'display:grid; gap:8px;' },
|
||||
|
||||
// Block link
|
||||
tx.blockId != null &&
|
||||
tx.blockHash != null &&
|
||||
h(
|
||||
'div',
|
||||
null,
|
||||
h('b', null, 'Block: '),
|
||||
h(
|
||||
'a',
|
||||
{ class: 'linkish mono', href: API.BLOCK_DETAIL_BY_ID(tx.blockId), title: String(tx.blockId) },
|
||||
String(tx.blockId),
|
||||
{ class: 'linkish mono', href: PAGE.BLOCK_DETAIL(tx.blockHash), title: String(tx.blockHash) },
|
||||
shortenHex(tx.blockHash),
|
||||
),
|
||||
),
|
||||
|
||||
@ -349,14 +350,13 @@ function Ledger({ ledger }) {
|
||||
|
||||
// ————— 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 transactionHash = parameters?.[0];
|
||||
const isValidHash = typeof transactionHash === 'string' && transactionHash.length > 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]);
|
||||
const pageTitle = useMemo(() => `Transaction ${shortenHex(transactionHash)}`, [transactionHash]);
|
||||
useEffect(() => {
|
||||
document.title = pageTitle;
|
||||
}, [pageTitle]);
|
||||
@ -365,8 +365,8 @@ export default function TransactionDetail({ parameters }) {
|
||||
setTx(null);
|
||||
setErr(null);
|
||||
|
||||
if (!isValidId) {
|
||||
setErr({ kind: 'invalid', msg: 'Invalid transaction id.' });
|
||||
if (!isValidHash) {
|
||||
setErr({ kind: 'invalid', msg: 'Invalid transaction hash.' });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -375,7 +375,7 @@ export default function TransactionDetail({ parameters }) {
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(API.TRANSACTION_DETAIL_BY_ID(id), {
|
||||
const res = await fetch(API.TRANSACTION_DETAIL_BY_HASH(transactionHash), {
|
||||
cache: 'no-cache',
|
||||
signal: controller.signal,
|
||||
});
|
||||
@ -397,7 +397,7 @@ export default function TransactionDetail({ parameters }) {
|
||||
alive = false;
|
||||
controller.abort();
|
||||
};
|
||||
}, [id, isValidId]);
|
||||
}, [transactionHash, isValidHash]);
|
||||
|
||||
return h(
|
||||
'main',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user