Clawdbot commited on
Commit ·
a6973d6
1
Parent(s): 6f5405c
Add L1 Hub (FastAPI + WebSocket backend)
Browse files- hub/main.py +98 -0
- hub/models.py +21 -0
- 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
|