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
playweb/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network SDK
|
|
3
|
+
L1 blockchain SDK for the PlayWebit public network.
|
|
4
|
+
Chain ID: 4968 | Currency: PLWB
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "1.0.0"
|
|
8
|
+
__author__ = "PlayWebIT"
|
|
9
|
+
__license__ = "MIT"
|
|
10
|
+
|
|
11
|
+
from playweb.node import PlayWebitNode
|
|
12
|
+
from playweb.client import PlayWebitClient
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"PlayWebitNode",
|
|
16
|
+
"PlayWebitClient",
|
|
17
|
+
"__version__",
|
|
18
|
+
]
|
playweb/api/__init__.py
ADDED
playweb/api/erc20.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — ERC20 Handler
|
|
3
|
+
PLWB token as ERC20 — reads from chain directly.
|
|
4
|
+
Contract address = Node wallet address.
|
|
5
|
+
No separate contract storage needed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def handle_erc20_call(call_data: str, node) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Handle ERC20 function calls from MetaMask.
|
|
12
|
+
Returns ABI-encoded response or None if not ERC20.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# balanceOf(address) = 0x70a08231
|
|
16
|
+
if call_data.startswith("0x70a08231"):
|
|
17
|
+
addr = "0x" + call_data[34:74]
|
|
18
|
+
balance = node.blockchain.get_balance(addr)
|
|
19
|
+
wei = int(balance * 10**18)
|
|
20
|
+
return "0x" + hex(wei)[2:].zfill(64)
|
|
21
|
+
|
|
22
|
+
# totalSupply() = 0x18160ddd
|
|
23
|
+
elif call_data.startswith("0x18160ddd"):
|
|
24
|
+
addresses = node.blockchain.storage.get_all_addresses()
|
|
25
|
+
total = sum(
|
|
26
|
+
node.blockchain.get_balance(a)
|
|
27
|
+
for a in addresses
|
|
28
|
+
)
|
|
29
|
+
wei = int(total * 10**18)
|
|
30
|
+
return "0x" + hex(wei)[2:].zfill(64)
|
|
31
|
+
|
|
32
|
+
# name() = 0x06fdde03
|
|
33
|
+
elif call_data.startswith("0x06fdde03"):
|
|
34
|
+
return _encode_string("PlayWebit Coin")
|
|
35
|
+
|
|
36
|
+
# symbol() = 0x95d89b41
|
|
37
|
+
elif call_data.startswith("0x95d89b41"):
|
|
38
|
+
return _encode_string("PLWB")
|
|
39
|
+
|
|
40
|
+
# decimals() = 0x313ce567
|
|
41
|
+
elif call_data.startswith("0x313ce567"):
|
|
42
|
+
return "0x" + hex(18)[2:].zfill(64)
|
|
43
|
+
|
|
44
|
+
# allowance() = 0xdd62ed3e → always 0
|
|
45
|
+
elif call_data.startswith("0xdd62ed3e"):
|
|
46
|
+
return "0x" + "0" * 64
|
|
47
|
+
|
|
48
|
+
# approve() = 0x095ea7b3 → always true
|
|
49
|
+
elif call_data.startswith("0x095ea7b3"):
|
|
50
|
+
return "0x" + "1".zfill(64)
|
|
51
|
+
|
|
52
|
+
# supportsInterface = 0x01ffc9a7
|
|
53
|
+
elif call_data.startswith("0x01ffc9a7"):
|
|
54
|
+
return "0x" + "1".zfill(64)
|
|
55
|
+
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _encode_string(s: str) -> str:
|
|
60
|
+
"""ABI encode a string response."""
|
|
61
|
+
b = s.encode("utf-8")
|
|
62
|
+
offset = "0" * 63 + "20"
|
|
63
|
+
length = hex(len(b))[2:].zfill(64)
|
|
64
|
+
padding = 32 - len(b) % 32
|
|
65
|
+
pad = b.hex() + "00" * (padding if padding != 32 else 0)
|
|
66
|
+
return "0x" + offset + length + pad
|
playweb/api/erc721.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — ERC721 Handler
|
|
3
|
+
NFT ownership reads from L1 chain directly.
|
|
4
|
+
Contract address = Node wallet address.
|
|
5
|
+
|
|
6
|
+
Ownership is ALREADY on chain in content_registry
|
|
7
|
+
and edition_registry. This just exposes it as ERC721.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def handle_erc721_call(call_data: str, node) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Handle ERC721 function calls from MetaMask.
|
|
16
|
+
Returns ABI-encoded response or None if not ERC721.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# balanceOf(address) = 0x70a08231
|
|
20
|
+
if call_data.startswith("0x70a08231"):
|
|
21
|
+
addr = "0x" + call_data[34:74]
|
|
22
|
+
records = node.blockchain.storage.get_all_content_by_owner(addr)
|
|
23
|
+
count = len(records)
|
|
24
|
+
return "0x" + hex(count)[2:].zfill(64)
|
|
25
|
+
|
|
26
|
+
# ownerOf(uint256) = 0x6352211e
|
|
27
|
+
elif call_data.startswith("0x6352211e"):
|
|
28
|
+
int_id = int(call_data[10:], 16)
|
|
29
|
+
cid = node.blockchain.storage.get_cid_by_int_id(int_id)
|
|
30
|
+
if cid:
|
|
31
|
+
record = node.blockchain.storage.get_content_record(cid)
|
|
32
|
+
if record:
|
|
33
|
+
owner = record.get("current_owner", "0x" + "0" * 40)
|
|
34
|
+
return "0x" + owner[2:].zfill(64)
|
|
35
|
+
return "0x" + "0" * 64
|
|
36
|
+
|
|
37
|
+
# tokenURI(uint256) = 0xc87b56dd
|
|
38
|
+
elif call_data.startswith("0xc87b56dd"):
|
|
39
|
+
int_id = int(call_data[10:], 16)
|
|
40
|
+
cid = node.blockchain.storage.get_cid_by_int_id(int_id)
|
|
41
|
+
if cid:
|
|
42
|
+
uri = f"{node.node_url}/api/metadata/{cid}"
|
|
43
|
+
return _encode_string(uri)
|
|
44
|
+
return "0x"
|
|
45
|
+
|
|
46
|
+
# name() = 0x06fdde03
|
|
47
|
+
elif call_data.startswith("0x06fdde03"):
|
|
48
|
+
return _encode_string("PlayWebit NFT")
|
|
49
|
+
|
|
50
|
+
# symbol() = 0x95d89b41
|
|
51
|
+
elif call_data.startswith("0x95d89b41"):
|
|
52
|
+
return _encode_string("PLWB-NFT")
|
|
53
|
+
|
|
54
|
+
# supportsInterface(bytes4) = 0x01ffc9a7
|
|
55
|
+
elif call_data.startswith("0x01ffc9a7"):
|
|
56
|
+
iface = call_data[10:18].lower()
|
|
57
|
+
# ERC721=80ac58cd, ERC721Metadata=5b5e139f, ERC165=01ffc9a7
|
|
58
|
+
supported = iface in ["80ac58cd", "5b5e139f", "01ffc9a7"]
|
|
59
|
+
return "0x" + ("1" if supported else "0").zfill(64)
|
|
60
|
+
|
|
61
|
+
# getApproved(uint256) = 0x081812fc → always zero address
|
|
62
|
+
elif call_data.startswith("0x081812fc"):
|
|
63
|
+
return "0x" + "0" * 64
|
|
64
|
+
|
|
65
|
+
# isApprovedForAll = 0xe985e9c5 → always false
|
|
66
|
+
elif call_data.startswith("0xe985e9c5"):
|
|
67
|
+
return "0x" + "0" * 64
|
|
68
|
+
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _encode_string(s: str) -> str:
|
|
73
|
+
"""ABI encode a string response."""
|
|
74
|
+
b = s.encode("utf-8")
|
|
75
|
+
offset = "0" * 63 + "20"
|
|
76
|
+
length = hex(len(b))[2:].zfill(64)
|
|
77
|
+
padding = 32 - len(b) % 32
|
|
78
|
+
pad = b.hex() + "00" * (padding if padding != 32 else 0)
|
|
79
|
+
return "0x" + offset + length + pad
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cid_to_int_id(cid: str) -> int:
|
|
83
|
+
"""
|
|
84
|
+
Convert CID string to integer token ID for MetaMask.
|
|
85
|
+
Deterministic — same CID always gives same int.
|
|
86
|
+
"""
|
|
87
|
+
hash_obj = hashlib.sha256(cid.encode())
|
|
88
|
+
return int(hash_obj.hexdigest()[:16], 16)
|
playweb/api/metadata.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — NFT Metadata Endpoint
|
|
3
|
+
Generic metadata for any NFT on the network.
|
|
4
|
+
Platforms enrich via plugin get_nft_metadata() hook.
|
|
5
|
+
|
|
6
|
+
MetaMask fetches: GET /api/metadata/{cid}
|
|
7
|
+
Returns OpenSea-compatible metadata JSON.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from flask import Blueprint, jsonify
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_metadata_api(node) -> Blueprint:
|
|
17
|
+
bp = Blueprint("metadata", __name__)
|
|
18
|
+
|
|
19
|
+
@bp.route("/api/metadata/<path:cid>", methods=["GET"])
|
|
20
|
+
def nft_metadata(cid):
|
|
21
|
+
"""
|
|
22
|
+
Returns NFT metadata in OpenSea format.
|
|
23
|
+
Reads base data from chain.
|
|
24
|
+
Plugin can enrich with thumbnail, filename etc.
|
|
25
|
+
"""
|
|
26
|
+
record = node.blockchain.storage.get_content_record(cid)
|
|
27
|
+
if not record:
|
|
28
|
+
return jsonify({"error": "CID not registered"}), 404
|
|
29
|
+
|
|
30
|
+
# Base metadata from chain — always available
|
|
31
|
+
metadata = {
|
|
32
|
+
"name": f"PlayWebit NFT — {cid[:12]}...",
|
|
33
|
+
"description": (
|
|
34
|
+
f"Registered on PlayWebit Network by "
|
|
35
|
+
f"{record.get('first_platform', 'unknown')}. "
|
|
36
|
+
f"CID: {cid}"
|
|
37
|
+
),
|
|
38
|
+
"image": "",
|
|
39
|
+
"external_url": f"{node.node_url}/api/owner/{cid}",
|
|
40
|
+
"attributes": [
|
|
41
|
+
{
|
|
42
|
+
"trait_type": "CID",
|
|
43
|
+
"value": cid,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"trait_type": "Platform",
|
|
47
|
+
"value": record.get("first_platform", "unknown"),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"trait_type": "Total Editions",
|
|
51
|
+
"value": record.get("total_editions", 1),
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"trait_type": "Royalty",
|
|
55
|
+
"value": f"{record.get('royalty_pct', 0)}%",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"trait_type": "Current Owner",
|
|
59
|
+
"value": record.get("current_owner", "unknown"),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"trait_type": "Registered Block",
|
|
63
|
+
"value": record.get("first_block", 0),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Let plugin enrich metadata
|
|
69
|
+
# (adds thumbnail, filename, custom attributes etc)
|
|
70
|
+
if node.plugin_manager:
|
|
71
|
+
for plugin in node.plugin_manager.get_all_plugins():
|
|
72
|
+
if hasattr(plugin, "get_nft_metadata"):
|
|
73
|
+
try:
|
|
74
|
+
enriched = plugin.get_nft_metadata(cid, record)
|
|
75
|
+
if enriched:
|
|
76
|
+
metadata.update(enriched)
|
|
77
|
+
break
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(
|
|
80
|
+
f"Plugin metadata error "
|
|
81
|
+
f"({plugin.plugin_id}): {e}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return jsonify(metadata)
|
|
85
|
+
|
|
86
|
+
return bp
|
playweb/api/node_api.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — Node API (Peer-to-Peer)
|
|
3
|
+
HTTP endpoints for node-to-node communication.
|
|
4
|
+
Every node exposes these. Peers call these directly.
|
|
5
|
+
Works on all cloud platforms — no WebSocket needed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from flask import Blueprint, request, jsonify
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def create_node_api(node) -> Blueprint:
|
|
15
|
+
"""
|
|
16
|
+
Factory — creates the peer API blueprint bound to a node instance.
|
|
17
|
+
Mount in Flask app: app.register_blueprint(create_node_api(node))
|
|
18
|
+
"""
|
|
19
|
+
bp = Blueprint("node_api", __name__, url_prefix="/peer")
|
|
20
|
+
|
|
21
|
+
# ─────────────────────────────────────────────────────────────
|
|
22
|
+
# Health check
|
|
23
|
+
# ─────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
@bp.route("/health", methods=["GET"])
|
|
26
|
+
def health():
|
|
27
|
+
tip = node.blockchain.get_chain_tip()
|
|
28
|
+
return jsonify({
|
|
29
|
+
"status": "ok",
|
|
30
|
+
"node_wallet": node.node_wallet,
|
|
31
|
+
"block_height": tip.index if tip else 0,
|
|
32
|
+
"peer_count": node.peer_manager.peer_count(),
|
|
33
|
+
"chain_id": 4968,
|
|
34
|
+
"synced": node.sync.get_sync_status()["synced"],
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
# ─────────────────────────────────────────────────────────────
|
|
38
|
+
# Chain tip — used by new nodes to find canonical chain
|
|
39
|
+
# ─────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
@bp.route("/chain_tip", methods=["GET"])
|
|
42
|
+
def chain_tip():
|
|
43
|
+
tip = node.blockchain.get_chain_tip()
|
|
44
|
+
if not tip:
|
|
45
|
+
return jsonify({"block_index": -1, "block_hash": None})
|
|
46
|
+
return jsonify({
|
|
47
|
+
"block_index": tip.index,
|
|
48
|
+
"block_hash": tip.hash,
|
|
49
|
+
"timestamp": tip.timestamp,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
# ─────────────────────────────────────────────────────────────
|
|
53
|
+
# Block range — used during chain sync
|
|
54
|
+
# ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
@bp.route("/blocks/<int:from_index>/<int:to_index>", methods=["GET"])
|
|
57
|
+
def get_blocks(from_index, to_index):
|
|
58
|
+
limit = min(to_index - from_index + 1, 50) # max 50 per request
|
|
59
|
+
blocks = node.blockchain.get_blocks_from(from_index, limit)
|
|
60
|
+
return jsonify({
|
|
61
|
+
"blocks": [b.to_dict() for b in blocks],
|
|
62
|
+
"from_index": from_index,
|
|
63
|
+
"to_index": to_index,
|
|
64
|
+
"count": len(blocks),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# ─────────────────────────────────────────────────────────────
|
|
68
|
+
# Peer list — new nodes get peers from existing nodes
|
|
69
|
+
# ─────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
@bp.route("/peers", methods=["GET"])
|
|
72
|
+
def get_peers():
|
|
73
|
+
return jsonify({
|
|
74
|
+
"peers": node.peer_manager.get_peer_list_for_sharing(),
|
|
75
|
+
"my_url": node.node_url,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
# ─────────────────────────────────────────────────────────────
|
|
79
|
+
# Receive new transaction from peer
|
|
80
|
+
# ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
@bp.route("/new_transaction", methods=["POST"])
|
|
83
|
+
def new_transaction():
|
|
84
|
+
data = request.get_json(silent=True)
|
|
85
|
+
if not data or "transaction" not in data:
|
|
86
|
+
return jsonify({"success": False, "error": "Missing transaction"}), 400
|
|
87
|
+
|
|
88
|
+
from playweb.core.transaction import Transaction
|
|
89
|
+
try:
|
|
90
|
+
tx = Transaction.from_dict(data["transaction"])
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return jsonify({"success": False, "error": f"Invalid tx: {e}"}), 400
|
|
93
|
+
|
|
94
|
+
# Skip if already in mempool or chain
|
|
95
|
+
if node.blockchain.mempool.contains(tx.hash):
|
|
96
|
+
return jsonify({"success": True, "status": "already_known"})
|
|
97
|
+
|
|
98
|
+
success, result = node.blockchain.add_transaction(
|
|
99
|
+
tx = tx,
|
|
100
|
+
node_wallet = node.node_wallet,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if success:
|
|
104
|
+
# Forward to our peers (gossip propagation)
|
|
105
|
+
node.gossip.broadcast_transaction(
|
|
106
|
+
tx = tx,
|
|
107
|
+
peers = node.peer_manager.get_active_peers(),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return jsonify({
|
|
111
|
+
"success": success,
|
|
112
|
+
"result": result,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
# ─────────────────────────────────────────────────────────────
|
|
116
|
+
# Receive block proposal from consensus leader
|
|
117
|
+
# NVF: Spacetime Fabric activation received
|
|
118
|
+
# ─────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
@bp.route("/propose", methods=["POST"])
|
|
121
|
+
def propose():
|
|
122
|
+
data = request.get_json(silent=True)
|
|
123
|
+
if not data or "block" not in data:
|
|
124
|
+
return jsonify({"success": False, "error": "Missing block"}), 400
|
|
125
|
+
|
|
126
|
+
from playweb.core.block import Block
|
|
127
|
+
try:
|
|
128
|
+
block = Block.from_dict(data["block"])
|
|
129
|
+
round_number = data.get("round_number", block.index)
|
|
130
|
+
from_peer = data.get("from_node", "unknown")
|
|
131
|
+
except Exception as e:
|
|
132
|
+
return jsonify({"success": False, "error": f"Invalid block: {e}"}), 400
|
|
133
|
+
|
|
134
|
+
# Hand off to consensus engine
|
|
135
|
+
node.consensus.on_propose(block, round_number, from_peer)
|
|
136
|
+
|
|
137
|
+
return jsonify({"success": True, "status": "proposal_received"})
|
|
138
|
+
|
|
139
|
+
# ─────────────────────────────────────────────────────────────
|
|
140
|
+
# Receive consensus vote
|
|
141
|
+
# NVF: Planck Threshold approach
|
|
142
|
+
# ─────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
@bp.route("/vote", methods=["POST"])
|
|
145
|
+
def receive_vote():
|
|
146
|
+
data = request.get_json(silent=True)
|
|
147
|
+
if not data or "vote" not in data:
|
|
148
|
+
return jsonify({"success": False, "error": "Missing vote"}), 400
|
|
149
|
+
|
|
150
|
+
from playweb.consensus.vote import Vote
|
|
151
|
+
try:
|
|
152
|
+
vote = Vote.from_dict(data["vote"])
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return jsonify({"success": False, "error": f"Invalid vote: {e}"}), 400
|
|
155
|
+
|
|
156
|
+
node.consensus.on_vote(vote)
|
|
157
|
+
return jsonify({"success": True, "status": "vote_received"})
|
|
158
|
+
|
|
159
|
+
# ─────────────────────────────────────────────────────────────
|
|
160
|
+
# Receive finalised block from peer
|
|
161
|
+
# For nodes that missed consensus round
|
|
162
|
+
# ─────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
@bp.route("/new_block", methods=["POST"])
|
|
165
|
+
def new_block():
|
|
166
|
+
data = request.get_json(silent=True)
|
|
167
|
+
if not data or "block" not in data:
|
|
168
|
+
return jsonify({"success": False, "error": "Missing block"}), 400
|
|
169
|
+
|
|
170
|
+
from playweb.core.block import Block
|
|
171
|
+
try:
|
|
172
|
+
block = Block.from_dict(data["block"])
|
|
173
|
+
votes = data.get("votes", [])
|
|
174
|
+
except Exception as e:
|
|
175
|
+
return jsonify({"success": False, "error": f"Invalid block: {e}"}), 400
|
|
176
|
+
|
|
177
|
+
# Check if we already have this block
|
|
178
|
+
existing = node.blockchain.get_block(block.hash)
|
|
179
|
+
if existing:
|
|
180
|
+
return jsonify({"success": True, "status": "already_known"})
|
|
181
|
+
|
|
182
|
+
# Add to chain
|
|
183
|
+
success, result = node.blockchain.add_block(
|
|
184
|
+
block = block,
|
|
185
|
+
votes = votes,
|
|
186
|
+
node_wallet = node.node_wallet,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return jsonify({"success": success, "result": result})
|
|
190
|
+
|
|
191
|
+
# ─────────────────────────────────────────────────────────────
|
|
192
|
+
# New node joined the network
|
|
193
|
+
# Cloudflare calls this when a new node registers
|
|
194
|
+
# ─────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
@bp.route("/new_node", methods=["POST"])
|
|
197
|
+
def new_node():
|
|
198
|
+
data = request.get_json(silent=True)
|
|
199
|
+
if not data or "url" not in data or "wallet" not in data:
|
|
200
|
+
return jsonify({"success": False, "error": "Missing url or wallet"}), 400
|
|
201
|
+
|
|
202
|
+
added = node.peer_manager.add_peer(
|
|
203
|
+
url = data["url"],
|
|
204
|
+
wallet = data["wallet"],
|
|
205
|
+
platform = data.get("platform", "unknown"),
|
|
206
|
+
role = data.get("role", "validator"),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if added:
|
|
210
|
+
logger.info(f"New peer joined: {data['url']}")
|
|
211
|
+
# Forward new node info to our peers
|
|
212
|
+
node.gossip.broadcast_new_node(
|
|
213
|
+
new_node_url = data["url"],
|
|
214
|
+
new_node_wallet = data["wallet"],
|
|
215
|
+
peers = node.peer_manager.get_active_peers(),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
return jsonify({
|
|
219
|
+
"success": True,
|
|
220
|
+
"added": added,
|
|
221
|
+
"peer_count": node.peer_manager.peer_count(),
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return bp
|