Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 30 additions & 126 deletions apps/api/src/hackproof/routes.py
Original file line number Diff line number Diff line change
@@ -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}
21 changes: 21 additions & 0 deletions apps/api/src/hackproof/tests/test_errors.yaml
Original file line number Diff line number Diff line change
@@ -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}"
12 changes: 12 additions & 0 deletions apps/web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Add to existing imports
import ErrorsPage from "./pages/errors";

// Add to route configuration
{
path: "/errors",
element: <ErrorsPage />,
},
{
path: "/errors/:code",
element: <ErrorDetailPage />, // Implement detail page as needed
},
62 changes: 62 additions & 0 deletions apps/web/src/pages/errors.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Soroban Error Taxonomy</h1>
<Table>
<Table.Header>
<Table.Row>
<Table.Head>Code</Table.Head>
<Table.Head>Category</Table.Head>
<Table.Head>Plain English</Table.Head>
<Table.Head>Common Cause</Table.Head>
<Table.Head>Fix</Table.Head>
<Table.Head>Docs</Table.Head>
<Table.Head>Occurrences</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{errors.map((error: ErrorEntry) => (
<Table.Row key={error.code}>
<Table.Cell>
<Link href={`/errors/${error.code}`} className="font-mono">
{error.code}
</Link>
</Table.Cell>
<Table.Cell>{error.category}</Table.Cell>
<Table.Cell>{error.plain_english}</Table.Cell>
<Table.Cell>{error.common_cause}</Table.Cell>
<Table.Cell>{error.fix}</Table.Cell>
<Table.Cell>
<Link href={error.docs_url} target="_blank" rel="noopener">
Docs →
</Link>
</Table.Cell>
<Table.Cell>{error.count}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</div>
);
}
28 changes: 28 additions & 0 deletions config/soroban-errors.yaml
Original file line number Diff line number Diff line change
@@ -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