Skip to content
Merged
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
88 changes: 12 additions & 76 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,7 @@
submit_wallet_transfer,
validate_public_url,
)
from app.ledger_views import (
account_ledger_transactions,
ledger_entry_to_dict,
recent_ledger_entries,
)
from app.ledger_views import ledger_entry_to_dict, recent_ledger_entries
from app.mcp import handle_mcp_request
from app.mcp_work_proof import (
generic_work_proof_guidance_json,
Expand All @@ -74,6 +70,7 @@
positive_ledger_sequence,
proof_hash_from_path,
)
from app.public_routes import register_public_routes
from app.serializers import (
bounty_awards_to_dict,
bounty_list_summary,
Expand Down Expand Up @@ -758,77 +755,16 @@ def hub(request: Request) -> HTMLResponse:
mergework_hub_context(status_data, settings.public_base_url),
)

@app.get("/bounties", response_class=HTMLResponse)
def bounties_page(
request: Request, status: str | None = Query(None), q: str | None = Query(None)
) -> HTMLResponse:
selected_status = status.strip().lower() if status is not None else None
query_text = q.strip() if q is not None else ""
bounties = list_bounties_by_status(status, q)
return templates.TemplateResponse(
request,
"bounties.html",
{
"bounties": bounties,
"summary": bounty_list_summary(bounties),
"selected_status": selected_status,
"query_text": query_text,
},
)

@app.get("/bounties/{bounty_id}", response_class=HTMLResponse)
def bounty_page(request: Request, bounty_id: int) -> HTMLResponse:
return templates.TemplateResponse(
request, "bounty_detail.html", {"bounty": api_bounty(bounty_id)}
)

@app.get("/ledger", response_class=HTMLResponse)
def ledger_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request, "ledger.html", {"entries": api_ledger()})

@app.get("/ledger/{sequence}", response_class=HTMLResponse)
def ledger_entry_page(request: Request, sequence: int) -> HTMLResponse:
return templates.TemplateResponse(
request, "ledger_entry.html", {"entry": api_ledger_entry(sequence)}
)

@app.get("/wallets", response_class=HTMLResponse)
def wallets_page(request: Request) -> HTMLResponse:
with session_scope(db_url) as session:
wallets = session.scalars(
select(Wallet).order_by(Wallet.created_at.desc()).limit(100)
).all()
wallet_rows = [wallet_to_dict(session, wallet) for wallet in wallets]
return templates.TemplateResponse(request, "wallets.html", {"wallets": wallet_rows})

@app.get("/wallets/{address}", response_class=HTMLResponse)
def wallet_page(request: Request, address: str) -> HTMLResponse:
address = normalized_wallet_address(address)
with session_scope(db_url) as session:
wallet = session.get(Wallet, address)
if wallet is None:
raise HTTPException(status_code=404, detail="wallet not found")
wallet_data = wallet_to_dict(session, wallet)
transactions = account_ledger_transactions(session, wallet.address)
return templates.TemplateResponse(
request,
"wallet_detail.html",
{"wallet": wallet_data, "transactions": transactions},
)

@app.get("/transfer", response_class=HTMLResponse)
def transfer_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request, "transfer.html")

@app.get("/proofs/{proof_hash}", response_class=HTMLResponse)
def proof_page(request: Request, proof_hash: str) -> HTMLResponse:
return templates.TemplateResponse(
request, "proof.html", {"proof": api_proof(proof_hash), "proof_hash": proof_hash}
)

@app.get("/docs", response_class=HTMLResponse)
def docs_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request, "docs.html")
register_public_routes(
app,
db_url=db_url,
templates=templates,
list_bounties_by_status=list_bounties_by_status,
api_bounty=api_bounty,
api_ledger=api_ledger,
api_ledger_entry=api_ledger_entry,
api_proof=api_proof,
)

@app.get("/auth/github/login")
def auth_github_login(next_path: str | None = Query(None, alias="next")) -> RedirectResponse:
Expand Down
110 changes: 110 additions & 0 deletions app/public_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from __future__ import annotations

from collections.abc import Callable
from typing import Any

from fastapi import FastAPI, HTTPException, Query, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.accounts import normalized_wallet_address
from app.db import session_scope
from app.ledger_views import account_ledger_transactions
from app.models import Wallet
from app.serializers import bounty_list_summary, wallet_to_dict


