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/api/rpc.py ADDED
@@ -0,0 +1,247 @@
1
+ """
2
+ PlayWebit Network — Ethereum JSON-RPC Compatibility Layer
3
+ Allows MetaMask to connect to PlayWebit Network.
4
+
5
+ MetaMask setup:
6
+ RPC URL: https://your-node.com/rpc
7
+ Chain ID: 4968
8
+ Currency: PLWB
9
+
10
+ Contract address = Node wallet address (for both ERC20 + ERC721)
11
+ No Solidity. No deployment. No gas fees to deploy.
12
+ """
13
+
14
+ import logging
15
+ from flask import Blueprint, request, jsonify
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def create_rpc(node) -> Blueprint:
21
+ bp = Blueprint("rpc", __name__)
22
+
23
+ @bp.route("/rpc", methods=["POST"])
24
+ def rpc():
25
+ data = request.get_json(silent=True) or {}
26
+ method = data.get("method", "")
27
+ params = data.get("params", [])
28
+ req_id = data.get("id", 1)
29
+
30
+ def ok(val):
31
+ return jsonify({
32
+ "jsonrpc": "2.0",
33
+ "id": req_id,
34
+ "result": val,
35
+ })
36
+
37
+ def err(code, msg):
38
+ return jsonify({
39
+ "jsonrpc": "2.0",
40
+ "id": req_id,
41
+ "error": {"code": code, "message": msg},
42
+ }), 400
43
+
44
+ # ── Network identity ──────────────────────────────────
45
+ if method == "eth_chainId":
46
+ return ok("0x1368") # 4968 in hex
47
+
48
+ if method == "net_version":
49
+ return ok("4968")
50
+
51
+ if method == "eth_blockNumber":
52
+ length = node.blockchain.get_chain_length()
53
+ return ok(hex(max(0, length - 1)))
54
+
55
+ if method == "eth_accounts":
56
+ return ok([])
57
+
58
+ if method == "eth_estimateGas":
59
+ return ok("0x5208") # 21000 fixed
60
+
61
+ if method == "eth_gasPrice":
62
+ return ok("0x1") # 1 wei fixed
63
+
64
+ if method == "eth_getTransactionCount":
65
+ return ok("0x1")
66
+
67
+ # ── PLWB native balance ───────────────────────────────
68
+ if method == "eth_getBalance":
69
+ addr = params[0] if params else "0x0"
70
+ balance = node.blockchain.get_balance(addr)
71
+ wei = int(balance * 10**18)
72
+ return ok(hex(wei))
73
+
74
+ # ── ERC20 + ERC721 via eth_call ───────────────────────
75
+ if method == "eth_call":
76
+ if not params or not isinstance(params[0], dict):
77
+ return ok("0x")
78
+
79
+ from playweb.api.erc20 import handle_erc20_call
80
+ from playweb.api.erc721 import handle_erc721_call
81
+
82
+ call_data = params[0].get("data", "0x")
83
+ to_addr = params[0].get("to", "").lower()
84
+
85
+ # Both ERC20 + ERC721 use node wallet as contract address
86
+ if to_addr == node.node_wallet.lower():
87
+ # Try ERC20 first (PLWB token)
88
+ result = handle_erc20_call(call_data, node)
89
+ if result is not None:
90
+ return ok(result)
91
+
92
+ # Then try ERC721 (NFT ownership)
93
+ result = handle_erc721_call(call_data, node)
94
+ if result is not None:
95
+ return ok(result)
96
+
97
+ return ok("0x")
98
+
99
+ # ── Transaction submission ────────────────────────────
100
+ if method == "eth_sendRawTransaction":
101
+ raw_tx = params[0] if params else None
102
+ if not raw_tx:
103
+ return err(-32602, "Missing raw transaction")
104
+ try:
105
+ from eth_account import Account
106
+ from eth_account.messages import encode_defunct
107
+ from playweb.core.transaction import Transaction
108
+ import time
109
+
110
+ decoded = Account.decode_transaction(raw_tx)
111
+ from_addr = decoded.get("from", "")
112
+ if not from_addr:
113
+ # recover from signature
114
+ from_addr = Account.recover_transaction(raw_tx)
115
+
116
+ tx = Transaction(
117
+ from_addr = from_addr.lower(),
118
+ to_addr = decoded["to"].lower(),
119
+ amount = decoded["value"] / 10**18,
120
+ tx_type = "transfer",
121
+ signature = raw_tx,
122
+ nonce = decoded.get("nonce", int(time.time())),
123
+ )
124
+
125
+ success, result = node.blockchain.add_transaction(
126
+ tx = tx,
127
+ node_wallet = node.node_wallet,
128
+ )
129
+
130
+ if success:
131
+ node.gossip.broadcast_transaction(
132
+ tx = tx,
133
+ peers = node.peer_manager.get_active_peers(),
134
+ )
135
+ return ok(tx.hash)
136
+ else:
137
+ return err(-32000, result)
138
+
139
+ except Exception as e:
140
+ logger.error(f"eth_sendRawTransaction error: {e}")
141
+ return err(-32000, str(e))
142
+
143
+ # ── Transaction queries ───────────────────────────────
144
+ if method == "eth_getTransactionByHash":
145
+ tx_hash = params[0] if params else None
146
+ if not tx_hash:
147
+ return ok(None)
148
+ tx = node.blockchain.get_transaction(tx_hash)
149
+ if not tx:
150
+ return ok(None)
151
+ return ok({
152
+ "hash": tx.hash,
153
+ "from": tx.from_addr,
154
+ "to": tx.to_addr,
155
+ "value": hex(int(tx.amount * 10**18)),
156
+ "blockNumber": None,
157
+ "transactionIndex": "0x0",
158
+ "gas": "0x5208",
159
+ "gasPrice": "0x1",
160
+ "nonce": hex(tx.nonce or 0),
161
+ "input": "0x",
162
+ })
163
+
164
+ if method == "eth_getTransactionReceipt":
165
+ tx_hash = params[0] if params else None
166
+ if not tx_hash:
167
+ return ok(None)
168
+ tx = node.blockchain.get_transaction(tx_hash)
169
+ if not tx:
170
+ return ok(None)
171
+ tip = node.blockchain.get_chain_tip()
172
+ return ok({
173
+ "transactionHash": tx_hash,
174
+ "status": "0x1",
175
+ "blockNumber": hex(tip.index) if tip else "0x0",
176
+ "blockHash": tip.hash if tip else "0x0",
177
+ "gasUsed": "0x5208",
178
+ "cumulativeGasUsed":"0x5208",
179
+ "logs": [],
180
+ "logsBloom": "0x" + "0" * 512,
181
+ })
182
+
183
+ if method == "eth_getBlockByNumber":
184
+ block_param = params[0] if params else "latest"
185
+ include_txs = params[1] if len(params) > 1 else False
186
+
187
+ if block_param == "latest":
188
+ block = node.blockchain.get_chain_tip()
189
+ elif block_param == "earliest":
190
+ block = node.blockchain.get_block_by_index(0)
191
+ else:
192
+ try:
193
+ block = node.blockchain.get_block_by_index(
194
+ int(block_param, 16)
195
+ )
196
+ except:
197
+ block = node.blockchain.get_chain_tip()
198
+
199
+ if not block:
200
+ return ok(None)
201
+
202
+ txs = (
203
+ [tx.to_dict() for tx in block.transactions]
204
+ if include_txs
205
+ else [tx.hash for tx in block.transactions]
206
+ )
207
+ return ok({
208
+ "number": hex(block.index),
209
+ "hash": block.hash,
210
+ "parentHash": block.previous_hash,
211
+ "timestamp": hex(int(block.timestamp)),
212
+ "transactions": txs,
213
+ "miner": block.validator_wallet,
214
+ "gasLimit": "0x1c9c380",
215
+ "gasUsed": "0x0",
216
+ "difficulty": "0x1",
217
+ "nonce": "0x0000000000000000",
218
+ "extraData": "0x",
219
+ "logsBloom": "0x" + "0" * 512,
220
+ })
221
+
222
+ if method == "eth_getBlockByHash":
223
+ block_hash = params[0] if params else None
224
+ include_txs = params[1] if len(params) > 1 else False
225
+ if not block_hash:
226
+ return ok(None)
227
+ block = node.blockchain.get_block(block_hash)
228
+ if not block:
229
+ return ok(None)
230
+ txs = (
231
+ [tx.to_dict() for tx in block.transactions]
232
+ if include_txs
233
+ else [tx.hash for tx in block.transactions]
234
+ )
235
+ return ok({
236
+ "number": hex(block.index),
237
+ "hash": block.hash,
238
+ "parentHash": block.previous_hash,
239
+ "timestamp": hex(int(block.timestamp)),
240
+ "transactions": txs,
241
+ })
242
+
243
+ # ── Unknown method ────────────────────────────────────
244
+ logger.debug(f"Unknown RPC method: {method}")
245
+ return ok("0x")
246
+
247
+ return bp
playweb/client.py ADDED
@@ -0,0 +1,341 @@
1
+ """
2
+ PlayWebit Network — PlayWebitClient
3
+ Lightweight client for dApps that connect to a validator node.
4
+ No local chain storage. No consensus participation.
5
+ Just calls a node's public API.
6
+
7
+ Use this for:
8
+ - HuggingFace apps (CipherVault frontend)
9
+ - Any app that doesn't want to run a full node
10
+ - spiderweave-sdk integration without running a node
11
+
12
+ Usage:
13
+ from playweb import PlayWebitClient
14
+
15
+ client = PlayWebitClient(
16
+ validator_url = "https://node1.playwebit.com",
17
+ platform_wallet = os.getenv("PLATFORM_WALLET"),
18
+ )
19
+
20
+ result = client.register_content(
21
+ cid = "QmXyz...",
22
+ owner = "0xabc...",
23
+ editions = 100,
24
+ royalty_pct = 10,
25
+ signature = "0x...",
26
+ )
27
+ """
28
+
29
+ import logging
30
+ import requests
31
+ from typing import Optional, Dict, Tuple
32
+
33
+ from playweb.config import PEER_TIMEOUT, CHAIN_ID
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+
38
+ class PlayWebitClient:
39
+
40
+ def __init__(
41
+ self,
42
+ validator_url: str,
43
+ platform_wallet: str,
44
+ timeout: int = PEER_TIMEOUT,
45
+ ):
46
+ self.validator_url = validator_url.rstrip("/")
47
+ self.platform_wallet = platform_wallet.lower()
48
+ self.timeout = timeout
49
+
50
+ # ─────────────────────────────────────────────────────────────
51
+ # Internal HTTP helpers
52
+ # ─────────────────────────────────────────────────────────────
53
+
54
+ def _get(self, path: str) -> Dict:
55
+ try:
56
+ res = requests.get(
57
+ f"{self.validator_url}{path}",
58
+ timeout=self.timeout,
59
+ )
60
+ return res.json()
61
+ except Exception as e:
62
+ logger.error(f"Client GET {path} failed: {e}")
63
+ return {"success": False, "error": str(e)}
64
+
65
+ def _post(self, path: str, data: Dict) -> Dict:
66
+ try:
67
+ res = requests.post(
68
+ f"{self.validator_url}{path}",
69
+ json = data,
70
+ timeout = self.timeout,
71
+ )
72
+ return res.json()
73
+ except Exception as e:
74
+ logger.error(f"Client POST {path} failed: {e}")
75
+ return {"success": False, "error": str(e)}
76
+
77
+ def transfer_plwb(
78
+ self,
79
+ from_addr: str,
80
+ to_addr: str,
81
+ amount: float,
82
+ signature: str,
83
+ ) -> Tuple[bool, str, Optional[str]]:
84
+ """
85
+ Transfer PLWB between any two wallets.
86
+ Requires MetaMask signature from sender.
87
+ Network fee: 1 PLWB (split 50/50).
88
+ Returns (success, reason, tx_hash).
89
+ """
90
+ res = self._post("/api/transfer", {
91
+ "from_addr": from_addr.lower(),
92
+ "to_addr": to_addr.lower(),
93
+ "amount": amount,
94
+ "signature": signature,
95
+ })
96
+ success = res.get("success", False)
97
+ result = res.get("tx_hash") or res.get("error", "Unknown error")
98
+ return success, result, res.get("tx_hash")
99
+
100
+ # ─────────────────────────────────────────────────────────────
101
+ # Content Registry
102
+ # ─────────────────────────────────────────────────────────────
103
+
104
+ def register_content(
105
+ self,
106
+ cid: str,
107
+ owner: str,
108
+ editions: int = 1,
109
+ royalty_pct: float = 0,
110
+ signature: str = None,
111
+ platform_id: str = None,
112
+ extra_data: Dict = None,
113
+ ) -> Tuple[bool, str, Optional[str]]:
114
+ """
115
+ Register a CID on the PlayWebit Network.
116
+ Checks for duplicates automatically.
117
+ Returns (success, reason, tx_hash).
118
+ """
119
+ res = self._post("/api/transaction", {
120
+ "from_addr": owner.lower(),
121
+ "to_addr": owner.lower(),
122
+ "amount": 0,
123
+ "tx_type": "content_register",
124
+ "signature": signature,
125
+ "chain_id": CHAIN_ID,
126
+ "cid": cid,
127
+ "editions": editions,
128
+ "royalty_pct": royalty_pct,
129
+ "data": {
130
+ "platform_id": platform_id or "unknown",
131
+ **(extra_data or {}),
132
+ },
133
+ })
134
+ success = res.get("success", False)
135
+ result = res.get("result") or res.get("error", "Unknown error")
136
+ tx_hash = result if success else None
137
+ return success, result, tx_hash
138
+
139
+ def check_duplicate(self, cid: str) -> Dict:
140
+ """
141
+ Check if a CID is already registered anywhere on the network.
142
+ Returns { exists, first_owner, first_platform, first_seen, ... }
143
+ """
144
+ return self._get(f"/api/check_duplicate/{cid}")
145
+
146
+ def get_owner(self, cid: str) -> Optional[Dict]:
147
+ """Get current owner of a CID."""
148
+ res = self._get(f"/api/owner/{cid}")
149
+ return res if res.get("success") else None
150
+
151
+ def verify_ownership(self, cid: str, wallet: str) -> bool:
152
+ """Verify a wallet owns a CID."""
153
+ res = self._post("/api/verify_ownership", {
154
+ "cid": cid,
155
+ "wallet": wallet,
156
+ })
157
+ return res.get("owns", False)
158
+
159
+ def transfer_ownership(
160
+ self,
161
+ cid: str,
162
+ from_wallet: str,
163
+ to_wallet: str,
164
+ signature: str = None,
165
+ sale_price: float = 0,
166
+ platform_id: str = None,
167
+ ) -> Tuple[bool, str, Optional[str]]:
168
+ """
169
+ Transfer ownership of a CID.
170
+ If sale_price > 0, royalty is automatically paid to creator.
171
+ Returns (success, reason, tx_hash).
172
+ """
173
+ res = self._post("/api/transaction", {
174
+ "from_addr": from_wallet.lower(),
175
+ "to_addr": to_wallet.lower(),
176
+ "amount": sale_price,
177
+ "tx_type": "ownership_transfer",
178
+ "signature": signature,
179
+ "chain_id": CHAIN_ID,
180
+ "cid": cid,
181
+ "data": {
182
+ "platform_id": platform_id or "unknown",
183
+ "sale_price": sale_price,
184
+ "transfer_type": "sale" if sale_price > 0 else "gift",
185
+ },
186
+ })
187
+ success = res.get("success", False)
188
+ result = res.get("result") or res.get("error", "Unknown error")
189
+ return success, result, result if success else None
190
+
191
+ # ─────────────────────────────────────────────────────────────
192
+ # Edition Registry
193
+ # ─────────────────────────────────────────────────────────────
194
+
195
+ def get_editions(self, cid: str) -> Dict:
196
+ """Get all editions for a CID across all platforms."""
197
+ return self._get(f"/api/editions/{cid}")
198
+
199
+ def get_edition(self, cid: str, edition_number: int) -> Optional[Dict]:
200
+ """Get a specific edition."""
201
+ res = self._get(f"/api/editions/{cid}/{edition_number}")
202
+ return res.get("edition") if res.get("success") else None
203
+
204
+ def transfer_edition(
205
+ self,
206
+ cid: str,
207
+ edition_number: int,
208
+ from_wallet: str,
209
+ to_wallet: str,
210
+ signature: str = None,
211
+ sale_price: float = 0,
212
+ platform_id: str = None,
213
+ ) -> Tuple[bool, str, Optional[str]]:
214
+ """Transfer a specific edition to a new owner."""
215
+ res = self._post("/api/transaction", {
216
+ "from_addr": from_wallet.lower(),
217
+ "to_addr": to_wallet.lower(),
218
+ "amount": sale_price,
219
+ "tx_type": "edition_transfer",
220
+ "signature": signature,
221
+ "chain_id": CHAIN_ID,
222
+ "cid": cid,
223
+ "edition_number": edition_number,
224
+ "data": {
225
+ "platform_id": platform_id or "unknown",
226
+ "sale_price": sale_price,
227
+ "action": "transfer",
228
+ },
229
+ })
230
+ success = res.get("success", False)
231
+ result = res.get("result") or res.get("error", "Unknown error")
232
+ return success, result, result if success else None
233
+
234
+ # ─────────────────────────────────────────────────────────────
235
+ # SpiderWeave hash anchoring
236
+ # Called internally by spiderweave-sdk's PlayWebitAdapter
237
+ # ─────────────────────────────────────────────────────────────
238
+
239
+ def anchor_spider_hash(
240
+ self,
241
+ chain_name: str,
242
+ spider_hash: str,
243
+ event_type: str = "integrity_check",
244
+ platform_wallet: str = None,
245
+ signature: str = None,
246
+ metadata: Dict = None,
247
+ ) -> Tuple[bool, Optional[str]]:
248
+ """
249
+ Anchor a SpiderWeave hash on the chain.
250
+ L1 stores it, never interprets it.
251
+ Returns (success, tx_hash).
252
+ """
253
+ res = self._post("/api/anchor_spider_hash", {
254
+ "chain_id": chain_name,
255
+ "chain_name": chain_name,
256
+ "spider_hash": spider_hash,
257
+ "event_type": event_type,
258
+ "platform_wallet": platform_wallet or self.platform_wallet,
259
+ "signature": signature,
260
+ "metadata": metadata or {},
261
+ })
262
+ success = res.get("success", False)
263
+ tx_hash = res.get("tx_hash")
264
+ return success, tx_hash
265
+
266
+ def verify_spider_hash(self, tx_hash: str) -> Optional[Dict]:
267
+ """Verify a previously anchored spider hash by tx_hash."""
268
+ res = self._get(f"/api/transaction/{tx_hash}")
269
+ if not res.get("success"):
270
+ return None
271
+ tx = res.get("transaction", {})
272
+ if tx.get("tx_type") != "spider_hash_anchor":
273
+ return None
274
+ return {
275
+ "tx_hash": tx_hash,
276
+ "spider_hash": tx.get("spider_hash"),
277
+ "chain_name": tx.get("chain_name"),
278
+ "timestamp": tx.get("timestamp"),
279
+ "confirmed": True,
280
+ }
281
+
282
+ def get_spider_hashes(self, chain_name: str) -> Dict:
283
+ """Get all anchored hashes for a chain_name."""
284
+ return self._get(f"/api/spider_hashes/{chain_name}")
285
+
286
+ # ─────────────────────────────────────────────────────────────
287
+ # Payments
288
+ # ─────────────────────────────────────────────────────────────
289
+
290
+ def pay(
291
+ self,
292
+ from_addr: str,
293
+ to_addr: str,
294
+ amount: float,
295
+ signature: str,
296
+ ) -> Tuple[bool, str, Optional[str]]:
297
+ """
298
+ Send PLWB from one wallet to another.
299
+ Returns (success, reason, tx_hash).
300
+ """
301
+ res = self._post("/api/transaction", {
302
+ "from_addr": from_addr.lower(),
303
+ "to_addr": to_addr.lower(),
304
+ "amount": amount,
305
+ "tx_type": "transfer",
306
+ "signature": signature,
307
+ "chain_id": CHAIN_ID,
308
+ })
309
+ success = res.get("success", False)
310
+ result = res.get("result") or res.get("error", "Unknown error")
311
+ return success, result, result if success else None
312
+
313
+ def get_balance(self, address: str) -> float:
314
+ """Get PLWB balance for an address."""
315
+ res = self._get(f"/api/balance/{address}")
316
+ return float(res.get("balance", 0.0))
317
+
318
+ # ─────────────────────────────────────────────────────────────
319
+ # Network info
320
+ # ─────────────────────────────────────────────────────────────
321
+
322
+ def get_network_stats(self) -> Dict:
323
+ """Get network stats from the connected validator."""
324
+ return self._get("/api/network/stats")
325
+
326
+ def get_transaction(self, tx_hash: str) -> Optional[Dict]:
327
+ """Get a transaction by hash."""
328
+ res = self._get(f"/api/transaction/{tx_hash}")
329
+ return res.get("transaction") if res.get("success") else None
330
+
331
+ def health_check(self) -> bool:
332
+ """Check if the connected validator is online."""
333
+ res = self._get("/peer/health")
334
+ return res.get("status") == "ok"
335
+
336
+ def __repr__(self):
337
+ return (
338
+ f"PlayWebitClient("
339
+ f"validator={self.validator_url}, "
340
+ f"wallet={self.platform_wallet[:10]}...)"
341
+ )