playweb-node 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. playweb/__init__.py +18 -0
  2. playweb/api/__init__.py +4 -0
  3. playweb/api/erc20.py +66 -0
  4. playweb/api/erc721.py +88 -0
  5. playweb/api/metadata.py +86 -0
  6. playweb/api/node_api.py +224 -0
  7. playweb/api/public_api.py +496 -0
  8. playweb/api/rpc.py +247 -0
  9. playweb/client.py +341 -0
  10. playweb/config.py +168 -0
  11. playweb/consensus/__init__.py +5 -0
  12. playweb/consensus/leader.py +50 -0
  13. playweb/consensus/nvf_bft.py +540 -0
  14. playweb/consensus/vote.py +108 -0
  15. playweb/core/__init__.py +6 -0
  16. playweb/core/block.py +156 -0
  17. playweb/core/blockchain.py +358 -0
  18. playweb/core/fee_engine.py +312 -0
  19. playweb/core/mempool.py +112 -0
  20. playweb/core/royalty_engine.py +207 -0
  21. playweb/core/transaction.py +270 -0
  22. playweb/network/__init__.py +6 -0
  23. playweb/network/bootstrap.py +217 -0
  24. playweb/network/gossip.py +222 -0
  25. playweb/network/peer_manager.py +164 -0
  26. playweb/network/sync.py +244 -0
  27. playweb/node.py +303 -0
  28. playweb/plugin/__init__.py +4 -0
  29. playweb/plugin/base_plugin.py +225 -0
  30. playweb/plugin/plugin_manager.py +131 -0
  31. playweb/registry/__init__.py +4 -0
  32. playweb/registry/content_registry.py +259 -0
  33. playweb/registry/edition_registry.py +220 -0
  34. playweb/storage/__init__.py +11 -0
  35. playweb/storage/base.py +154 -0
  36. playweb/storage/ram_storage.py +123 -0
  37. playweb/storage/sqlite_storage.py +369 -0
  38. playweb/storage/supabase_storage.py +389 -0
  39. playweb_node-1.0.0.dist-info/METADATA +416 -0
  40. playweb_node-1.0.0.dist-info/RECORD +43 -0
  41. playweb_node-1.0.0.dist-info/WHEEL +5 -0
  42. playweb_node-1.0.0.dist-info/licenses/LICENSE +201 -0
  43. playweb_node-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,496 @@
