waclaw-claw 2d95bd3baa feat: add transaction search functionality
- Add search endpoint to backend (GET /api/v1/transactions/search)
- Support search by transaction hash (partial match) or block height
- Add search bar UI to TransactionsTable component
- Increase default page size from 10 to 50 transactions
- Add Block Height and Block Slot columns to transaction table
- Debounce search input (300ms) for better UX

Fixes:
- Fix health endpoint JSON serialization
- Fix main.py import path
2026-03-28 03:16:02 -04:00

107 lines
4.3 KiB
Python

from http.client import NOT_FOUND
from typing import TYPE_CHECKING, AsyncIterator, List
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:
from core.app import NBE
async def _get_transactions_stream_serialized(
app: "NBE", transaction_from: Option[Transaction], *, fork: int
) -> AsyncIterator[List[TransactionRead]]:
_stream = app.state.transaction_repository.updates_stream(transaction_from, fork=fork)
async for transactions in _stream:
yield [TransactionRead.from_transaction(transaction) for transaction in transactions]
async def stream(
request: NBERequest,
prefetch_limit: int = Query(0, alias="prefetch-limit", ge=0),
fork: int = Query(...),
) -> Response:
latest_transactions: List[Transaction] = await request.app.state.transaction_repository.get_latest(
prefetch_limit, fork=fork, ascending=True, preload_relationships=True
)
latest_transaction = Some(latest_transactions[-1]) if latest_transactions else Empty()
bootstrap_transactions = [TransactionRead.from_transaction(transaction) for transaction in latest_transactions]
transactions_stream: AsyncIterator[List[TransactionRead]] = _get_transactions_stream_serialized(
request.app, latest_transaction, fork=fork
)
ndjson_transactions_stream = into_ndjson_stream(transactions_stream, bootstrap_data=bootstrap_transactions)
return NDJsonStreamingResponse(ndjson_transactions_stream)
async def list_transactions(
request: NBERequest,
page: int = Query(0, ge=0),
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)
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,
}
)
async def get(request: NBERequest, transaction_hash: str, fork: int = Query(...)) -> 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, fork=fork)
return transaction.map(
lambda _transaction: JSONResponse(TransactionRead.from_transaction(_transaction).model_dump(mode="json"))
).unwrap_or_else(lambda: Response(status_code=NOT_FOUND))
async def search(
request: NBERequest,
q: str = Query(..., description="Search query (hash partial match or block height)"),
page: int = Query(0, ge=0, description="Page number"),
page_size: int = Query(50, ge=1, le=100, description="Items per page"),
fork: int = Query(..., description="Fork ID"),
) -> Response:
"""Search transactions by hash or block height."""
if not q:
return JSONResponse({"transactions": [], "page": page, "page_size": page_size, "total_count": 0, "total_pages": 0})
# Try to parse as block height (integer)
try:
block_height = int(q)
transactions, total_count = await request.app.state.transaction_repository.search_by_block_height(
block_height, fork=fork, page=page, page_size=page_size
)
except ValueError:
# Search by hash (partial match)
transactions, total_count = await request.app.state.transaction_repository.search(
q, fork=fork, page=page, page_size=page_size
)
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 0
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,
"query": q,
})