Clawdbot commited on
Commit
4e321e0
Β·
1 Parent(s): 4301a95

Add Three Markets integration test to verify economy logic

Browse files
hub/__pycache__/logger.cpython-310.pyc ADDED
Binary file (2.5 kB). View file
 
hub/__pycache__/main.cpython-310.pyc CHANGED
Binary files a/hub/__pycache__/main.cpython-310.pyc and b/hub/__pycache__/main.cpython-310.pyc differ
 
hub/ledger.db CHANGED
Binary files a/hub/ledger.db and b/hub/ledger.db differ
 
hub/logs/hub.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"timestamp": "2026-02-24T07:15:32.589565+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_7c115a964de4 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_7c115a964de4", "starting_balance": 10.0}
2
+ {"timestamp": "2026-02-24T07:15:32.603735+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_31a01d787f88 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_31a01d787f88", "starting_balance": 10.0}
3
+ {"timestamp": "2026-02-24T07:16:06.691635+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_7c115a964de4 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_7c115a964de4", "starting_balance": 10.0}
4
+ {"timestamp": "2026-02-24T07:16:06.711586+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_31a01d787f88 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_31a01d787f88", "starting_balance": 10.0}
5
+ {"timestamp": "2026-02-24T07:16:23.994625+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_7c115a964de4 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_7c115a964de4", "starting_balance": 10.0}
6
+ {"timestamp": "2026-02-24T07:16:24.007658+00:00", "level": "INFO", "logger": "mep.hub", "message": "Node node_31a01d787f88 registered with starting balance 10.0", "event": "node_registered", "node_id": "node_31a01d787f88", "starting_balance": 10.0}
7
+ {"timestamp": "2026-02-24T07:16:24.573446+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task c9d51463 broadcasted by node_7c115a964de4 for 5.0", "event": "task_submitted", "consumer_id": "node_7c115a964de4", "task_id": "c9d51463-1d89-4aa0-9899-be5090bc4edf", "bounty": 5.0}
8
+ {"timestamp": "2026-02-24T07:16:24.582713+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task c9d51463 assigned to node_31a01d787f88", "event": "bid_accepted", "task_id": "c9d51463-1d89-4aa0-9899-be5090bc4edf", "provider_id": "node_31a01d787f88", "bounty": 5.0}
9
+ {"timestamp": "2026-02-24T07:16:24.598410+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task c9d51463 completed by node_31a01d787f88", "event": "task_completed", "task_id": "c9d51463-1d89-4aa0-9899-be5090bc4edf", "provider_id": "node_31a01d787f88", "bounty": 5.0}
10
+ {"timestamp": "2026-02-24T07:16:24.608789+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task 4f2e4436 broadcasted by node_7c115a964de4 for 0.0", "event": "task_submitted", "consumer_id": "node_7c115a964de4", "task_id": "4f2e4436-abff-4c7e-8777-2b8326c824ed", "bounty": 0.0}
11
+ {"timestamp": "2026-02-24T07:16:24.617370+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task 4f2e4436 completed by node_31a01d787f88", "event": "task_completed", "task_id": "4f2e4436-abff-4c7e-8777-2b8326c824ed", "provider_id": "node_31a01d787f88", "bounty": 0.0}
12
+ {"timestamp": "2026-02-24T07:16:24.625135+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task b9c676c9 broadcasted by node_7c115a964de4 for -2.0", "event": "task_submitted", "consumer_id": "node_7c115a964de4", "task_id": "b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35", "bounty": -2.0}
13
+ {"timestamp": "2026-02-24T07:16:24.632191+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task b9c676c9 assigned to node_31a01d787f88", "event": "bid_accepted", "task_id": "b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35", "provider_id": "node_31a01d787f88", "bounty": -2.0}
14
+ {"timestamp": "2026-02-24T07:16:24.651742+00:00", "level": "INFO", "logger": "mep.hub", "message": "Task b9c676c9 completed by node_31a01d787f88", "event": "task_completed", "task_id": "b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35", "provider_id": "node_31a01d787f88", "bounty": -2.0}
hub/logs/ledger_audit.log ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 2026-02-24 07:15:32,590 - mep.audit - INFO - AUDIT | REGISTER | Node: node_7c115a964de4 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
2
+ 2026-02-24 07:15:32,604 - mep.audit - INFO - AUDIT | REGISTER | Node: node_31a01d787f88 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
3
+ 2026-02-24 07:16:06,692 - mep.audit - INFO - AUDIT | REGISTER | Node: node_7c115a964de4 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
4
+ 2026-02-24 07:16:06,712 - mep.audit - INFO - AUDIT | REGISTER | Node: node_31a01d787f88 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
5
+ 2026-02-24 07:16:23,995 - mep.audit - INFO - AUDIT | REGISTER | Node: node_7c115a964de4 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
6
+ 2026-02-24 07:16:24,008 - mep.audit - INFO - AUDIT | REGISTER | Node: node_31a01d787f88 | Amount: +10.000000 | Balance: 10.000000 | Ref: START_BONUS
7
+ 2026-02-24 07:16:24,573 - mep.audit - INFO - AUDIT | ESCROW | Node: node_7c115a964de4 | Amount: -5.000000 | Balance: 5.000000 | Ref: c9d51463-1d89-4aa0-9899-be5090bc4edf
8
+ 2026-02-24 07:16:24,598 - mep.audit - INFO - AUDIT | EARN_COMPUTE | Node: node_31a01d787f88 | Amount: +5.000000 | Balance: 15.000000 | Ref: c9d51463-1d89-4aa0-9899-be5090bc4edf
9
+ 2026-02-24 07:16:24,617 - mep.audit - INFO - AUDIT | EARN_COMPUTE | Node: node_31a01d787f88 | Amount: +0.000000 | Balance: 15.000000 | Ref: 4f2e4436-abff-4c7e-8777-2b8326c824ed
10
+ 2026-02-24 07:16:24,645 - mep.audit - INFO - AUDIT | BUY_DATA | Node: node_31a01d787f88 | Amount: -2.000000 | Balance: 13.000000 | Ref: b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35
11
+ 2026-02-24 07:16:24,651 - mep.audit - INFO - AUDIT | SELL_DATA | Node: node_7c115a964de4 | Amount: +2.000000 | Balance: 7.000000 | Ref: b9c676c9-cf9f-4ebf-a54e-b96b9c49cd35
hub/main.py CHANGED
@@ -23,14 +23,14 @@ async def verify_request(
23
  ) -> str:
