diff --git a/src/api/v1/router.py b/src/api/v1/router.py index 3c582c7..a08f771 100644 --- a/src/api/v1/router.py +++ b/src/api/v1/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from . import blocks, fork_choice, health, index, transactions +from . import blocks, fork_choice, health, index, search, transactions def create_v1_router() -> APIRouter: @@ -14,10 +14,16 @@ def create_v1_router() -> APIRouter: 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("/search/{hash:str}", search.search, methods=["GET"]) + router.add_api_route("/transactions/stream", transactions.stream, methods=["GET"]) - router.add_api_route("/transactions/list", transactions.list_transactions, methods=["GET"]) + router.add_api_route( + "/transactions/list", transactions.list_transactions, methods=["GET"] + ) router.add_api_route("/transactions/search", transactions.search, methods=["GET"]) - router.add_api_route("/transactions/{transaction_hash:str}", transactions.get, methods=["GET"]) + router.add_api_route( + "/transactions/{transaction_hash:str}", transactions.get, methods=["GET"] + ) router.add_api_route("/fork-choice", fork_choice.get, methods=["GET"]) diff --git a/src/api/v1/search.py b/src/api/v1/search.py new file mode 100644 index 0000000..d774721 --- /dev/null +++ b/src/api/v1/search.py @@ -0,0 +1,39 @@ +from http.client import BAD_REQUEST, NOT_FOUND +from typing import TYPE_CHECKING + +from fastapi import Path +from starlette.responses import JSONResponse, Response + +from core.api import NBERequest +from core.types import dehexify + +if TYPE_CHECKING: + from core.app import NBE + + +async def search(request: NBERequest, hash: str = Path(...)) -> Response: + """ + Search for a block or transaction by hash. + + Returns: + - 200 with {"type": "block"|"transaction", "id": int} if found + - 404 if not found + - 400 if hash is invalid + """ + if not hash: + return Response(status_code=BAD_REQUEST) + + try: + if hash.startswith("0x"): + hash = hash[2:] + normalized_hash = dehexify(hash) + except ValueError: + return Response(status_code=BAD_REQUEST) + + result = await request.app.state.search_repository.search_by_hash(normalized_hash) + + if result is None: + return Response(status_code=NOT_FOUND) + + result_type, result_id = result + return JSONResponse({"type": result_type, "id": result_id}) diff --git a/tests/test_search_api.py b/tests/test_search_api.py new file mode 100644 index 0000000..1bda091 --- /dev/null +++ b/tests/test_search_api.py @@ -0,0 +1,28 @@ +"""Tests for Search API endpoint.""" + +import pytest +from fastapi.testclient import TestClient +from src.app import create_app + +app = create_app("") +client = TestClient(app) + + +@pytest.mark.asyncio +async def test_search_block_hash(): + response = client.get("/api/v1/search/0x" + "00" * 32) + # Should return 404 for non-existent block + assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_search_transaction_hash(): + response = client.get("/api/v1/search/0x" + "11" * 32) + # Should return 404 for non-existent transaction + assert response.status_code == 404 + + +@pytest.mark.asyncio +async def test_search_invalid_hash(): + response = client.get("/api/v1/search/invalid") + assert response.status_code == 400