Squashed 'l2/' content from commit 79caa24
git-subtree-dir: l2 git-subtree-split: 79caa24e2129bf6e2cee819327d5622425306b67
This commit is contained in:
203
app/routers/anchors.py
Normal file
203
app/routers/anchors.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""
|
||||
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_anchors_paginated(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}
|
||||
Reference in New Issue
Block a user