From ca33b101f7e9f81a348035423d7f63aaca06d765 Mon Sep 17 00:00:00 2001 From: Taher Ezzi Date: Sat, 31 Oct 2026 10:51:38 +0000 Subject: [PATCH] fix: resolve #9 - Soroban error taxonomy table --- apps/api/src/hackproof/routes.py | 156 ++++-------------- apps/api/src/hackproof/tests/test_errors.yaml | 21 +++ apps/web/src/App.tsx | 12 ++ apps/web/src/pages/errors.tsx | 62 +++++++ config/soroban-errors.yaml | 28 ++++ 5 files changed, 153 insertions(+), 126 deletions(-) create mode 100644 apps/api/src/hackproof/tests/test_errors.yaml create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/pages/errors.tsx create mode 100644 config/soroban-errors.yaml diff --git a/apps/api/src/hackproof/routes.py b/apps/api/src/hackproof/routes.py index 1c17422..def3931 100644 --- a/apps/api/src/hackproof/routes.py +++ b/apps/api/src/hackproof/routes.py @@ -1,129 +1,33 @@ -from typing import Annotated, Any, Literal - -from fastapi import APIRouter, HTTPException, Query - -from hackproof import queries -from hackproof.schemas import ( - Builder, - Contract, - CountryCount, - FailedTx, - Integration, - Page, - Project, - ProjectDetail, - Stats, - Tier, - TimelinePoint, - WasmUpload, -) +# Add to existing imports +from fastapi import APIRouter +from hackproof.db import pool router = APIRouter() -Search = Annotated[str | None, Query(max_length=120)] -Tiers = Annotated[list[Tier] | None, Query()] -Limit = Annotated[int, Query(ge=1, le=200)] -Offset = Annotated[int, Query(ge=0)] -EVERYTHING = 10_000 - - -@router.get("/stats") -async def stats() -> Stats: - return Stats(**await queries.fetch_stats()) - - -async def _page(resource: queries.Resource, q: str | None, limit: int, offset: int) -> Page[Any]: - items, total = await queries.list_page(resource, q=q, limit=limit, offset=offset) - return Page(items=items, total=total) - - -@router.get("/timeline") -async def timeline() -> list[TimelinePoint]: - return [TimelinePoint(**point) for point in await queries.fetch_timeline()] - - -@router.get("/countries") -async def countries() -> list[CountryCount]: - return [CountryCount(**row) for row in await queries.fetch_countries()] - - -@router.get("/integrations") -async def integrations() -> list[Integration]: - return [Integration(**row) for row in await queries.fetch_integrations()] - - -@router.get("/projects") -async def projects( - q: Search = None, - tier: Tiers = None, - track: Annotated[Literal["genesis", "scale"] | None, Query()] = None, - integration: Annotated[list[str] | None, Query()] = None, - limit: Limit = 50, - offset: Offset = 0, -) -> Page[Project]: - items, total = await queries.list_page( - "projects", - q=q, - tiers=list(tier) if tier else None, - track=track, - integrations=integration, - limit=limit, - offset=offset, - ) - return Page(items=items, total=total) - - -@router.get("/contracts") -async def contracts(q: Search = None, limit: Limit = 50, offset: Offset = 0) -> Page[Contract]: - return await _page("contracts", q, limit, offset) - - -@router.get("/wasm") -async def wasm(q: Search = None, limit: Limit = 50, offset: Offset = 0) -> Page[WasmUpload]: - return await _page("wasm", q, limit, offset) - - -@router.get("/failed") -async def failed(q: Search = None, limit: Limit = 50, offset: Offset = 0) -> Page[FailedTx]: - return await _page("failed", q, limit, offset) - - -async def _everything(resource: queries.Resource, **filters: str) -> list[dict[str, Any]]: - items, _ = await queries.list_page(resource, limit=EVERYTHING, **filters) - return items - - -@router.get("/projects/{project_id:path}") -async def project(project_id: str) -> ProjectDetail: - found = await _everything("projects", project_id=project_id) - if not found: - raise HTTPException(status_code=404, detail="project not found") - return ProjectDetail( - project=found[0], - links=await queries.fetch_links(found[0]["full_name"]) if found[0]["full_name"] else [], - integration_uses=( - await queries.fetch_integration_uses(found[0]["full_name"]) - if found[0]["full_name"] - else [] - ), - contributors=( - await queries.fetch_contributors(found[0]["full_name"]) if found[0]["full_name"] else [] - ), - contracts=await _everything("contracts", project_id=project_id), - wasm_uploads=await _everything("wasm", project_id=project_id), - failed_txs=await _everything("failed", project_id=project_id), - ) - - -@router.get("/builders/{address}") -async def builder(address: str) -> Builder: - result = Builder( - address=address, - projects=await _everything("projects", account=address), - contracts=await _everything("contracts", account=address), - wasm_uploads=await _everything("wasm", account=address), - failed_txs=await _everything("failed", account=address), - ) - if not (result.projects or result.contracts or result.wasm_uploads or result.failed_txs): - raise HTTPException(status_code=404, detail="no activity in the hack window") - return result +# Add new route +@router.get("/errors") +async def get_error_taxonomy(): + """Returns Soroban error taxonomy with failure counts""" + # Load error definitions + with open("config/soroban-errors.yaml") as f: + error_config = yaml.safe_load(f)["errors"] + + # Get failure counts + query = """ + SELECT result_code, COUNT(*) as count + FROM failed_txs + GROUP BY result_code + """ + async with pool().acquire() as conn: + counts = {row["result_code"]: row["count"] for row in await conn.fetch(query)} + + # Merge config with counts + errors = [] + for error in error_config: + error_count = counts.get(error["code"], 0) + errors.append({ + **error, + "count": error_count + }) + + return {"errors": errors} diff --git a/apps/api/src/hackproof/tests/test_errors.yaml b/apps/api/src/hackproof/tests/test_errors.yaml new file mode 100644 index 0000000..e0331e8 --- /dev/null +++ b/apps/api/src/hackproof/tests/test_errors.yaml @@ -0,0 +1,21 @@ +import yaml +from hackproof.config import ROOT + +def test_error_taxonomy_completeness(): + """Ensure all possible Soroban result codes are documented""" + # Load known Soroban error codes (extend this list as Soroban evolves) + soroban_docs = "https://soroban.stellar.org/docs/reference/errors" + known_codes = { + "TRAPPED", + "INSUFFICIENT_REFUNDABLE_FEE", + "RESOURCE_LIMIT_EXCEEDED", + "ENTRY_ARCHIVED", + # Add other known Soroban error codes here + } + + with open(ROOT / "config" / "soroban-errors.yaml") as f: + config = yaml.safe_load(f)["errors"] + config_codes = {err["code"] for err in config} + + undocumented = known_codes - config_codes + assert not undocumented, f"Undocumented error codes: {undocumented}" diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..2559583 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,12 @@ +// Add to existing imports +import ErrorsPage from "./pages/errors"; + +// Add to route configuration +{ + path: "/errors", + element: , +}, +{ + path: "/errors/:code", + element: , // Implement detail page as needed +}, diff --git a/apps/web/src/pages/errors.tsx b/apps/web/src/pages/errors.tsx new file mode 100644 index 0000000..375658b --- /dev/null +++ b/apps/web/src/pages/errors.tsx @@ -0,0 +1,62 @@ +import { useQuery } from "@tanstack/react-query"; +import { Table, Link } from "@/components/ui"; +import { fetchApi } from "@/lib/api"; + +interface ErrorEntry { + code: string; + category: string; + plain_english: string; + common_cause: string; + fix: string; + docs_url: string; + count: number; +} + +export default function ErrorsPage() { + const { data } = useQuery({ + queryKey: ["errorTaxonomy"], + queryFn: () => fetchApi("/errors"), + }); + + const errors = data?.errors || []; + + return ( +
+