1
+ """
2
+ PlayWebit Network — Public API
3
+ HTTP endpoints for dApps, clients, and the spiderweave-sdk.
4
+
5
+ spiderweave-sdk's PlayWebitAdapter calls:
6
+ POST /api/anchor_spider_hash
7
+ GET /api/transaction/{tx_hash}
8
+ GET /api/spider_hashes/{chain_id}
9
+
10
+ All other endpoints are for general dApp use.
11
+ """
12
+
13
+ import logging
14
+ from flask import Blueprint, request, jsonify
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def create_public_api(node) -> Blueprint:
20
+ """
21
+ Factory — creates the public API blueprint bound to a node instance.
22
+ Mount in Flask app: app.register_blueprint(create_public_api(node))
23
+ """
24
+ bp = Blueprint("public_api", __name__, url_prefix="/api")
25
+
26
+ # ─────────────────────────────────────────────────────────────
27
+ # Spider Hash Anchor
28
+ # Called by spiderweave-sdk's PlayWebitAdapter
29
+ # ─────────────────────────────────────────────────────────────
30
+
31
+ @bp.route("/anchor_spider_hash", methods=["POST"])
32
+ def anchor_spider_hash():
33
+ """
34
+ Anchor a SpiderWeave hash on the chain.
35
+ L1 stores it, never interprets it.
36
+ The hash can represent any database state on any platform.
37
+ """
38
+ data = request.get_json(silent=True)
39
+ if not data:
40
+ return jsonify({"success": False, "error": "Missing body"}), 400
41
+
42
+ chain_name = data.get("chain_id") or data.get("chain_name")
43
+ spider_hash = data.get("spider_hash")
44
+ event_type = data.get("event_type", "integrity_check")
45
+ platform_wallet = data.get("platform_wallet") or data.get("wallet")
46
+ signature = data.get("signature")
47
+ metadata = data.get("metadata", {})
48
+
49
+ if not chain_name or not spider_hash:
50
+ return jsonify({
51
+ "success": False,
52
+ "error": "Missing chain_id/chain_name and spider_hash",
53
+ }), 400
54
+
55
+ if not platform_wallet:
56
+ return jsonify({
57
+ "success": False,
58
+ "error": "Missing platform_wallet",
59
+ }), 400
60
+
61
+ from playweb.core.transaction import Transaction
62
+ tx = Transaction(
63
+ from_addr = platform_wallet.lower(),
64
+ to_addr = platform_wallet.lower(),
65
+ amount = 0,
66
+ tx_type = "spider_hash_anchor",
67
+ signature = signature,
68
+ spider_hash = spider_hash,
69
+ chain_name = chain_name,
70
+ data = {
71
+ "event_type": event_type,
72
+ "platform": data.get("platform_id", "unknown"),
73
+ **metadata,
74
+ },
75
+ )
76
+
77
+ success, result = node.blockchain.add_transaction(
78
+ tx = tx,
79
+ node_wallet = node.node_wallet,
80
+ )
81
+
82
+ if not success:
83
+ return jsonify({"success": False, "error": result}), 400
84
+
85
+ # Broadcast to peers
86
+ node.gossip.broadcast_transaction(
87
+ tx = tx,
88
+ peers = node.peer_manager.get_active_peers(),
89
+ )
90
+
91
+ return jsonify({
92
+ "success": True,
93
+ "tx_hash": tx.hash,
94
+ "chain_id": chain_name,
95
+ "status": "pending",
96
+ })
97
+
98
+ # ─────────────────────────────────────────────────────────────
99
+ # Get transaction
100
+ # Called by spiderweave-sdk to verify anchored hashes
101
+ # ─────────────────────────────────────────────────────────────
102
+
103
+ @bp.route("/transaction/<tx_hash>", methods=["GET"])
104
+ def get_transaction(tx_hash):
105
+ tx = node.blockchain.get_transaction(tx_hash)
106
+ if not tx:
107
+ return jsonify({"success": False, "error": "Transaction not found"}), 404
108
+ return jsonify({
109
+ "success": True,
110
+ "transaction": tx.to_dict(),
111
+ })
112
+
113
+ # ─────────────────────────────────────────────────────────────
114
+ # Get spider hashes for a chain
115
+ # Called by spiderweave-sdk to get all anchored hashes
116
+ # ─────────────────────────────────────────────────────────────
117
+
118
+ @bp.route("/spider_hashes/<chain_name>", methods=["GET"])
119
+ def get_spider_hashes(chain_name):
120
+ hashes = []
121
+
122
+ # ── Confirmed (in blocks) ─────────────────────────────────
123
+ length = node.blockchain.get_chain_length()
124
+ scan_from = max(0, length - 1000)
125
+ blocks = node.blockchain.get_blocks_from(scan_from, 1000)
126
+
127
+ for block in blocks:
128
+ for tx in block.transactions:
129
+ if (
130
+ tx.tx_type == "spider_hash_anchor"
131
+ and tx.chain_name == chain_name
132
+ ):
133
+ hashes.append({
134
+ "tx_hash": tx.hash,
135
+ "spider_hash": tx.spider_hash,
136
+ "timestamp": tx.timestamp,
137
+ "block_index": block.index,
138
+ "status": "confirmed",
139
+ "event_type": tx.data.get("event_type")
140
+ if tx.data else None,
141
+ })
142
+
143
+ # ── Pending (in mempool — not yet mined) ─────────────────
144
+ for tx in node.blockchain.mempool.get_pending():
145
+ if (
146
+ tx.tx_type == "spider_hash_anchor"
147
+ and tx.chain_name == chain_name
148
+ ):
149
+ hashes.append({
150
+ "tx_hash": tx.hash,
151
+ "spider_hash": tx.spider_hash,
152
+ "timestamp": tx.timestamp,
153
+ "block_index": None,
154
+ "status": "pending",
155
+ "event_type": tx.data.get("event_type")
156
+ if tx.data else None,
157
+ })
158
+
159
+ return jsonify({
160
+ "success": True,
161
+ "chain_name": chain_name,
162
+ "count": len(hashes),
163
+ "confirmed": len([h for h in hashes if h["status"] == "confirmed"]),
164
+ "pending": len([h for h in hashes if h["status"] == "pending"]),
165
+ "hashes": hashes,
166
+ })
167
+
168
+ # ─────────────────────────────────────────────────────────────
169
+ # Balance
170
+ # ─────────────────────────────────────────────────────────────
171
+
172
+ @bp.route("/balance/<address>", methods=["GET"])
173
+ def get_balance(address):
174
+ balance = node.blockchain.get_balance(address)
175
+ return jsonify({
176
+ "success": True,
177
+ "address": address.lower(),
178
+ "balance": balance,
179
+ "currency": "PLWB",
180
+ })
181
+
182
+ # ─────────────────────────────────────────────────────────────
183
+ # Submit transaction
184
+ # ─────────────────────────────────────────────────────────────
185
+
186
+ @bp.route("/transaction", methods=["POST"])
187
+ def submit_transaction():
188
+ data = request.get_json(silent=True)
189
+ if not data:
190
+ return jsonify({"success": False, "error": "Missing body"}), 400
191
+
192
+ from playweb.core.transaction import Transaction
193
+ try:
194
+ tx = Transaction.from_dict(data)
195
+ except Exception as e:
196
+ return jsonify({"success": False, "error": f"Invalid tx: {e}"}), 400
197
+
198
+ success, result = node.blockchain.add_transaction(
199
+ tx = tx,
200
+ node_wallet = node.node_wallet,
201
+ )
202
+
203
+ if success:
204
+ node.gossip.broadcast_transaction(
205
+ tx = tx,
206
+ peers = node.peer_manager.get_active_peers(),
207
+ )
208
+
209
+ return jsonify({"success": success, "result": result})
210
+
211
+ # ─────────────────────────────────────────────────────────────
212
+ # PLWB Transfer between wallets
213
+ # ─────────────────────────────────────────────────────────────
214
+
215
+ @bp.route("/transfer", methods=["POST"])
216
+ def transfer_plwb():
217
+ """
218
+ Transfer PLWB from one wallet to another.
219
+ Requires MetaMask signature from sender.
220
+ Network fee: 1 PLWB split 50/50.
221
+ """
222
+ data = request.get_json(silent=True) or {}
223
+ from_addr = data.get("from_addr")
224
+ to_addr = data.get("to_addr")
225
+ amount = data.get("amount")
226
+ signature = data.get("signature")
227
+
228
+ if not all([from_addr, to_addr, amount, signature]):
229
+ return jsonify({
230
+ "success": False,
231
+ "error": "Missing: from_addr, to_addr, amount, signature",
232
+ }), 400
233
+
234
+ if float(amount) <= 0:
235
+ return jsonify({
236
+ "success": False,
237
+ "error": "Amount must be greater than 0",
238
+ }), 400
239
+
240
+ from playweb.core.transaction import Transaction
241
+ tx = Transaction(
242
+ from_addr = from_addr,
243
+ to_addr = to_addr,
244
+ amount = float(amount),
245
+ tx_type = "transfer",
246
+ signature = signature,
247
+ )
248
+
249
+ success, result = node.blockchain.add_transaction(
250
+ tx = tx,
251
+ node_wallet = node.node_wallet,
252
+ )
253
+ if not success:
254
+ return jsonify({"success": False, "error": result}), 400
255
+
256
+ node.gossip.broadcast_transaction(
257
+ tx = tx,
258
+ peers = node.peer_manager.get_active_peers(),
259
+ )
260
+
261
+ return jsonify({
262
+ "success": True,
263
+ "tx_hash": tx.hash,
264
+ "from": from_addr,
265
+ "to": to_addr,
266
+ "amount": float(amount),
267
+ "fee": 1.0,
268
+ "total": float(amount) + 1.0,
269
+ "status": "pending",
270
+ })
271
+
272
+ # ─────────────────────────────────────────────────────────────
273
+ # Content ownership
274
+ # ─────────────────────────────────────────────────────────────
275
+
276
+ @bp.route("/owner/<path:cid>", methods=["GET"])
277
+ def get_owner(cid):
278
+ """Who owns this CID across all platforms."""
279
+ owner = node.content_registry.get_owner(cid)
280
+ if not owner:
281
+ return jsonify({
282
+ "success": False,
283
+ "error": "CID not registered on PlayWebit Network",
284
+ }), 404
285
+ return jsonify({"success": True, **owner})
286
+
287
+ @bp.route("/check_duplicate/<path:cid>", methods=["GET"])
288
+ def check_duplicate(cid):
289
+ """Is this CID already registered anywhere on the network?"""
290
+ result = node.content_registry.check_duplicate(cid)
291
+ return jsonify({"success": True, **result})
292
+
293
+ @bp.route("/verify_ownership", methods=["POST"])
294
+ def verify_ownership():
295
+ data = request.get_json(silent=True) or {}
296
+ cid = data.get("cid")
297
+ wallet = data.get("wallet")
298
+ if not cid or not wallet:
299
+ return jsonify({
300
+ "success": False,
301
+ "error": "Missing cid or wallet",
302
+ }), 400
303
+ owns = node.content_registry.verify_ownership(cid, wallet)
304
+ return jsonify({
305
+ "success": True,
306
+ "owns": owns,
307
+ "cid": cid,
308
+ "wallet": wallet,
309
+ })
310
+
311
+ # ─────────────────────────────────────────────────────────────
312
+ # Editions
313
+ # ─────────────────────────────────────────────────────────────
314
+
315
+ @bp.route("/editions/<path:cid>", methods=["GET"])
316
+ def get_editions(cid):
317
+ """All editions, all owners, all platforms for a CID."""
318
+ summary = node.edition_registry.get_edition_summary(cid)
319
+ return jsonify({"success": True, **summary})
320
+
321
+ @bp.route("/editions/<path:cid>/<int:edition_number>", methods=["GET"])
322
+ def get_edition(cid, edition_number):
323
+ edition = node.edition_registry.get_edition(cid, edition_number)
324
+ if not edition:
325
+ return jsonify({"success": False, "error": "Edition not found"}), 404
326
+ return jsonify({"success": True, "edition": edition})
327
+
328
+ @bp.route("/editions/<path:cid>/available", methods=["GET"])
329
+ def get_available_editions(cid):
330
+ """
331
+ Editions still owned by original creator = not yet sold.
332
+ Used by CipherVault creator tab to show available editions.
333
+ """
334
+ content = node.blockchain.storage.get_content_record(cid)
335
+ if not content:
336
+ return jsonify({"success": False, "error": "CID not found"}), 404
337
+
338
+ creator = content["creator_wallet"]
339
+ all_editions = node.edition_registry.get_all_editions(cid)
340
+ total = content["total_editions"]
341
+
342
+ available = [
343
+ e for e in all_editions
344
+ if e["current_owner"].lower() == creator.lower()
345
+ ]
346
+ sold = [
347
+ e for e in all_editions
348
+ if e["current_owner"].lower() != creator.lower()
349
+ ]
350
+
351
+ return jsonify({
352
+ "success": True,
353
+ "cid": cid,
354
+ "total": total,
355
+ "available": len(available),
356
+ "sold": len(sold),
357
+ "creator": creator,
358
+ "editions": all_editions,
359
+ })
360
+
361
+ # ─────────────────────────────────────────────────────────────
362
+ # Node Registration — Sybil Resistance
363
+ # Called by your PlayWebit Portal admin dashboard
364
+ # when approving a new platform/validator
365
+ # ─────────────────────────────────────────────────────────────
366
+
367
+ @bp.route("/node/register", methods=["POST"])
368
+ def register_node():
369
+ """
370
+ Register a node wallet as a validator on the network.
371
+ Only callable with X-Authority-Secret header.
372
+ Submits a node_register tx signed by authority wallet.
373
+ After next block mines, the node can participate in consensus.
374
+ """
375
+ import os
376
+ import time
377
+
378
+ # Verify internal secret — only your portal can call this
379
+ secret = request.headers.get("X-Authority-Secret")
380
+ if secret != os.getenv("AUTHORITY_INTERNAL_SECRET", ""):
381
+ return jsonify({
382
+ "success": False,
383
+ "error": "Unauthorized — X-Authority-Secret required",
384
+ }), 401
385
+
386
+ data = request.get_json(silent=True) or {}
387
+ platform_wallet = data.get("platform_wallet", "").lower()
388
+ platform_id = data.get("platform_id", "unknown")
389
+ node_url = data.get("node_url", "")
390
+
391
+ if not platform_wallet:
392
+ return jsonify({
393
+ "success": False,
394
+ "error": "Missing platform_wallet",
395
+ }), 400
396
+
397
+ if not platform_wallet.startswith("0x") or len(platform_wallet) != 42:
398
+ return jsonify({
399
+ "success": False,
400
+ "error": "Invalid wallet address format",
401
+ }), 400
402
+
403
+ # Check if already registered
404
+ if node.consensus._is_registered_node(platform_wallet):
405
+ return jsonify({
406
+ "success": True,
407
+ "already_registered": True,
408
+ "platform_wallet": platform_wallet,
409
+ "message": "Node already registered as validator",
410
+ })
411
+
412
+ # Submit node_register tx from authority wallet
413
+ from playweb.core.transaction import Transaction
414
+
415
+ tx = Transaction(
416
+ from_addr = node.node_wallet,
417
+ to_addr = platform_wallet,
418
+ amount = 0,
419
+ tx_type = "node_register",
420
+ nonce = int(time.time() * 1000),
421
+ data = {
422
+ "platform_id": platform_id,
423
+ "node_url": node_url,
424
+ "approved_at": time.time(),
425
+ "approved_by": node.node_wallet,
426
+ },
427
+ )
428
+
429
+ success, result = node.blockchain.add_transaction(
430
+ tx = tx,
431
+ node_wallet = node.node_wallet,
432
+ )
433
+
434
+ if not success:
435
+ return jsonify({"success": False, "error": result}), 400
436
+
437
+ # Broadcast to peers
438
+ node.gossip.broadcast_transaction(
439
+ tx = tx,
440
+ peers = node.peer_manager.get_active_peers(),
441
+ )
442
+
443
+ logger.info(
444
+ f"Node registration submitted: {platform_wallet[:12]}... "
445
+ f"({platform_id})"
446
+ )
447
+
448
+ return jsonify({
449
+ "success": True,
450
+ "tx_hash": tx.hash,
451
+ "platform_wallet": platform_wallet,
452
+ "platform_id": platform_id,
453
+ "node_url": node_url,
454
+ "status": "pending",
455
+ "message": (
456
+ f"{platform_id} will be a registered validator "
457
+ f"after next block is mined"
458
+ ),
459
+ })
460
+
461
+ @bp.route("/node/validators", methods=["GET"])
462
+ def get_validators():
463
+ """
464
+ Get list of all registered validator wallets.
465
+ Public endpoint — anyone can see who the validators are.
466
+ """
467
+ validators = node.consensus.get_registered_validators()
468
+ return jsonify({
469
+ "success": True,
470
+ "count": len(validators),
471
+ "validators": validators,
472
+ })
473
+
474
+ # ─────────────────────────────────────────────────────────────
475
+ # Network stats
476
+ # ─────────────────────────────────────────────────────────────
477
+
478
+ @bp.route("/network/stats", methods=["GET"])
479
+ def network_stats():
480
+ tip = node.blockchain.get_chain_tip()
481
+ stats = node.blockchain.get_stats()
482
+ return jsonify({
483
+ "success": True,
484
+ "chain_id": 4968,
485
+ "currency": "PLWB",
486
+ "block_height": tip.index if tip else 0,
487
+ "chain_length": stats["chain_length"],
488
+ "pending_txs": stats["pending_tx_count"],
489
+ "node_count": node.peer_manager.peer_count() + 1,
490
+ "node_wallet": node.node_wallet,
491
+ "consensus": node.consensus.get_status(),
492
+ "sync": node.sync.get_sync_status(),
493
+ "plugins": node.plugin_manager.get_status(),
494
+ })
495
+
496
+ return bp # ← always last, after ALL routes