diff --git a/src/api/v1/serializers/blocks.py b/src/api/v1/serializers/blocks.py index 3a01fa5..f5afd1d 100644 --- a/src/api/v1/serializers/blocks.py +++ b/src/api/v1/serializers/blocks.py @@ -1,4 +1,4 @@ -from typing import List, Self +from typing import List, Optional, Self from core.models import NbeSchema from core.types import HexBytes @@ -16,6 +16,8 @@ class BlockRead(NbeSchema): fork: int block_root: HexBytes proof_of_leadership: ProofOfLeadership + lib: Optional[HexBytes] = None + tip: Optional[HexBytes] = None transactions: List[Transaction] @classmethod @@ -29,5 +31,7 @@ class BlockRead(NbeSchema): fork=block.fork, block_root=block.block_root, proof_of_leadership=block.proof_of_leadership, + lib=block.lib, + tip=block.tip, transactions=block.transactions, ) diff --git a/src/models/block.py b/src/models/block.py index e4e0dd3..5885080 100644 --- a/src/models/block.py +++ b/src/models/block.py @@ -1,5 +1,5 @@ import logging -from typing import TYPE_CHECKING, List, Self +from typing import TYPE_CHECKING, List, Optional, Self from sqlalchemy import Column from sqlmodel import Field, Relationship @@ -30,6 +30,8 @@ class Block(TimestampedModel, table=True): proof_of_leadership: ProofOfLeadership = Field( sa_column=Column(PydanticJsonColumn(ProofOfLeadership), nullable=False) ) + lib: Optional[HexBytes] = Field(default=None, nullable=True) + tip: Optional[HexBytes] = Field(default=None, nullable=True) # --- Relationships --- # diff --git a/src/node/api/http.py b/src/node/api/http.py index cabd3c9..1c1ab3d 100644 --- a/src/node/api/http.py +++ b/src/node/api/http.py @@ -113,6 +113,8 @@ class HttpNodeApi(NodeApi): try: event = json.loads(line) block = BlockSerializer.model_validate(event["block"]) + block.lib = event.get("lib") + block.tip = event.get("tip") except (ValidationError, KeyError, json.JSONDecodeError) as error: logger.exception(error) continue diff --git a/src/node/api/serializers/block.py b/src/node/api/serializers/block.py index d4ecfc9..f3a36fb 100644 --- a/src/node/api/serializers/block.py +++ b/src/node/api/serializers/block.py @@ -1,7 +1,7 @@ import logging from os import getenv from random import randint -from typing import List, Self +from typing import List, Optional, Self from rusty_results import Empty, Option @@ -28,6 +28,8 @@ logger = logging.getLogger(__name__) class BlockSerializer(NbeSerializer, FromRandom): header: HeaderSerializer transactions: List[SignedTransactionSerializer] + lib: Optional[str] = None + tip: Optional[str] = None @classmethod def model_validate_json(cls, *args, **kwargs) -> Self: @@ -46,6 +48,8 @@ class BlockSerializer(NbeSerializer, FromRandom): "slot": self.header.slot, "block_root": self.header.block_root, "proof_of_leadership": self.header.proof_of_leadership.into_proof_of_leadership(), + "lib": bytes.fromhex(self.lib) if self.lib else None, + "tip": bytes.fromhex(self.tip) if self.tip else None, } ).with_transactions(transactions) diff --git a/static/components/BlocksTable.js b/static/components/BlocksTable.js index bed272e..c01716b 100644 --- a/static/components/BlocksTable.js +++ b/static/components/BlocksTable.js @@ -21,6 +21,8 @@ const normalize = (raw) => { hash: raw.hash ?? header?.hash ?? '', parent: raw.parent_block_hash ?? header?.parent_block ?? raw.parent_block ?? '', root: raw.block_root ?? header?.block_root ?? '', + lib: raw.lib ?? '', + tip: raw.tip ?? '', transactionCount: txLen, }; }; @@ -179,6 +181,10 @@ export default function BlocksTable({ live, onDisableLive }) { h('td', null, h('span', { class: 'mono', title: b.root }, shortenHex(b.root))), // Transactions h('td', null, h('span', { class: 'mono' }, String(b.transactionCount))), + // LIB + h('td', null, h('span', { class: 'mono', title: b.lib }, b.lib ? shortenHex(b.lib) : '—')), + // Tip + h('td', null, h('span', { class: 'mono', title: b.tip }, b.tip ? shortenHex(b.tip) : '—')), ); }; @@ -192,6 +198,8 @@ export default function BlocksTable({ live, onDisableLive }) { h('td', null, '\u00A0'), h('td', null, '\u00A0'), h('td', null, '\u00A0'), + h('td', null, '\u00A0'), + h('td', null, '\u00A0'), ); }; @@ -232,6 +240,8 @@ export default function BlocksTable({ live, onDisableLive }) { h('col', { style: 'width:200px' }), // Parent h('col', { style: 'width:200px' }), // Block Root h('col', { style: 'width:100px' }), // Transactions + h('col', { style: 'width:160px' }), // LIB + h('col', { style: 'width:160px' }), // Tip ), h( 'thead', @@ -245,6 +255,8 @@ export default function BlocksTable({ live, onDisableLive }) { h('th', null, 'Parent'), h('th', null, 'Block Root'), h('th', null, 'Transactions'), + h('th', null, 'LIB'), + h('th', null, 'Tip'), ), ), h('tbody', null, ...rows), diff --git a/static/pages/BlockDetail.js b/static/pages/BlockDetail.js index c7d28f9..9b8fd8d 100644 --- a/static/pages/BlockDetail.js +++ b/static/pages/BlockDetail.js @@ -138,6 +138,8 @@ export default function BlockDetailPage({ parameters }) { const blockRoot = block?.block_root ?? header?.block_root ?? ''; const currentBlockHash = block?.hash ?? header?.hash ?? ''; const parentHash = block?.parent_block_hash ?? header?.parent_block ?? ''; + const lib = block?.lib ?? ''; + const tip = block?.tip ?? ''; return h( 'main', @@ -255,6 +257,60 @@ export default function BlockDetailPage({ parameters }) { ), h(CopyPill, { text: parentHash }), ), + + // LIB + h('div', null, h('b', null, 'LIB:')), + h( + 'div', + { style: 'display:flex; gap:8px; flex-wrap:wrap; align-items:flex-start;' }, + lib + ? h( + 'a', + { + class: 'pill mono linkish', + href: PAGE.BLOCK_DETAIL(lib), + title: String(lib), + style: 'max-width:100%; overflow-wrap:anywhere; word-break:break-word;', + }, + shortenHex(lib), + ) + : h( + 'span', + { + class: 'pill mono', + style: 'max-width:100%; overflow-wrap:anywhere; word-break:break-word;', + }, + '—', + ), + lib && h(CopyPill, { text: lib }), + ), + + // Tip + h('div', null, h('b', null, 'Tip:')), + h( + 'div', + { style: 'display:flex; gap:8px; flex-wrap:wrap; align-items:flex-start;' }, + tip + ? h( + 'a', + { + class: 'pill mono linkish', + href: PAGE.BLOCK_DETAIL(tip), + title: String(tip), + style: 'max-width:100%; overflow-wrap:anywhere; word-break:break-word;', + }, + shortenHex(tip), + ) + : h( + 'span', + { + class: 'pill mono', + style: 'max-width:100%; overflow-wrap:anywhere; word-break:break-word;', + }, + '—', + ), + tip && h(CopyPill, { text: tip }), + ), ), ),