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.
- playweb/__init__.py +18 -0
- playweb/api/__init__.py +4 -0
- playweb/api/erc20.py +66 -0
- playweb/api/erc721.py +88 -0
- playweb/api/metadata.py +86 -0
- playweb/api/node_api.py +224 -0
- playweb/api/public_api.py +496 -0
- playweb/api/rpc.py +247 -0
- playweb/client.py +341 -0
- playweb/config.py +168 -0
- playweb/consensus/__init__.py +5 -0
- playweb/consensus/leader.py +50 -0
- playweb/consensus/nvf_bft.py +540 -0
- playweb/consensus/vote.py +108 -0
- playweb/core/__init__.py +6 -0
- playweb/core/block.py +156 -0
- playweb/core/blockchain.py +358 -0
- playweb/core/fee_engine.py +312 -0
- playweb/core/mempool.py +112 -0
- playweb/core/royalty_engine.py +207 -0
- playweb/core/transaction.py +270 -0
- playweb/network/__init__.py +6 -0
- playweb/network/bootstrap.py +217 -0
- playweb/network/gossip.py +222 -0
- playweb/network/peer_manager.py +164 -0
- playweb/network/sync.py +244 -0
- playweb/node.py +303 -0
- playweb/plugin/__init__.py +4 -0
- playweb/plugin/base_plugin.py +225 -0
- playweb/plugin/plugin_manager.py +131 -0
- playweb/registry/__init__.py +4 -0
- playweb/registry/content_registry.py +259 -0
- playweb/registry/edition_registry.py +220 -0
- playweb/storage/__init__.py +11 -0
- playweb/storage/base.py +154 -0
- playweb/storage/ram_storage.py +123 -0
- playweb/storage/sqlite_storage.py +369 -0
- playweb/storage/supabase_storage.py +389 -0
- playweb_node-1.0.0.dist-info/METADATA +416 -0
- playweb_node-1.0.0.dist-info/RECORD +43 -0
- playweb_node-1.0.0.dist-info/WHEEL +5 -0
- playweb_node-1.0.0.dist-info/licenses/LICENSE +201 -0
- 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
|