def public_bounties_context(
bounties: list[dict[str, Any]], status: str | None, q: str | None
) -> dict[str, Any]:
selected_status = status.strip().lower() if status is not None else None
query_text = q.strip() if q is not None else ""
return {
"bounties": bounties,
"summary": bounty_list_summary(bounties),
"selected_status": selected_status,
"query_text": query_text,
}


def wallets_page_context(session: Session) -> dict[str, Any]:
wallets = session.scalars(select(Wallet).order_by(Wallet.created_at.desc()).limit(100)).all()
return {"wallets": [wallet_to_dict(session, wallet) for wallet in wallets]}


def wallet_page_context(session: Session, address: str) -> dict[str, Any]:
normalized_address = normalized_wallet_address(address)
wallet = session.get(Wallet, normalized_address)
if wallet is None:
raise HTTPException(status_code=404, detail="wallet not found")
return {
"wallet": wallet_to_dict(session, wallet),
"transactions": account_ledger_transactions(session, wallet.address),
}


def register_public_routes(
app: FastAPI,
*,
db_url: str,
templates: Jinja2Templates,
list_bounties_by_status: Callable[[str | None, str | None], list[dict[str, Any]]],
api_bounty: Callable[[int], dict[str, Any]],
api_ledger: Callable[[], list[dict[str, Any]]],
api_ledger_entry: Callable[[int], dict[str, Any]],
api_proof: Callable[[str], dict[str, Any]],
) -> None:
@app.get("/bounties", response_class=HTMLResponse)
def bounties_page(
request: Request, status: str | None = Query(None), q: str | None = Query(None)
) -> HTMLResponse:
bounties = list_bounties_by_status(status, q)
return templates.TemplateResponse(
request,
"bounties.html",
public_bounties_context(bounties, status, q),
)

@app.get("/bounties/{bounty_id}", response_class=HTMLResponse)
def bounty_page(request: Request, bounty_id: int) -> HTMLResponse:
return templates.TemplateResponse(
request, "bounty_detail.html", {"bounty": api_bounty(bounty_id)}
)

@app.get("/ledger", response_class=HTMLResponse)
def ledger_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request, "ledger.html", {"entries": api_ledger()})

@app.get("/ledger/{sequence}", response_class=HTMLResponse)
def ledger_entry_page(request: Request, sequence: int) -> HTMLResponse:
return templates.TemplateResponse(
request, "ledger_entry.html", {"entry": api_ledger_entry(sequence)}
)

@app.get("/wallets", response_class=HTMLResponse)
def wallets_page(request: Request) -> HTMLResponse:
with session_scope(db_url) as session:
context = wallets_page_context(session)
return templates.TemplateResponse(request, "wallets.html", context)

@app.get("/wallets/{address}", response_class=HTMLResponse)
def wallet_page(request: Request, address: str) -> HTMLResponse:
with session_scope(db_url) as session:
context = wallet_page_context(session, address)
return templates.TemplateResponse(request, "wallet_detail.html", context)

@app.get("/transfer", response_class=HTMLResponse)
def transfer_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request, "transfer.html")

@app.get("/proofs/{proof_hash}", response_class=HTMLResponse)
def proof_page(request: Request, proof_hash: str) -> HTMLResponse:
return templates.TemplateResponse(
request, "proof.html", {"proof": api_proof(proof_hash), "proof_hash": proof_hash}
)

@app.get("/docs", response_class=HTMLResponse)
def docs_page(request: Request) -> HTMLResponse:
return templates.TemplateResponse(request, "docs.html")
27 changes: 27 additions & 0 deletions tests/test_public_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from __future__ import annotations

from app.public_routes import public_bounties_context


def test_public_bounties_context_normalizes_filter_state() -> None:
bounties = [
{
"id": 1,
"status": "open",
"awards_remaining": 2,
"reward_mrwk": "25",
}
]

context = public_bounties_context(bounties, status=" OPEN ", q=" proof ")

assert context == {
"bounties": bounties,
"summary": {
"bounties_shown": 1,
"open_awards": 2,
"open_pool_mrwk": "50",
},
"selected_status": "open",
"query_text": "proof",
}
Loading