Clawdbot commited on
Commit
a6973d6
·
1 Parent(s): 6f5405c

Add L1 Hub (FastAPI + WebSocket backend)

Browse files
Files changed (3) hide show
  1. hub/main.py +98 -0
  2. hub/models.py +21 -0
  3. hub/requirements.txt +3 -0
hub/main.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
2
+ from typing import Dict, List
3
+ import uuid
4
+ import json
5
+
6
+ from models import NodeRegistration, TaskCreate, TaskResult, NodeBalance
7
+
8
+ app = FastAPI(title="Chronos Protocol L1 Hub", description="The Time Exchange Clearinghouse", version="0.1.0")
9
+
10
+ # In-memory storage for MVP (Use Postgres/Redis in prod)
11
+ ledger: Dict[str, float] = {} # node_id -> balance
12
+ active_tasks: Dict[str, dict] = {} # task_id -> task_details
13
+ completed_tasks: Dict[str, dict] = {} # task_id -> result
14
+ connected_nodes: Dict[str, WebSocket] = {} # node_id -> websocket
15
+
16
+ @app.post("/register")
17
+ async def register_node(node: NodeRegistration):
18
+ """Register a new node and initialize its SECONDS balance."""
19
+ if node.pubkey not in ledger:
20
+ # Give a 10 SECOND starter bonus for the MVP
21
+ ledger[node.pubkey] = 10.0
22
+ return {"status": "success", "node_id": node.pubkey, "balance": ledger[node.pubkey]}
23
+
24
+ @app.get("/balance/{node_id}")
25
+ async def get_balance(node_id: str):
26
+ """Check a node's SECONDS balance."""
27
+ if node_id not in ledger:
28
+ raise HTTPException(status_code=404, detail="Node not found")
29
+ return {"node_id": node_id, "balance_seconds": ledger[node_id]}
30
+
31
+ @app.post("/tasks/submit")
32
+ async def submit_task(task: TaskCreate):
33
+ """Consumer submits a task, locking their SECONDS."""
34
+ if task.consumer_id not in ledger:
35
+ raise HTTPException(status_code=404, detail="Consumer node not found")
36
+ if ledger[task.consumer_id] < task.bounty:
37
+ raise HTTPException(status_code=400, detail="Insufficient SECONDS balance")
38
+
39
+ # Deduct bounty
40
+ ledger[task.consumer_id] -= task.bounty
41
+
42
+ task_id = str(uuid.uuid4())
43
+ task_data = {
44
+ "id": task_id,
45
+ "consumer_id": task.consumer_id,
46
+ "payload": task.payload,
47
+ "bounty": task.bounty,
48
+ "status": "pending"
49
+ }
50
+ active_tasks[task_id] = task_data
51
+
52
+ # Broadcast to connected sleeping nodes (Providers)
53
+ for node_id, ws in connected_nodes.items():
54
+ if node_id != task.consumer_id:
55
+ try:
56
+ await ws.send_json({"event": "new_task", "data": task_data})
57
+ except:
58
+ pass # Disconnected
59
+
60
+ return {"status": "success", "task_id": task_id}
61
+
62
+ @app.post("/tasks/complete")
63
+ async def complete_task(result: TaskResult):
64
+ """Provider submits the result and claims the bounty."""
65
+ task = active_tasks.get(result.task_id)
66
+ if not task:
67
+ raise HTTPException(status_code=404, detail="Task not found or already claimed")
68
+
69
+ if result.provider_id not in ledger:
70
+ ledger[result.provider_id] = 0.0
71
+
72
+ # Transfer SECONDS to provider
73
+ ledger[result.provider_id] += task["bounty"]
74
+
75
+ # Move task to completed
76
+ task["status"] = "completed"
77
+ task["provider_id"] = result.provider_id
78
+ task["result"] = result.result_payload
79
+ completed_tasks[result.task_id] = task
80
+ del active_tasks[result.task_id]
81
+
82
+ return {"status": "success", "earned": task["bounty"], "new_balance": ledger[result.provider_id]}
83
+
84
+ @app.websocket("/ws/{node_id}")
85
+ async def websocket_endpoint(websocket: WebSocket, node_id: str):
86
+ """WebSocket connection for real-time task broadcasting (Sleeping APIs listen here)."""
87
+ await websocket.accept()
88
+ connected_nodes[node_id] = websocket
89
+ try:
90
+ while True:
91
+ # Ping/Pong to keep alive
92
+ data = await websocket.receive_text()
93
+ except WebSocketDisconnect:
94
+ del connected_nodes[node_id]
95
+
96
+ if __name__ == "__main__":
97
+ import uvicorn
98
+ uvicorn.run(app, host="0.0.0.0", port=8000)
hub/models.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, Dict
3
+ from datetime import datetime
4
+
5
+ class NodeRegistration(BaseModel):
6
+ pubkey: str = Field(..., description="Node's public key or UUID")
7
+ alias: Optional[str] = None
8
+
9
+ class TaskCreate(BaseModel):
10
+ consumer_id: str
11
+ payload: str
12
+ bounty: float
13
+
14
+ class TaskResult(BaseModel):
15
+ task_id: str
16
+ provider_id: str
17
+ result_payload: str
18
+
19
+ class NodeBalance(BaseModel):
20
+ node_id: str
21
+ balance_seconds: float
hub/requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ pydantic