24
  body = await request.body()
25
  payload_str = body.decode('utf-8')
26
-
27
  pub_pem = db.get_pub_pem(x_mep_nodeid)
28
  if not pub_pem:
29
  raise HTTPException(status_code=401, detail="Unknown Node ID. Please register first.")
30
-
31
  if not auth.verify_signature(pub_pem, payload_str, x_mep_timestamp, x_mep_signature):
32
  raise HTTPException(status_code=401, detail="Invalid cryptographic signature.")
33
-
34
  return x_mep_nodeid
35
 
36
  @app.post("/register")
@@ -38,10 +38,10 @@ async def register_node(node: NodeRegistration):
38
  # Registration derives the Node ID from the provided Public Key PEM
39
  node_id = auth.derive_node_id(node.pubkey)
40
  balance = db.register_node(node_id, node.pubkey)
41
-
42
  log_event("node_registered", f"Node {node_id} registered with starting balance {balance}", node_id=node_id, starting_balance=balance)
43
  log_audit("REGISTER", node_id, balance, balance, "START_BONUS")
44
-
45
  return {"status": "success", "node_id": node_id, "balance": balance}
46
 
47
  @app.get("/balance/{node_id}")
@@ -58,15 +58,17 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
58
  # Verify the signer is actually the consumer claiming to submit the task
59
  if authenticated_node != task.consumer_id:
60
  raise HTTPException(status_code=403, detail="Cannot submit tasks on behalf of another node")
61
-
62
  consumer_balance = db.get_balance(task.consumer_id)
