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/core/block.py ADDED
@@ -0,0 +1,156 @@
1
+ """
2
+ PlayWebit Network — Block
3
+ """
4
+
5
+ import hashlib
6
+ import json
7
+ import time
8
+ import logging
9
+ from typing import List, Dict, Optional
10
+
11
+ from playweb.core.transaction import Transaction
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Block:
17
+
18
+ def __init__(
19
+ self,
20
+ index: int,
21
+ transactions: List[Transaction],
22
+ previous_hash: str,
23
+ validator_wallet: str,
24
+ timestamp: float = None,
25
+ nonce: int = 0,
26
+ consensus_round: int = 0,
27
+ votes: List = None,
28
+ ):
29
+ self.index = index
30
+ self.transactions = transactions
31
+ self.previous_hash = previous_hash
32
+ self.validator_wallet = validator_wallet.lower()
33
+ self.timestamp = timestamp or time.time()
34
+ self.nonce = nonce
35
+ self.consensus_round = consensus_round
36
+ self.votes = votes or []
37
+
38
+ self.merkle_root = self.calculate_merkle_root()
39
+ self.hash = self.calculate_hash()
40
+
41
+ # ─────────────────────────────────────────────────────────────
42
+ # Merkle Tree
43
+ # ─────────────────────────────────────────────────────────────
44
+
45
+ def calculate_merkle_root(self) -> str:
46
+ """Build merkle root from transaction hashes."""
47
+ if not self.transactions:
48
+ return hashlib.sha256(b"empty").hexdigest()
49
+
50
+ hashes = [tx.hash for tx in self.transactions]
51
+
52
+ while len(hashes) > 1:
53
+ # Pad odd number of hashes
54
+ if len(hashes) % 2 != 0:
55
+ hashes.append(hashes[-1])
56
+
57
+ hashes = [
58
+ hashlib.sha256(
59
+ (hashes[i] + hashes[i + 1]).encode()
60
+ ).hexdigest()
61
+ for i in range(0, len(hashes), 2)
62
+ ]
63
+
64
+ return hashes[0]
65
+
66
+ # ─────────────────────────────────────────────────────────────
67
+ # Hashing
68
+ # ─────────────────────────────────────────────────────────────
69
+
70
+ def calculate_hash(self) -> str:
71
+ block_data = {
72
+ "index": self.index,
73
+ "previous_hash": self.previous_hash,
74
+ "merkle_root": self.merkle_root,
75
+ "timestamp": self.timestamp,
76
+ "nonce": self.nonce,
77
+ "validator_wallet": self.validator_wallet,
78
+ "consensus_round": self.consensus_round,
79
+ }
80
+ raw = json.dumps(block_data, sort_keys=True)
81
+ return hashlib.sha256(raw.encode()).hexdigest()
82
+
83
+ # ─────────────────────────────────────────────────────────────
84
+ # Validation
85
+ # ─────────────────────────────────────────────────────────────
86
+
87
+ def validate(self, previous_block: Optional["Block"] = None) -> tuple[bool, str]:
88
+ """
89
+ Validate block integrity.
90
+ Returns (is_valid, reason).
91
+ """
92
+ # Hash integrity
93
+ if self.hash != self.calculate_hash():
94
+ return False, "Block hash mismatch"
95
+
96
+ # Merkle root integrity
97
+ if self.merkle_root != self.calculate_merkle_root():
98
+ return False, "Merkle root mismatch"
99
+
100
+ # Chain linkage
101
+ if previous_block:
102
+ if self.previous_hash != previous_block.hash:
103
+ return False, "Previous hash does not match"
104
+ if self.index != previous_block.index + 1:
105
+ return False, f"Block index gap: {previous_block.index} → {self.index}"
106
+
107
+ # Validate each transaction
108
+ for tx in self.transactions:
109
+ valid, reason = tx.validate_fields()
110
+ if not valid:
111
+ return False, f"Invalid tx {tx.hash[:12]}: {reason}"
112
+
113
+ return True, "Valid"
114
+
115
+ # ─────────────────────────────────────────────────────────────
116
+ # Serialisation
117
+ # ─────────────────────────────────────────────────────────────
118
+
119
+ def to_dict(self) -> Dict:
120
+ return {
121
+ "index": self.index,
122
+ "hash": self.hash,
123
+ "previous_hash": self.previous_hash,
124
+ "merkle_root": self.merkle_root,
125
+ "timestamp": self.timestamp,
126
+ "nonce": self.nonce,
127
+ "validator_wallet": self.validator_wallet,
128
+ "consensus_round": self.consensus_round,
129
+ "votes": self.votes,
130
+ "transactions": [tx.to_dict() for tx in self.transactions],
131
+ }
132
+
133
+ @classmethod
134
+ def from_dict(cls, d: Dict) -> "Block":
135
+ txs = [Transaction.from_dict(t) for t in d.get("transactions", [])]
136
+ block = cls(
137
+ index = d["index"],
138
+ transactions = txs,
139
+ previous_hash = d["previous_hash"],
140
+ validator_wallet = d["validator_wallet"],
141
+ timestamp = d["timestamp"],
142
+ nonce = d.get("nonce", 0),
143
+ consensus_round = d.get("consensus_round", 0),
144
+ votes = d.get("votes", []),
145
+ )
146
+ # Restore stored hash
147
+ block.hash = d["hash"]
148
+ block.merkle_root = d["merkle_root"]
149
+ return block
150
+
151
+ def __repr__(self):
152
+ return (
153
+ f"Block(index={self.index}, "
154
+ f"txs={len(self.transactions)}, "
155
+ f"hash={self.hash[:12]}...)"
156
+ )
@@ -0,0 +1,358 @@
1
+ """
2
+ PlayWebit Network — Blockchain
3
+ Storage-agnostic. No Supabase imports. No CipherVault logic.
4
+ Works with any ChainStorage implementation.
5
+ """
6
+
7
+ import time
8
+ import logging
9
+ from typing import List, Optional, Tuple, Dict
10
+
11
+ from playweb.core.block import Block
12
+ from playweb.core.transaction import Transaction
13
+ from playweb.core.mempool import Mempool
14
+ from playweb.core.fee_engine import FeeEngine
15
+ from playweb.config import (
16
+ CHAIN_ID,
17
+ AUTHORITY_WALLET,
18
+ AUTHORITY_TX_TYPES,
19
+ L1_TX_TYPES,
20
+ MAX_TX_PER_BLOCK,
21
+ TRANSACTION_FEE,
22
+ CV_LINK_FEE,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ class Blockchain:
29
+
30
+ def __init__(self, storage):
31
+ """
32
+ storage — any ChainStorage implementation.
33
+ LevelDBStorage, SQLiteStorage, SupabaseStorage, RAMStorage.
34
+ """
35
+ self.storage = storage
36
+ self.mempool = Mempool()
37
+ self.fee_engine = FeeEngine()
38
+ self._ensure_genesis()
39
+
40
+ # ─────────────────────────────────────────────────────────────
41
+ # Genesis
42
+ # ─────────────────────────────────────────────────────────────
43
+
44
+ def _ensure_genesis(self):
45
+ """Create genesis block if chain is empty."""
46
+ if self.storage.get_chain_length() == 0:
47
+ logger.info("Chain is empty — creating genesis block")
48
+ genesis_tx = Transaction(
49
+ from_addr = AUTHORITY_WALLET,
50
+ to_addr = AUTHORITY_WALLET,
51
+ amount = 0,
52
+ tx_type = "genesis",
53
+ nonce = 0,
54
+ timestamp = 0,
55
+ data = {
56
+ "chain_id": CHAIN_ID,
57
+ "message": "PlayWebit Network Genesis",
58
+ "created_at": time.time(),
59
+ }
60
+ )
61
+ genesis_block = Block(
62
+ index = 0,
63
+ transactions = [genesis_tx],
64
+ previous_hash = "0" * 64,
65
+ validator_wallet = AUTHORITY_WALLET,
66
+ timestamp = 0,
67
+ )
68
+ self.storage.save_block(genesis_block)
69
+ logger.info(f"Genesis block created: {genesis_block.hash[:16]}...")
70
+
71
+ # ─────────────────────────────────────────────────────────────
72
+ # Transactions
73
+ # ─────────────────────────────────────────────────────────────
74
+
75
+ def add_transaction(
76
+ self,
77
+ tx: Transaction,
78
+ node_wallet: str = None,
79
+ ) -> Tuple[bool, str]:
80
+ """
81
+ Validate and add a transaction to the mempool.
82
+ Also creates and queues the required fee transactions.
83
+ Returns (success, reason).
84
+ """
85
+ # Field validation
86
+ valid, reason = tx.validate_fields()
87
+ if not valid:
88
+ return False, reason
89
+
90
+ # Signature verification (skipped for authority txs)
91
+ if tx.tx_type not in AUTHORITY_TX_TYPES:
92
+ if not tx.verify_signature():
93
+ return False, "Invalid signature"
94
+
95
+ # Balance check (skip for authority txs)
96
+ if tx.tx_type not in AUTHORITY_TX_TYPES and tx.amount > 0:
97
+ fee_info = self.fee_engine.calculate_fee(tx.tx_type, tx.amount)
98
+ total_needed = tx.amount + fee_info["total"]
99
+ balance = self.get_balance(tx.from_addr)
100
+
101
+ if balance < total_needed:
102
+ return (
103
+ False,
104
+ f"Insufficient balance: need {total_needed} PLWB, "
105
+ f"have {balance} PLWB"
106
+ )
107
+
108
+ # Duplicate check
109
+ if self.mempool.contains(tx.hash):
110
+ return False, "Transaction already in mempool"
111
+
112
+ if self.storage.get_transaction(tx.hash):
113
+ return False, "Transaction already on chain"
114
+
115
+ # Add to mempool
116
+ success, reason = self.mempool.add(tx)
117
+ if not success:
118
+ return False, reason
119
+
120
+ # Create and queue fee transactions
121
+ if node_wallet and tx.tx_type not in AUTHORITY_TX_TYPES:
122
+ fee_txs = self.fee_engine.create_fee_transactions(tx, node_wallet)
123
+ for fee_tx in fee_txs:
124
+ self.mempool.add(fee_tx)
125
+
126
+ logger.info(f"Transaction added to mempool: {tx.hash[:16]} ({tx.tx_type})")
127
+ return True, tx.hash
128
+
129
+ def get_transaction(self, tx_hash: str) -> Optional[Transaction]:
130
+ """Get a transaction by hash — checks mempool first, then chain."""
131
+ # Check mempool first
132
+ tx = self.mempool.get(tx_hash)
133
+ if tx:
134
+ return tx
135
+ # Then check storage
136
+ return self.storage.get_transaction(tx_hash)
137
+
138
+ def validate_transaction(
139
+ self,
140
+ tx: Transaction,
141
+ node_wallet: str = None,
142
+ ) -> Tuple[bool, str]:
143
+ """
144
+ Full transaction validation.
145
+ Used during consensus when receiving blocks from peers.
146
+ """
147
+ valid, reason = tx.validate_fields()
148
+ if not valid:
149
+ return False, reason
150
+
151
+ if tx.tx_type not in AUTHORITY_TX_TYPES:
152
+ if not tx.verify_signature():
153
+ return False, "Invalid signature"
154
+
155
+ return True, "Valid"
156
+
157
+ # ─────────────────────────────────────────────────────────────
158
+ # Blocks
159
+ # ─────────────────────────────────────────────────────────────
160
+
161
+ def create_block(self, validator_wallet: str) -> Optional[Block]:
162
+ """
163
+ Create a new block from pending mempool transactions.
164
+ Called by the consensus leader before proposing.
165
+ """
166
+ pending = self.mempool.get_pending(MAX_TX_PER_BLOCK)
167
+ if not pending:
168
+ logger.debug("No pending transactions — skipping block creation")
169
+ return None
170
+
171
+ tip = self.storage.get_chain_tip()
172
+ if not tip:
173
+ logger.error("No chain tip found — chain may be corrupt")
174
+ return None
175
+
176
+ block = Block(
177
+ index = tip.index + 1,
178
+ transactions = pending,
179
+ previous_hash = tip.hash,
180
+ validator_wallet = validator_wallet,
181
+ )
182
+
183
+ logger.info(
184
+ f"Block created: index={block.index}, "
185
+ f"txs={len(pending)}, "
186
+ f"hash={block.hash[:16]}..."
187
+ )
188
+ return block
189
+
190
+ def add_block(
191
+ self,
192
+ block: Block,
193
+ votes: List = None,
194
+ node_wallet: str = None,
195
+ ) -> Tuple[bool, str]:
196
+ """
197
+ Add a finalised block to the chain.
198
+ Called after consensus (2/3 votes received).
199
+ Validates block then writes to storage.
200
+ """
201
+ # Get previous block for chain validation
202
+ tip = self.storage.get_chain_tip()
203
+
204
+ # Validate block integrity
205
+ valid, reason = block.validate(previous_block=tip)
206
+ if not valid:
207
+ return False, f"Block validation failed: {reason}"
208
+
209
+ # Validate fee transactions (50/50 split enforced)
210
+ if node_wallet:
211
+ fee_valid, fee_reason = self.fee_engine.validate_fee_transactions(
212
+ block, node_wallet
213
+ )
214
+ if not fee_valid:
215
+ return False, f"Fee validation failed: {fee_reason}"
216
+
217
+ # Attach votes
218
+ if votes:
219
+ block.votes = votes
220
+
221
+ # Save to storage
222
+ self.storage.save_block(block)
223
+
224
+ # Update balances
225
+ self._apply_block_to_balances(block)
226
+
227
+ # Update content registry for content_register transactions
228
+ self._apply_content_registry(block)
229
+
230
+ # Remove confirmed transactions from mempool
231
+ self.mempool.remove_batch([tx.hash for tx in block.transactions])
232
+
233
+ logger.info(
234
+ f"Block finalised: index={block.index}, "
235
+ f"txs={len(block.transactions)}, "
236
+ f"hash={block.hash[:16]}..."
237
+ )
238
+ return True, block.hash
239
+
240
+ def _apply_block_to_balances(self, block: Block):
241
+ """Update balance records for all transactions in a block."""
242
+ for tx in block.transactions:
243
+ if tx.amount <= 0:
244
+ continue
245
+
246
+ # Deduct from sender (not for authority/genesis txs)
247
+ if tx.tx_type not in AUTHORITY_TX_TYPES:
248
+ from_bal = self.storage.get_balance(tx.from_addr)
249
+ self.storage.save_balance(tx.from_addr, from_bal - tx.amount)
250
+
251
+ # Credit receiver
252
+ to_bal = self.storage.get_balance(tx.to_addr)
253
+ self.storage.save_balance(tx.to_addr, to_bal + tx.amount)
254
+
255
+ # Save transaction record
256
+ self.storage.save_transaction(tx)
257
+
258
+ def _apply_content_registry(self, block: Block):
259
+ """Register CIDs and editions from content_register transactions."""
260
+ for tx in block.transactions:
261
+ if tx.tx_type == "content_register" and tx.cid:
262
+ record = {
263
+ "cid": tx.cid,
264
+ "creator_wallet": tx.from_addr,
265
+ "first_owner": tx.to_addr,
266
+ "first_platform": tx.data.get("platform_id", "unknown"),
267
+ "first_tx_hash": tx.hash,
268
+ "first_block": block.index,
269
+ "timestamp": tx.timestamp,
270
+ "total_editions": tx.editions or 1,
271
+ "royalty_pct": tx.royalty_pct or 0,
272
+ }
273
+ self.storage.save_content_record(record)
274
+
275
+ elif tx.tx_type == "ownership_transfer" and tx.cid:
276
+ # Update current owner
277
+ existing = self.storage.get_content_record(tx.cid)
278
+ if existing:
279
+ existing["current_owner"] = tx.to_addr
280
+ self.storage.save_content_record(existing)
281
+
282
+ elif tx.tx_type == "edition_transfer" and tx.cid:
283
+ edition_record = {
284
+ "cid": tx.cid,
285
+ "edition_number": tx.edition_number,
286
+ "current_owner": tx.to_addr,
287
+ "platform": tx.data.get("platform_id", "unknown"),
288
+ "tx_hash": tx.hash,
289
+ "timestamp": tx.timestamp,
290
+ }
291
+ self.storage.save_edition_record(edition_record)
292
+
293
+ # ─────────────────────────────────────────────────────────────
294
+ # Balances
295
+ # ─────────────────────────────────────────────────────────────
296
+
297
+ def get_balance(self, address: str) -> float:
298
+ """Get PLWB balance for an address."""
299
+ return self.storage.get_balance(address.lower())
300
+
301
+ # ─────────────────────────────────────────────────────────────
302
+ # Chain queries
303
+ # ─────────────────────────────────────────────────────────────
304
+
305
+ def get_block(self, block_hash: str) -> Optional[Block]:
306
+ return self.storage.get_block(block_hash)
307
+
308
+ def get_block_by_index(self, index: int) -> Optional[Block]:
309
+ return self.storage.get_block_by_index(index)
310
+
311
+ def get_chain_tip(self) -> Optional[Block]:
312
+ return self.storage.get_chain_tip()
313
+
314
+ def get_chain_length(self) -> int:
315
+ return self.storage.get_chain_length()
316
+
317
+ def get_pending_transactions(self) -> List[Transaction]:
318
+ return self.mempool.get_pending()
319
+
320
+ def get_blocks_from(self, from_index: int, limit: int = 50) -> List[Block]:
321
+ """Get a range of blocks — used for chain sync."""
322
+ return self.storage.get_blocks_from(from_index, limit)
323
+
324
+ # ─────────────────────────────────────────────────────────────
325
+ # Chain validation
326
+ # ─────────────────────────────────────────────────────────────
327
+
328
+ def validate_chain(self) -> Tuple[bool, str]:
329
+ """
330
+ Validate entire chain integrity.
331
+ Used during sync to verify downloaded chain from peers.
332
+ """
333
+ length = self.storage.get_chain_length()
334
+ if length == 0:
335
+ return False, "Empty chain"
336
+
337
+ previous_block = None
338
+ for i in range(length):
339
+ block = self.storage.get_block_by_index(i)
340
+ if not block:
341
+ return False, f"Missing block at index {i}"
342
+
343
+ valid, reason = block.validate(previous_block)
344
+ if not valid:
345
+ return False, f"Block {i} invalid: {reason}"
346
+
347
+ previous_block = block
348
+
349
+ return True, f"Chain valid ({length} blocks)"
350
+
351
+ def get_stats(self) -> Dict:
352
+ tip = self.get_chain_tip()
353
+ return {
354
+ "chain_length": self.get_chain_length(),
355
+ "chain_tip": tip.hash if tip else None,
356
+ "pending_tx_count": self.mempool.size(),
357
+ "chain_id": CHAIN_ID,
358
+ }