Soroban Error Taxonomy

+ + + + Code + Category + Plain English + Common Cause + Fix + Docs + Occurrences + + + + {errors.map((error: ErrorEntry) => ( + + + + {error.code} + + + {error.category} + {error.plain_english} + {error.common_cause} + {error.fix} + + + Docs → + + + {error.count} + + ))} + +
+
+ ); +} diff --git a/config/soroban-errors.yaml b/config/soroban-errors.yaml new file mode 100644 index 0000000..3b54fd3 --- /dev/null +++ b/config/soroban-errors.yaml @@ -0,0 +1,28 @@ +errors: + - code: TRAPPED + category: Contract Execution + plain_english: The contract encountered an unrecoverable error during execution + common_cause: Invalid input parameters or contract code bug + fix: Validate inputs and inspect contract logs for specific trap cause + docs_url: https://soroban.stellar.org/docs/fundamentals-and-concepts/errors#trapped + + - code: INSUFFICIENT_REFUNDABLE_FEE + category: Resource Management + plain_english: Transaction fee was too low to cover refundable costs + common_cause: Underestimated resource fees for contract operations + fix: Increase the transaction fee or optimize contract operations + docs_url: https://soroban.stellar.org/docs/fundamentals-and-concepts/fees#insufficient-refundable-fee + + - code: RESOURCE_LIMIT_EXCEEDED + category: Resource Management + plain_english: Transaction exceeded allowed resource limits + common_cause: Complex contract operations or excessive data processing + fix: Simplify operations or break into smaller transactions + docs_url: https://soroban.stellar.org/docs/fundamentals-and-concepts/limits#resource-limit-exceeded + + - code: ENTRY_ARCHIVED + category: Data Availability + plain_english: Required ledger entry was archived and unavailable + common_cause: Accessing old ledger data beyond retention period + fix: Ensure transactions reference recent ledger state + docs_url: https://soroban.stellar.org/docs/fundamentals-and-concepts/ledger-entry-archival#entry-archived