63
  if consumer_balance is None:
64
  raise HTTPException(status_code=404, detail="Consumer node not found")
65
-
66
  # If bounty is positive, consumer is PAYING. Check consumer balance.
67
  if task.bounty > 0 and consumer_balance < task.bounty:
68
  raise HTTPException(status_code=400, detail="Insufficient SECONDS balance to pay for task")
69
-
 
 
70
  # Note: If bounty is negative, consumer is SELLING data. We don't deduct here.
71
  # We will deduct from the provider when they complete the task.
72
  if task.bounty > 0:
@@ -79,8 +81,7 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
79
  log_audit("ESCROW", task.consumer_id, -task.bounty, new_balance, task_id)
80
 
81
  log_event("task_submitted", f"Task {task_id[:8]} broadcasted by {task.consumer_id} for {task.bounty}", consumer_id=task.consumer_id, task_id=task_id, bounty=task.bounty)
82
-
83
- task_id = str(uuid.uuid4())
84
  task_data = {
85
  "id": task_id,
86
  "consumer_id": task.consumer_id,
@@ -91,7 +92,7 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
91
  "model_requirement": task.model_requirement
92
  }
93
  active_tasks[task_id] = task_data
94
-
95
  # Target specific node if requested (Direct Message skips bidding)
96
  if task.target_node:
97
  if task.target_node in connected_nodes:
@@ -118,27 +119,27 @@ async def submit_task(task: TaskCreate, authenticated_node: str = Depends(verify
118
  await ws.send_json({"event": "rfc", "data": rfc_data})
119
  except:
120
  pass
121
-
122
  return {"status": "success", "task_id": task_id}
123
 
124
  @app.post("/tasks/bid")
125
  async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_request)):
126
  if authenticated_node != bid.provider_id:
127
  raise HTTPException(status_code=403, detail="Cannot bid on behalf of another node")
128
-
129
  task = active_tasks.get(bid.task_id)
130
  if not task:
131
  raise HTTPException(status_code=404, detail="Task not found or already completed")
132
-
133
  if task["status"] != "bidding":
134
  return {"status": "rejected", "detail": "Task already assigned to another node"}
135
-
136
  # Phase 2 Fast Auction: Accept the first valid bid
137
  task["status"] = "assigned"
138
  task["provider_id"] = bid.provider_id
139
-
140
  log_event("bid_accepted", f"Task {bid.task_id[:8]} assigned to {bid.provider_id}", task_id=bid.task_id, provider_id=bid.provider_id, bounty=task["bounty"])
141
-
142
  # Return the full payload to the winner
