@@ -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
+
+
+
+
+ | Merkle Root |
+ Activities |
+ Status |
+ Created |
+ Actions |
+
+
+
+ {rows}
+
+
+
+
+
+
How it works:
+
+ - Activities are batched and hashed into a merkle tree
+ - The merkle root is submitted to OpenTimestamps
+ - OTS aggregates hashes and anchors to Bitcoin (~1-2 hours)
+ - Once confirmed, anyone can verify the timestamp
+
+
+ '''
+
+ 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)