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
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
+ ]
@@ -0,0 +1,4 @@
1
+ from playweb.api.node_api import create_node_api
2
+ from playweb.api.public_api import create_public_api
3
+
4
+ __all__ = ["create_node_api", "create_public_api"]
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)
@@ -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
@@ -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