- Create app factory with routers and templates - Auth, assets, activities, anchors, storage, users, renderers routers - Federation router for WebFinger and nodeinfo - Jinja2 templates for L2 pages - Config and dependency injection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
204 lines
5.3 KiB
Python
204 lines
5.3 KiB
Python
"""
|
|
Anchor routes for L2 server.
|
|
|
|
Handles OpenTimestamps anchoring and verification.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Request, Depends, HTTPException
|
|
from fastapi.responses import HTMLResponse, FileResponse
|
|
|
|
from artdag_common import render
|
|
from artdag_common.middleware import wants_html, wants_json
|
|
|
|
from ..config import settings
|
|
from ..dependencies import get_templates, require_auth, get_user_from_cookie
|
|
|
|
router = APIRouter()
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@router.get("")
|
|
async def list_anchors(
|
|
request: Request,
|
|
offset: int = 0,
|
|
limit: int = 20,
|
|
):
|
|
"""List user's anchors."""
|
|
import db
|
|
|
|
username = get_user_from_cookie(request)
|
|
if not username:
|
|
if wants_json(request):
|
|
raise HTTPException(401, "Authentication required")
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse(url="/login", status_code=302)
|
|
|
|
anchors = await db.get_user_anchors(username, offset=offset, limit=limit)
|
|
has_more = len(anchors) >= limit
|
|
|
|
if wants_json(request):
|
|
return {"anchors": anchors, "offset": offset, "limit": limit}
|
|
|
|
templates = get_templates(request)
|
|
return render(templates, "anchors/list.html", request,
|
|
anchors=anchors,
|
|
user={"username": username},
|
|
offset=offset,
|
|
limit=limit,
|
|
has_more=has_more,
|
|
active_tab="anchors",
|
|
)
|
|
|
|
|
|
@router.post("")
|
|
async def create_anchor(
|
|
request: Request,
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
"""Create a new timestamp anchor."""
|
|
import db
|
|
import anchoring
|
|
|
|
body = await request.json()
|
|
content_hash = body.get("content_hash")
|
|
|
|
if not content_hash:
|
|
raise HTTPException(400, "content_hash required")
|
|
|
|
# Create OTS timestamp
|
|
try:
|
|
ots_data = await anchoring.create_timestamp(content_hash)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create timestamp: {e}")
|
|
raise HTTPException(500, f"Timestamping failed: {e}")
|
|
|
|
# Save anchor
|
|
anchor_id = await db.create_anchor(
|
|
username=user["username"],
|
|
content_hash=content_hash,
|
|
ots_data=ots_data,
|
|
)
|
|
|
|
return {
|
|
"anchor_id": anchor_id,
|
|
"content_hash": content_hash,
|
|
"status": "pending",
|
|
"message": "Anchor created, pending Bitcoin confirmation",
|
|
}
|
|
|
|
|
|
@router.get("/{anchor_id}")
|
|
async def get_anchor(
|
|
anchor_id: str,
|
|
request: Request,
|
|
):
|
|
"""Get anchor details."""
|
|
import db
|
|
|
|
anchor = await db.get_anchor(anchor_id)
|
|
if not anchor:
|
|
raise HTTPException(404, "Anchor not found")
|
|
|
|
if wants_json(request):
|
|
return anchor
|
|
|
|
username = get_user_from_cookie(request)
|
|
templates = get_templates(request)
|
|
return render(templates, "anchors/detail.html", request,
|
|
anchor=anchor,
|
|
user={"username": username} if username else None,
|
|
active_tab="anchors",
|
|
)
|
|
|
|
|
|
@router.get("/{anchor_id}/ots")
|
|
async def download_ots(anchor_id: str):
|
|
"""Download OTS proof file."""
|
|
import db
|
|
|
|
anchor = await db.get_anchor(anchor_id)
|
|
if not anchor:
|
|
raise HTTPException(404, "Anchor not found")
|
|
|
|
ots_data = anchor.get("ots_data")
|
|
if not ots_data:
|
|
raise HTTPException(404, "OTS data not available")
|
|
|
|
# Return as file download
|
|
from fastapi.responses import Response
|
|
return Response(
|
|
content=ots_data,
|
|
media_type="application/octet-stream",
|
|
headers={
|
|
"Content-Disposition": f"attachment; filename={anchor['content_hash']}.ots"
|
|
},
|
|
)
|
|
|
|
|
|
@router.post("/{anchor_id}/verify")
|
|
async def verify_anchor(
|
|
anchor_id: str,
|
|
request: Request,
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
"""Verify anchor status (check Bitcoin confirmation)."""
|
|
import db
|
|
import anchoring
|
|
|
|
anchor = await db.get_anchor(anchor_id)
|
|
if not anchor:
|
|
raise HTTPException(404, "Anchor not found")
|
|
|
|
try:
|
|
result = await anchoring.verify_timestamp(
|
|
anchor["content_hash"],
|
|
anchor["ots_data"],
|
|
)
|
|
|
|
# Update anchor status
|
|
if result.get("confirmed"):
|
|
await db.update_anchor(
|
|
anchor_id,
|
|
status="confirmed",
|
|
bitcoin_block=result.get("block_height"),
|
|
confirmed_at=result.get("confirmed_at"),
|
|
)
|
|
|
|
if wants_html(request):
|
|
if result.get("confirmed"):
|
|
return HTMLResponse(
|
|
f'<span class="text-green-400">Confirmed in block {result["block_height"]}</span>'
|
|
)
|
|
return HTMLResponse('<span class="text-yellow-400">Pending confirmation</span>')
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Verification failed: {e}")
|
|
raise HTTPException(500, f"Verification failed: {e}")
|
|
|
|
|
|
@router.delete("/{anchor_id}")
|
|
async def delete_anchor(
|
|
anchor_id: str,
|
|
user: dict = Depends(require_auth),
|
|
):
|
|
"""Delete an anchor."""
|
|
import db
|
|
|
|
anchor = await db.get_anchor(anchor_id)
|
|
if not anchor:
|
|
raise HTTPException(404, "Anchor not found")
|
|
|
|
if anchor.get("username") != user["username"]:
|
|
raise HTTPException(403, "Not authorized")
|
|
|
|
success = await db.delete_anchor(anchor_id)
|
|
if not success:
|
|
raise HTTPException(400, "Failed to delete anchor")
|
|
|
|
return {"deleted": True}
|