diff --git a/server.py b/server.py index b802483..faecacc 100644 --- a/server.py +++ b/server.py @@ -281,6 +281,7 @@ def base_html(title: str, content: str, username: str = None) -> str: Assets Activities Users + Anchors
@@ -2331,7 +2332,7 @@ async def get_object(content_hash: str, request: Request): # ============ Anchoring (Bitcoin timestamps) ============ @app.post("/anchors/create") -async def create_anchor_endpoint(user: User = Depends(get_required_user)): +async def create_anchor_endpoint(request: Request): """ Create a new anchor for all unanchored activities. @@ -2341,9 +2342,26 @@ async def create_anchor_endpoint(user: User = Depends(get_required_user)): import anchoring import ipfs_client + # Check auth (cookie or header) + username = get_user_from_cookie(request) + if not username: + if wants_html(request): + return HTMLResponse(''' +
+ Error: Login required +
+ ''') + raise HTTPException(401, "Authentication required") + # Get unanchored activities unanchored = await db.get_unanchored_activities() if not unanchored: + if wants_html(request): + return HTMLResponse(''' +
+ No unanchored activities to anchor. +
+ ''') return {"message": "No unanchored activities", "anchored": 0} activity_ids = [a["activity_id"] for a in unanchored] @@ -2352,6 +2370,14 @@ async def create_anchor_endpoint(user: User = Depends(get_required_user)): anchor = await anchoring.create_anchor(activity_ids, db, ipfs_client) if anchor: + if wants_html(request): + return HTMLResponse(f''' +
+ Success! Anchored {len(activity_ids)} activities.
+ Merkle root: {anchor["merkle_root"][:32]}...
+ Refresh page to see the new anchor. +
+ ''') return { "message": f"Anchored {len(activity_ids)} activities", "merkle_root": anchor["merkle_root"], @@ -2359,6 +2385,12 @@ async def create_anchor_endpoint(user: User = Depends(get_required_user)): "activity_count": anchor["activity_count"] } else: + if wants_html(request): + return HTMLResponse(''' +
+ Failed! Could not create anchor. +
+ ''') raise HTTPException(500, "Failed to create anchor") @@ -2462,6 +2494,208 @@ async def verify_activity_anchor(activity_id: str): } +@app.post("/anchors/{merkle_root}/upgrade") +async def upgrade_anchor_proof(merkle_root: str): + """ + Try to upgrade an OTS proof from pending to confirmed. + + Bitcoin confirmation typically takes 1-2 hours. Call this periodically + to check if the proof has been included in a Bitcoin block. + """ + import anchoring + import ipfs_client + import asyncio + + anchor = await db.get_anchor(merkle_root) + if not anchor: + raise HTTPException(404, f"Anchor not found: {merkle_root}") + + if anchor.get("confirmed_at"): + return {"status": "already_confirmed", "bitcoin_txid": anchor.get("bitcoin_txid")} + + # Get current OTS proof from IPFS + ots_cid = anchor.get("ots_proof_cid") + if not ots_cid: + return {"status": "no_proof", "message": "No OTS proof stored"} + + try: + ots_proof = await asyncio.to_thread(ipfs_client.get_bytes, ots_cid) + if not ots_proof: + return {"status": "error", "message": "Could not fetch OTS proof from IPFS"} + except Exception as e: + return {"status": "error", "message": f"IPFS error: {e}"} + + # Try to upgrade + upgraded = await asyncio.to_thread(anchoring.upgrade_ots_proof, ots_proof) + + if upgraded and len(upgraded) > len(ots_proof): + # Store upgraded proof on IPFS + try: + new_cid = await asyncio.to_thread(ipfs_client.add_bytes, upgraded) + # TODO: Update anchor record with new CID and confirmed status + return { + "status": "upgraded", + "message": "Proof upgraded - Bitcoin confirmation received", + "new_ots_cid": new_cid, + "proof_size": len(upgraded) + } + except Exception as e: + return {"status": "error", "message": f"Failed to store upgraded proof: {e}"} + else: + return { + "status": "pending", + "message": "Not yet confirmed on Bitcoin. Try again in ~1 hour.", + "proof_size": len(ots_proof) if ots_proof else 0 + } + + +@app.get("/anchors/ui", response_class=HTMLResponse) +async def anchors_ui(request: Request): + """Anchors UI page - view and test OpenTimestamps anchoring.""" + username = get_user_from_cookie(request) + + anchors = await db.get_all_anchors() + stats = await db.get_anchor_stats() + + # Build anchors table rows + rows = "" + for anchor in anchors: + status = "confirmed" if anchor.get("confirmed_at") else "pending" + status_class = "text-green-400" if status == "confirmed" else "text-yellow-400" + merkle_root = anchor.get("merkle_root", "")[:16] + "..." + + rows += f''' + + {merkle_root} + {anchor.get("activity_count", 0)} + {status} + {format_date(anchor.get("created_at"), 16)} + + + + + + ''' + + if not rows: + rows = 'No anchors yet' + + content = f''' + + +

Bitcoin Anchoring via OpenTimestamps

+ +
+
+
{stats.get("total_anchors", 0)}
+
Total Anchors
+
+
+
{stats.get("confirmed_anchors", 0)}
+
Confirmed
+
+
+
{stats.get("pending_anchors", 0)}
+
Pending
+
+
+ +
+

Test Anchoring

+

Create a test anchor for unanchored activities, or test the OTS connection.

+
+ + +
+
+
+ +

Anchors

+
+ + + + + + + + + + + + {rows} + +
Merkle RootActivitiesStatusCreatedActions
+
+ +
+

How it works:

+
    +
  1. Activities are batched and hashed into a merkle tree
  2. +
  3. The merkle root is submitted to OpenTimestamps
  4. +
  5. OTS aggregates hashes and anchors to Bitcoin (~1-2 hours)
  6. +
  7. Once confirmed, anyone can verify the timestamp
  8. +
+
+ ''' + + return HTMLResponse(base_html("Anchors", content, username)) + + +@app.post("/anchors/test-ots", response_class=HTMLResponse) +async def test_ots_connection(): + """Test OpenTimestamps connection by submitting a test hash.""" + import anchoring + import hashlib + import asyncio + + # Create a test hash + test_data = f"test-{datetime.now(timezone.utc).isoformat()}" + test_hash = hashlib.sha256(test_data.encode()).hexdigest() + + # Try to submit + try: + ots_proof = await asyncio.to_thread(anchoring.submit_to_opentimestamps, test_hash) + if ots_proof: + return HTMLResponse(f''' +
+ Success! OpenTimestamps is working.
+ Test hash: {test_hash[:32]}...
+ Proof size: {len(ots_proof)} bytes +
+ ''') + else: + return HTMLResponse(''' +
+ Failed! Could not reach OpenTimestamps servers. +
+ ''') + except Exception as e: + return HTMLResponse(f''' +
+ Error: {str(e)} +
+ ''') + + if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8200) + uvicorn.run(app, host="0.0.0.0", port=8200, workers=4)