143
  return {
144
  "status": "accepted",
@@ -151,11 +152,11 @@ async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_reque
151
  async def complete_task(result: TaskResult, authenticated_node: str = Depends(verify_request)):
152
  if authenticated_node != result.provider_id:
153
  raise HTTPException(status_code=403, detail="Cannot complete tasks on behalf of another node")
154
-
155
  task = active_tasks.get(result.task_id)
156
  if not task:
157
  raise HTTPException(status_code=404, detail="Task not found or already claimed")
158
-
159
  provider_balance = db.get_balance(result.provider_id)
160
  if provider_balance is None:
161
  db.set_balance(result.provider_id, 0.0)
@@ -174,14 +175,14 @@ async def complete_task(result: TaskResult, authenticated_node: str = Depends(ve
174
  if not success:
175
  log_event("data_purchase_failed", f"Provider {result.provider_id} lacks SECONDS to buy {result.task_id}", task_id=result.task_id, provider_id=result.provider_id, cost=cost)
176
  raise HTTPException(status_code=400, detail="Provider lacks SECONDS to buy this data")
177
-
178
  p_balance = db.get_balance(result.provider_id)
179
  log_audit("BUY_DATA", result.provider_id, -cost, p_balance, result.task_id)
180
-
181
  db.add_balance(task["consumer_id"], cost) # The sender earns SECONDS
182
  c_balance = db.get_balance(task["consumer_id"])
183
  log_audit("SELL_DATA", task["consumer_id"], cost, c_balance, result.task_id)
184
-
185
  log_event("task_completed", f"Task {result.task_id[:8]} completed by {result.provider_id}", task_id=result.task_id, provider_id=result.provider_id, bounty=bounty)
186
 
187
  # Move task to completed
@@ -190,7 +191,7 @@ async def complete_task(result: TaskResult, authenticated_node: str = Depends(ve
190
  task["result"] = result.result_payload
191
  completed_tasks[result.task_id] = task
192
  del active_tasks[result.task_id]
193
-
194
  # ROUTE RESULT BACK TO CONSUMER VIA WEBSOCKET
195
  consumer_id = task["consumer_id"]
196
  if consumer_id in connected_nodes:
@@ -215,11 +216,11 @@ async def websocket_endpoint(websocket: WebSocket, node_id: str, timestamp: str,
215
  if not pub_pem:
216
  await websocket.close(code=4001, reason="Unknown Node ID")
217
  return
218
-
219
  if not auth.verify_signature(pub_pem, node_id, timestamp, signature):
220
  await websocket.close(code=4002, reason="Invalid Signature")
221
  return
222
-
223
  await websocket.accept()
224
  connected_nodes[node_id] = websocket
225
  try:
 
23
  ) -> str:
24
  body = await request.body()
25
  payload_str = body.decode('utf-8')
26
+
27
  pub_pem = db.get_pub_pem(x_mep_nodeid)
28
  if not pub_pem:
29
  raise HTTPException(status_code=401, detail="Unknown Node ID. Please register first.")
30
+
31
  if not auth.verify_signature(pub_pem, payload_str, x_mep_timestamp, x_mep_signature):
32
  raise HTTPException(status_code=401, detail="Invalid cryptographic signature.")
33
+
34
  return x_mep_nodeid
35
 
36
  @app.post("/register")
 
38
  # Registration derives the Node ID from the provided Public Key PEM
39
  node_id = auth.derive_node_id(node.pubkey)
40
  balance = db.register_node(node_id, node.pubkey)
41
+
42
  log_event("node_registered", f"Node {node_id} registered with starting balance {balance}", node_id=node_id, starting_balance=balance)
43
  log_audit("REGISTER", node_id, balance, balance, "START_BONUS")
44
+
45
  return {"status": "success", "node_id": node_id, "balance": balance}
46
 
47
  @app.get("/balance/{node_id}")
 
58
  # Verify the signer is actually the consumer claiming to submit the task
59
  if authenticated_node != task.consumer_id:
60
  raise HTTPException(status_code=403, detail="Cannot submit tasks on behalf of another node")
61
+
62
  consumer_balance = db.get_balance(task.consumer_id)
63
  if consumer_balance is None:
64
  raise HTTPException(status_code=404, detail="Consumer node not found")
65
+
66
  # If bounty is positive, consumer is PAYING. Check consumer balance.
67
  if task.bounty > 0 and consumer_balance < task.bounty:
68
  raise HTTPException(status_code=400, detail="Insufficient SECONDS balance to pay for task")
69
+
70
+ task_id = str(uuid.uuid4())
71
+
72
  # Note: If bounty is negative, consumer is SELLING data. We don't deduct here.
73
  # We will deduct from the provider when they complete the task.
74
  if task.bounty > 0:
 
81
  log_audit("ESCROW", task.consumer_id, -task.bounty, new_balance, task_id)
82
 
83
  log_event("task_submitted", f"Task {task_id[:8]} broadcasted by {task.consumer_id} for {task.bounty}", consumer_id=task.consumer_id, task_id=task_id, bounty=task.bounty)
84
+
 
85
  task_data = {
86
  "id": task_id,
87
  "consumer_id": task.consumer_id,
 
92
  "model_requirement": task.model_requirement
93
  }
94
  active_tasks[task_id] = task_data
95
+
96
  # Target specific node if requested (Direct Message skips bidding)
97
  if task.target_node:
98
  if task.target_node in connected_nodes:
 
119
  await ws.send_json({"event": "rfc", "data": rfc_data})
120
  except:
121
  pass
122
+
123
  return {"status": "success", "task_id": task_id}
124
 
125
  @app.post("/tasks/bid")
126
  async def place_bid(bid: TaskBid, authenticated_node: str = Depends(verify_request)):
127
  if authenticated_node != bid.provider_id:
128
  raise HTTPException(status_code=403, detail="Cannot bid on behalf of another node")
129
+
130
  task = active_tasks.get(bid.task_id)
131
  if not task:
132
  raise HTTPException(status_code=404, detail="Task not found or already completed")
133
+
134
  if task["status"] != "bidding":
135
  return {"status": "rejected", "detail": "Task already assigned to another node"}
136
+
137
  # Phase 2 Fast Auction: Accept the first valid bid
138
  task["status"] = "assigned"
139
  task["provider_id"] = bid.provider_id
140
+
141
  log_event("bid_accepted", f"Task {bid.task_id[:8]} assigned to {bid.provider_id}", task_id=bid.task_id, provider_id=bid.provider_id, bounty=task["bounty"])
142
+
143
  # Return the full payload to the winner
144
  return {
145
  "status": "accepted",
 
152
  async def complete_task(result: TaskResult, authenticated_node: str = Depends(verify_request)):
153
  if authenticated_node != result.provider_id:
154
  raise HTTPException(status_code=403, detail="Cannot complete tasks on behalf of another node")
155
+
156
  task = active_tasks.get(result.task_id)
157
  if not task:
158
  raise HTTPException(status_code=404, detail="Task not found or already claimed")
159
+
160
  provider_balance = db.get_balance(result.provider_id)
161
  if provider_balance is None:
162
  db.set_balance(result.provider_id, 0.0)
 
175
  if not success:
176
  log_event("data_purchase_failed", f"Provider {result.provider_id} lacks SECONDS to buy {result.task_id}", task_id=result.task_id, provider_id=result.provider_id, cost=cost)
177
  raise HTTPException(status_code=400, detail="Provider lacks SECONDS to buy this data")
178
+
179
  p_balance = db.get_balance(result.provider_id)
180
  log_audit("BUY_DATA", result.provider_id, -cost, p_balance, result.task_id)
181
+
182
  db.add_balance(task["consumer_id"], cost) # The sender earns SECONDS
183
  c_balance = db.get_balance(task["consumer_id"])
184
  log_audit("SELL_DATA", task["consumer_id"], cost, c_balance, result.task_id)
185
+
186
  log_event("task_completed", f"Task {result.task_id[:8]} completed by {result.provider_id}", task_id=result.task_id, provider_id=result.provider_id, bounty=bounty)
187
 
188
  # Move task to completed
 
191
  task["result"] = result.result_payload
192
  completed_tasks[result.task_id] = task
193
  del active_tasks[result.task_id]
194
+
195
  # ROUTE RESULT BACK TO CONSUMER VIA WEBSOCKET
196
  consumer_id = task["consumer_id"]
197
  if consumer_id in connected_nodes:
 
216
  if not pub_pem:
217
  await websocket.close(code=4001, reason="Unknown Node ID")
218
  return
219
+
220
  if not auth.verify_signature(pub_pem, node_id, timestamp, signature):
221
  await websocket.close(code=4002, reason="Invalid Signature")
222
  return
223
+
224
  await websocket.accept()
225
  connected_nodes[node_id] = websocket
226
  try:
node/alice.pem ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ -----BEGIN PRIVATE KEY-----
2
+ MC4CAQAwBQYDK2VwBCIEINL0Z70Fzpal0mk+yjhLvB3wbVVlghhYcMd9n7f6UjAM
3
+ -----END PRIVATE KEY-----
node/bob.pem ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ -----BEGIN PRIVATE KEY-----
2
+ MC4CAQAwBQYDK2VwBCIEIB4LQ5b/vBp7/S89Z/RAGI4/eg6i8MEc40lpzxlWHpTC
3
+ -----END PRIVATE KEY-----
node/test_three_markets.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import requests
4
+ import websockets
5
+ import time
6
+ import urllib.parse
7
+ from identity import MEPIdentity
8
+
9
+ HUB_URL = "http://localhost:8000"
10
+ WS_URL = "ws://localhost:8000/ws"
11
+
12
+ def get_auth_url(identity: MEPIdentity):
13
+ ts = str(int(time.time()))
14
+ sig = identity.sign(identity.node_id, ts)
15
+ sig_safe = urllib.parse.quote(sig)
16
+ return f"{WS_URL}/{identity.node_id}?timestamp={ts}&signature={sig_safe}"
17
+
18
+ def submit_task(identity: MEPIdentity, payload: str, bounty: float, target: str = None):
19
+ data = {
20
+ "consumer_id": identity.node_id,
21
+ "payload": payload,
22
+ "bounty": bounty
23
+ }
24
+ if target:
25
+ data["target_node"] = target
26
+
27
+ payload_str = json.dumps(data)
28
+ headers = identity.get_auth_headers(payload_str)
29
+ headers["Content-Type"] = "application/json"
30
+ r = requests.post(f"{HUB_URL}/tasks/submit", data=payload_str, headers=headers)
31
+ return r.json()
32
+
33
+ def place_bid(identity: MEPIdentity, task_id: str):
34
+ data = {
35
+ "task_id": task_id,
36
+ "provider_id": identity.node_id
37
+ }
38
+ payload_str = json.dumps(data)
39
+ headers = identity.get_auth_headers(payload_str)
40
+ headers["Content-Type"] = "application/json"
41
+ r = requests.post(f"{HUB_URL}/tasks/bid", data=payload_str, headers=headers)
42
+ return r.json()
43
+
44
+ def complete_task(identity: MEPIdentity, task_id: str, result: str):
45
+ data = {
46
+ "task_id": task_id,
47
+ "provider_id": identity.node_id,
48
+ "result_payload": result
49
+ }
50
+ payload_str = json.dumps(data)
51
+ headers = identity.get_auth_headers(payload_str)
52
+ headers["Content-Type"] = "application/json"
53
+ r = requests.post(f"{HUB_URL}/tasks/complete", data=payload_str, headers=headers)
54
+ return r.json()
55
+
56
+ def get_balance(identity: MEPIdentity):
57
+ r = requests.get(f"{HUB_URL}/balance/{identity.node_id}")
58
+ return r.json().get("balance_seconds", 0.0)
59
+
60
+ async def test_three_markets():
61
+ print("=" * 60)
62
+ print("Testing the 3 MEP Markets (+, 0, -)")
63
+ print("=" * 60)
64
+
65
+ alice = MEPIdentity("alice.pem")
66
+ bob = MEPIdentity("bob.pem")
67
+
68
+ requests.post(f"{HUB_URL}/register", json={"pubkey": alice.pub_pem})
69
+ requests.post(f"{HUB_URL}/register", json={"pubkey": bob.pub_pem})
70
+
71
+ print(f"πŸ‘© Alice (Consumer): {alice.node_id} | Starting Bal: {get_balance(alice)}")
72
+ print(f"πŸ‘¦ Bob (Provider): {bob.node_id} | Starting Bal: {get_balance(bob)}\n")
73
+
74
+ async def bob_listener():
75
+ async with websockets.connect(get_auth_url(bob)) as ws:
76
+ # 1. Wait for Compute Market RFC (+5.0)
77
+ msg = await ws.recv()
78
+ data = json.loads(msg)
79
+ if data["event"] == "rfc" and data["data"]["bounty"] > 0:
80
+ task_id = data["data"]["id"]
81
+ print(f"πŸ‘¦ Bob: Received Compute RFC {task_id[:8]} for +{data['data']['bounty']} SECONDS")
82
+ bid_res = place_bid(bob, task_id)
83
+ if bid_res["status"] == "accepted":
84
+ print(f"πŸ‘¦ Bob: Won Compute Bid! Completing task...")
85
+ complete_task(bob, task_id, "Here is the code you requested.")
86
+ print(f"πŸ‘¦ Bob: Compute task done.\n")
87
+
88
+ # 2. Wait for Cyberspace Direct Message (0.0)
89
+ msg = await ws.recv()
90
+ data = json.loads(msg)
91
+ if data["event"] == "new_task" and data["data"]["bounty"] == 0.0:
92
+ task_id = data["data"]["id"]
93
+ print(f"πŸ‘¦ Bob: Received Cyberspace DM {task_id[:8]} from Alice (0.0 SECONDS)")
94
+ print(f"πŸ‘¦ Bob: Message = '{data['data']['payload']}'")
95
+ complete_task(bob, task_id, "Yes Alice, I am free.")
96
+ print(f"πŸ‘¦ Bob: Sent free reply.\n")
97
+
98
+ # 3. Wait for Data Market RFC (-2.0)
99
+ msg = await ws.recv()
100
+ data = json.loads(msg)
101
+ if data["event"] == "rfc" and data["data"]["bounty"] < 0:
102
+ task_id = data["data"]["id"]
103
+ cost = data["data"]["bounty"]
104
+ print(f"πŸ‘¦ Bob: Received Data Market RFC {task_id[:8]} costing {cost} SECONDS")
105
+
106
+ # Bob's local configuration allows him to spend up to 5.0 SECONDS
107
+ max_purchase_price = -5.0
108
+ if cost >= max_purchase_price:
109
+ print(f"πŸ‘¦ Bob: Budget allows it! Bidding on premium data...")
110
+ bid_res = place_bid(bob, task_id)
111
+ if bid_res["status"] == "accepted":
112
+ print(f"πŸ‘¦ Bob: Paid {abs(cost)} SECONDS to download premium data: '{bid_res['payload']}'")
113
+ complete_task(bob, task_id, "Data received successfully.")
114
+ print(f"πŸ‘¦ Bob: Premium data acquisition complete.\n")
115
+ else:
116
+ print(f"πŸ‘¦ Bob: Too expensive. Ignored.")
117
+
118
+ await asyncio.sleep(0.5)
119
+
120
+ async def alice_sender():
121
+ # Let Bob connect
122
+ await asyncio.sleep(0.5)
123
+
124
+ async with websockets.connect(get_auth_url(alice)) as ws:
125
+ # Market 1: Compute Market (+5.0)
126
+ print(f"πŸ‘© Alice: Submitting Compute Task (+5.0 SECONDS)...")
127
+ submit_task(alice, "Write me a python script", 5.0)
128
+ await asyncio.wait_for(ws.recv(), timeout=2.0) # wait for result
129
+
130
+ # Market 2: Cyberspace Market (0.0)
131
+ print(f"πŸ‘© Alice: Sending Cyberspace DM to Bob (0.0 SECONDS)...")
132
+ submit_task(alice, "Are you free to chat?", 0.0, target=bob.node_id)
133
+ await asyncio.wait_for(ws.recv(), timeout=2.0) # wait for result
134
+
135
+ # Market 3: Data Market (-2.0)
136
+ print(f"πŸ‘© Alice: Broadcasting Premium Dataset (-2.0 SECONDS)...")
137
+ submit_task(alice, "SECRET_TRADING_ALGO_V9", -2.0)
138
+ await asyncio.wait_for(ws.recv(), timeout=2.0) # wait for result
139
+
140
+ await asyncio.sleep(0.5)
141
+
142
+ await asyncio.gather(bob_listener(), alice_sender())
143
+
144
+ print("=" * 60)
145
+ print("Final Balances:")
146
+ print(f"πŸ‘© Alice (Started 10.0): {get_balance(alice)} (Paid 5.0, Earned 2.0 = Expected 7.0)")
147
+ print(f"πŸ‘¦ Bob (Started 10.0): {get_balance(bob)} (Earned 5.0, Paid 2.0 = Expected 13.0)")
148
+ print("=" * 60)
149
+
150
+ if __name__ == "__main__":
151
+ asyncio.run(test_three_markets())