diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml
index 7ac0856..d8ac549 100644
--- a/.github/workflows/publish-docker-image.yml
+++ b/.github/workflows/publish-docker-image.yml
@@ -38,7 +38,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
- file: ./Dockerfile
+ file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
diff --git a/src/api/v1/blocks.py b/src/api/v1/blocks.py
index 9138443..ab00e93 100644
--- a/src/api/v1/blocks.py
+++ b/src/api/v1/blocks.py
@@ -24,13 +24,15 @@ async def list_blocks(
blocks, total_count = await request.app.state.block_repository.get_paginated(page, page_size, fork=fork)
total_pages = (total_count + page_size - 1) // page_size # ceiling division
- return JSONResponse({
- "blocks": [BlockRead.from_block(block).model_dump(mode="json") for block in blocks],
- "page": page,
- "page_size": page_size,
- "total_count": total_count,
- "total_pages": total_pages,
- })
+ return JSONResponse(
+ {
+ "blocks": [BlockRead.from_block(block).model_dump(mode="json") for block in blocks],
+ "page": page,
+ "page_size": page_size,
+ "total_count": total_count,
+ "total_pages": total_pages,
+ }
+ )
async def _get_blocks_stream_serialized(
diff --git a/src/api/v1/transactions.py b/src/api/v1/transactions.py
index f3319a3..57729d8 100644
--- a/src/api/v1/transactions.py
+++ b/src/api/v1/transactions.py
@@ -47,18 +47,18 @@ async def list_transactions(
page_size: int = Query(10, ge=1, le=100, alias="page-size"),
fork: int = Query(...),
) -> Response:
- transactions, total_count = await request.app.state.transaction_repository.get_paginated(
- page, page_size, fork=fork
- )
+ transactions, total_count = await request.app.state.transaction_repository.get_paginated(page, page_size, fork=fork)
total_pages = (total_count + page_size - 1) // page_size
- return JSONResponse({
- "transactions": [TransactionRead.from_transaction(tx).model_dump(mode="json") for tx in transactions],
- "page": page,
- "page_size": page_size,
- "total_count": total_count,
- "total_pages": total_pages,
- })
+ return JSONResponse(
+ {
+ "transactions": [TransactionRead.from_transaction(tx).model_dump(mode="json") for tx in transactions],
+ "page": page,
+ "page_size": page_size,
+ "total_count": total_count,
+ "total_pages": total_pages,
+ }
+ )
async def get(request: NBERequest, transaction_hash: str, fork: int = Query(...)) -> Response:
diff --git a/src/db/blocks.py b/src/db/blocks.py
index 7ab98e9..99721a7 100644
--- a/src/db/blocks.py
+++ b/src/db/blocks.py
@@ -3,16 +3,13 @@ from asyncio import sleep
from typing import AsyncIterator, Dict, List
from rusty_results import Empty, Option, Some
-from sqlalchemy import Result, Select
+from sqlalchemy import Result, Select, func as sa_func
from sqlalchemy.orm import aliased
from sqlmodel import select
-from sqlalchemy import func as sa_func
-
from db.clients import DbClient
from models.block import Block
-
logger = logging.getLogger(__name__)
@@ -25,33 +22,18 @@ def chain_block_ids_cte(*, fork: int):
from fork 0 at height 50, the CTE returns fork 1 blocks (50+) AND the
ancestor fork 0 blocks (0–49).
"""
- tip_hash = (
- select(Block.hash)
- .where(Block.fork == fork)
- .order_by(Block.height.desc())
- .limit(1)
- .scalar_subquery()
- )
+ tip_hash = select(Block.hash).where(Block.fork == fork).order_by(Block.height.desc()).limit(1).scalar_subquery()
- base = select(Block.id, Block.hash, Block.parent_block).where(
- Block.hash == tip_hash
- )
+ base = select(Block.id, Block.hash, Block.parent_block).where(Block.hash == tip_hash)
cte = base.cte(name="chain", recursive=True)
- recursive = select(Block.id, Block.hash, Block.parent_block).where(
- Block.hash == cte.c.parent_block
- )
+ recursive = select(Block.id, Block.hash, Block.parent_block).where(Block.hash == cte.c.parent_block)
return cte.union_all(recursive)
def get_latest_statement(limit: int, *, fork: int, output_ascending: bool = True) -> Select:
chain = chain_block_ids_cte(fork=fork)
- base = (
- select(Block)
- .join(chain, Block.id == chain.c.id)
- .order_by(Block.height.desc())
- .limit(limit)
- )
+ base = select(Block).join(chain, Block.id == chain.c.id).order_by(Block.height.desc()).limit(limit)
if not output_ascending:
return base
@@ -191,9 +173,7 @@ class BlockRepository:
parent_hashes_in_batch = {b.parent_block for b in blocks_to_add}
parents_with_children: set[bytes] = set()
if parent_hashes_in_batch:
- stmt = select(Block.parent_block).where(
- Block.parent_block.in_(parent_hashes_in_batch)
- ).distinct()
+ stmt = select(Block.parent_block).where(Block.parent_block.in_(parent_hashes_in_batch)).distinct()
for ph in session.exec(stmt).all():
parents_with_children.add(ph)
@@ -310,9 +290,7 @@ class BlockRepository:
while True:
statement = (
- select(Block)
- .where(Block.fork == fork, Block.height >= height_cursor)
- .order_by(Block.height.asc())
+ select(Block).where(Block.fork == fork, Block.height >= height_cursor).order_by(Block.height.asc())
)
with self.client.session() as session:
diff --git a/src/db/transaction.py b/src/db/transaction.py
index 9aee8f7..bb4e55d 100644
--- a/src/db/transaction.py
+++ b/src/db/transaction.py
@@ -74,7 +74,9 @@ class TransactionRepository:
if limit == 0:
return []
- statement = get_latest_statement(limit, fork=fork, output_ascending=ascending, preload_relationships=preload_relationships)
+ statement = get_latest_statement(
+ limit, fork=fork, output_ascending=ascending, preload_relationships=preload_relationships
+ )
with self.client.session() as session:
results: Result[Transaction] = session.exec(statement)
diff --git a/src/node/api/serializers/signed_transaction.py b/src/node/api/serializers/signed_transaction.py
index 5915c24..8ffe9fd 100644
--- a/src/node/api/serializers/signed_transaction.py
+++ b/src/node/api/serializers/signed_transaction.py
@@ -83,5 +83,9 @@ class SignedTransactionSerializer(NbeSerializer, FromRandom):
n = len(transaction.operations_contents)
operations_proofs = [OperationProofSerializer.from_random() for _ in range(n)]
return cls.model_validate(
- {"mantle_tx": transaction, "ops_proofs": operations_proofs, "ledger_tx_proof": Groth16ProofSerializer.from_random()}
+ {
+ "mantle_tx": transaction,
+ "ops_proofs": operations_proofs,
+ "ledger_tx_proof": Groth16ProofSerializer.from_random(),
+ }
)
diff --git a/src/node/lifespan.py b/src/node/lifespan.py
index d202f38..0ac8ae0 100644
--- a/src/node/lifespan.py
+++ b/src/node/lifespan.py
@@ -155,7 +155,9 @@ async def subscribe_to_new_blocks(app: "NBE"):
# Re-check if parent now exists after backfill
parent_exists = (await app.state.block_repository.get_by_hash(block.parent_block)).is_some
if not parent_exists:
- logger.warning(f"Parent block still not found after backfill for block at slot {block.slot}. Skipping block.")
+ logger.warning(
+ f"Parent block still not found after backfill for block at slot {block.slot}. Skipping block."
+ )
continue
# Capture values before create() detaches the block from the session
diff --git a/static/app.js b/static/app.js
index d8457c9..cd7bf3b 100644
--- a/static/app.js
+++ b/static/app.js
@@ -9,11 +9,11 @@ import TransactionDetailPage from './pages/TransactionDetail.js';
const ROOT = document.getElementById('app');
- // Detect the Base Path from the HTML tag.
- // If the tag is missing or equals "__BASE_PATH__", default to root "/".
+// Detect the Base Path from the HTML tag.
+// If the tag is missing or equals "__BASE_PATH__", default to root "/".
const BASE_PATH = (() => {
const baseHref = document.querySelector('base')?.getAttribute('href');
- if (!baseHref || baseHref === "__BASE_PATH__") return '/';
+ if (!baseHref || baseHref === '__BASE_PATH__') return '/';
return baseHref.endsWith('/') ? baseHref : `${baseHref}/`;
})();
diff --git a/static/components/BlocksTable.js b/static/components/BlocksTable.js
index d9c822c..bed272e 100644
--- a/static/components/BlocksTable.js
+++ b/static/components/BlocksTable.js
@@ -210,7 +210,12 @@ export default function BlocksTable({ live, onDisableLive }) {
h(
'div',
{ class: 'card-header', style: 'display:flex; justify-content:space-between; align-items:center;' },
- h('div', null, h('strong', null, 'Blocks '), !live && totalCount > 0 && h('span', { class: 'pill' }, String(totalCount))),
+ h(
+ 'div',
+ null,
+ h('strong', null, 'Blocks '),
+ !live && totalCount > 0 && h('span', { class: 'pill' }, String(totalCount)),
+ ),
),
h(
'div',
diff --git a/static/components/TransactionsTable.js b/static/components/TransactionsTable.js
index fd2e3fb..8e751a2 100644
--- a/static/components/TransactionsTable.js
+++ b/static/components/TransactionsTable.js
@@ -262,7 +262,12 @@ export default function TransactionsTable({ live, onDisableLive }) {
h(
'div',
{ class: 'card-header', style: 'display:flex; justify-content:space-between; align-items:center;' },
- h('div', null, h('strong', null, 'Transactions '), !live && totalCount > 0 && h('span', { class: 'pill' }, String(totalCount))),
+ h(
+ 'div',
+ null,
+ h('strong', null, 'Transactions '),
+ !live && totalCount > 0 && h('span', { class: 'pill' }, String(totalCount)),
+ ),
),
h(
'div',
@@ -310,7 +315,11 @@ export default function TransactionsTable({ live, onDisableLive }) {
h(
'span',
{ style: 'color:var(--muted); font-size:13px;' },
- live ? 'Streaming live transactions...' : totalPages > 0 ? `Page ${page + 1} of ${totalPages}` : 'No transactions',
+ live
+ ? 'Streaming live transactions...'
+ : totalPages > 0
+ ? `Page ${page + 1} of ${totalPages}`
+ : 'No transactions',
),
h(
'button',
diff --git a/static/lib/api.js b/static/lib/api.js
index c47be9d..effd18b 100644
--- a/static/lib/api.js
+++ b/static/lib/api.js
@@ -15,8 +15,7 @@ const TRANSACTIONS_STREAM = joinUrl(API_PREFIX, 'transactions/stream');
const FORK_CHOICE = joinUrl(API_PREFIX, 'fork-choice');
const BLOCK_DETAIL_BY_HASH = (hash) => joinUrl(API_PREFIX, 'blocks', encodeHash(hash));
-const BLOCKS_STREAM = (fork) =>
- `${joinUrl(API_PREFIX, 'blocks/stream')}?fork=${encodeURIComponent(fork)}`;
+const BLOCKS_STREAM = (fork) => `${joinUrl(API_PREFIX, 'blocks/stream')}?fork=${encodeURIComponent(fork)}`;
const BLOCKS_LIST = (page, pageSize, fork) =>
`${joinUrl(API_PREFIX, 'blocks/list')}?page=${encodeURIComponent(page)}&page-size=${encodeURIComponent(pageSize)}&fork=${encodeURIComponent(fork)}`;
diff --git a/static/pages/Home.js b/static/pages/Home.js
index 26b34eb..3d22f7c 100644
--- a/static/pages/Home.js
+++ b/static/pages/Home.js
@@ -40,7 +40,9 @@ export default function HomeView() {
live ? 'LIVE \u2022' : 'LIVE',
),
),
- h('section', { class: 'two-columns twocol' },
+ h(
+ 'section',
+ { class: 'two-columns twocol' },
h(BlocksTable, { live, onDisableLive: () => setLive(false) }),
h(TransactionsTable, { live, onDisableLive: () => setLive(false) }),
),
diff --git a/static/pages/TransactionDetail.js b/static/pages/TransactionDetail.js
index e80e33f..bcc2b0d 100644
--- a/static/pages/TransactionDetail.js
+++ b/static/pages/TransactionDetail.js
@@ -319,10 +319,7 @@ function tryDecodeUtf8Hex(hex) {
}
/** Human-friendly label for a content field key */
-const fieldLabel = (key) =>
- key
- .replace(/_/g, ' ')
- .replace(/\b\w/g, (c) => c.toUpperCase());
+const fieldLabel = (key) => key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
/** Render the value of a single content field */
function FieldValue({ value }) {
@@ -390,10 +387,16 @@ function OperationProof({ proof }) {
entries.length > 0 &&
h(
'div',
- { style: 'margin-top:4px; display:grid; grid-template-columns:auto 1fr; gap:4px 12px; align-items:baseline;' },
+ {
+ style: 'margin-top:4px; display:grid; grid-template-columns:auto 1fr; gap:4px 12px; align-items:baseline;',
+ },
...entries.flatMap(([key, value]) => [
h('span', { style: 'color:var(--muted); font-size:12px; white-space:nowrap;' }, fieldLabel(key)),
- h('span', { class: 'mono', style: 'font-size:12px; overflow-wrap:anywhere; word-break:break-all;' }, renderBytes(value)),
+ h(
+ 'span',
+ { class: 'mono', style: 'font-size:12px; overflow-wrap:anywhere; word-break:break-all;' },
+ renderBytes(value),
+ ),
]),
),
);
@@ -562,6 +565,13 @@ export default function TransactionDetail({ parameters }) {
!tx && !err && h('p', null, 'Loading…'),
// Success
- tx && h(Fragment, null, h(Summary, { tx }), h(Operations, { operations: tx.operations }), h(Ledger, { ledger: tx.ledger })),
+ tx &&
+ h(
+ Fragment,
+ null,
+ h(Summary, { tx }),
+ h(Operations, { operations: tx.operations }),
+ h(Ledger, { ledger: tx.ledger }),
+ ),
);
}
diff --git a/static/styles.css b/static/styles.css
index 9bbba3c..648a4f7 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -161,6 +161,16 @@ button.pill:disabled {
}
@keyframes live-pulse {
- 0%, 100% { box-shadow: 0 0 4px #ff4444, 0 0 8px #ff4444; }
- 50% { box-shadow: 0 0 8px #ff4444, 0 0 16px #ff4444, 0 0 24px #ff6666; }
+ 0%,
+ 100% {
+ box-shadow:
+ 0 0 4px #ff4444,
+ 0 0 8px #ff4444;
+ }
+ 50% {
+ box-shadow:
+ 0 0 8px #ff4444,
+ 0 0 16px #ff4444,
+ 0 0 24px #ff6666;
+ }
}
diff --git a/tests/test_block_forks.py b/tests/test_block_forks.py
index 65bdd10..3cfc6e8 100644
--- a/tests/test_block_forks.py
+++ b/tests/test_block_forks.py
@@ -259,6 +259,7 @@ def test_batch_with_fork_and_chain(client, repo):
def test_fork_choice_empty_db(client, repo):
"""Fork choice returns Empty when no blocks exist."""
from rusty_results import Empty
+
result = asyncio.run(repo.get_fork_choice())
assert isinstance(result, Empty)