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
@@ -0,0 +1,259 @@
1
+ """
2
+ PlayWebit Network — Content Registry
3
+ Cross-platform CID ownership. The core value of PlayWebit Network.
4
+
5
+ Any platform using IPFS CIDs gets duplicate detection for free —
6
+ IPFS CID format matches across all platforms automatically.
7
+
8
+ Register once → protected everywhere.
9
+ """
10
+
11
+ import time
12
+ import logging
13
+ from typing import Dict, Optional, Tuple
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ContentRegistry:
19
+
20
+ def __init__(self, blockchain, node_wallet: str):
21
+ self.blockchain = blockchain
22
+ self.node_wallet = node_wallet
23
+
24
+ # ─────────────────────────────────────────────────────────────
25
+ # Register content
26
+ # ─────────────────────────────────────────────────────────────
27
+
28
+ def register(
29
+ self,
30
+ cid: str,
31
+ owner_wallet: str,
32
+ platform_id: str,
33
+ editions: int = 1,
34
+ royalty_pct: float = 0,
35
+ signature: str = None,
36
+ extra_data: Dict = None,
37
+ ) -> Tuple[bool, str, Optional[str]]:
38
+ """
39
+ Register a CID on the network.
40
+ Checks for duplicates first — rejects if already registered
41
+ by ANY platform on the network.
42
+
43
+ Returns (success, reason, tx_hash).
44
+ tx_hash is None on failure.
45
+
46
+ IPFS CIDs match automatically — a file registered on MusicApp
47
+ cannot be registered again on ArtApp.
48
+ """
49
+
50
+ # Duplicate check — most important check
51
+ existing = self.check_duplicate(cid)
52
+ if existing["exists"]:
53
+ return (
54
+ False,
55
+ f"CID already registered by {existing['first_platform']} "
56
+ f"on {existing['first_seen_human']}. "
57
+ f"Owner: {existing['first_owner']}",
58
+ None,
59
+ )
60
+
61
+ # Validate inputs
62
+ if not cid:
63
+ return False, "CID is required", None
64
+ if not owner_wallet:
65
+ return False, "Owner wallet is required", None
66
+ if editions < 1:
67
+ return False, "Editions must be at least 1", None
68
+ if not (0 <= royalty_pct <= 100):
69
+ return False, "Royalty must be 0-100%", None
70
+
71
+ from playweb.core.transaction import Transaction
72
+
73
+ # Create the content_register transaction
74
+ tx = Transaction(
75
+ from_addr = owner_wallet.lower(),
76
+ to_addr = owner_wallet.lower(),
77
+ amount = 0,
78
+ tx_type = "content_register",
79
+ signature = signature,
80
+ cid = cid,
81
+ editions = editions,
82
+ royalty_pct = royalty_pct,
83
+ data = {
84
+ "platform_id": platform_id,
85
+ **(extra_data or {}),
86
+ },
87
+ )
88
+
89
+ # Submit to blockchain (also creates cv_link fee tx)
90
+ success, result = self.blockchain.add_transaction(
91
+ tx = tx,
92
+ node_wallet = self.node_wallet,
93
+ )
94
+
95
+ if not success:
96
+ return False, result, None
97
+
98
+ logger.info(
99
+ f"ContentRegistry: registered CID {cid[:16]}... "
100
+ f"by {platform_id} owner={owner_wallet[:8]}..."
101
+ )
102
+ return True, "Registered", tx.hash
103
+
104
+ # ─────────────────────────────────────────────────────────────
105
+ # Duplicate detection
106
+ # ─────────────────────────────────────────────────────────────
107
+
108
+ def check_duplicate(self, cid: str) -> Dict:
109
+ """
110
+ Check if a CID is already registered on the network.
111
+ Called by any platform before accepting a new upload.
112
+
113
+ Returns:
114
+ {
115
+ exists: bool,
116
+ first_owner: str or None,
117
+ first_platform: str or None,
118
+ first_seen: float or None,
119
+ first_seen_human: str or None,
120
+ current_owner: str or None,
121
+ tx_hash: str or None,
122
+ }
123
+ """
124
+ record = self.blockchain.storage.get_content_record(cid)
125
+
126
+ if not record:
127
+ return {
128
+ "exists": False,
129
+ "first_owner": None,
130
+ "first_platform": None,
131
+ "first_seen": None,
132
+ "first_seen_human": None,
133
+ "current_owner": None,
134
+ "tx_hash": None,
135
+ }
136
+
137
+ from datetime import datetime
138
+ first_seen = record.get("timestamp")
139
+ first_seen_human = (
140
+ datetime.fromtimestamp(first_seen).strftime("%Y-%m-%d %H:%M UTC")
141
+ if first_seen else None
142
+ )
143
+
144
+ return {
145
+ "exists": True,
146
+ "first_owner": record.get("first_owner"),
147
+ "first_platform": record.get("first_platform"),
148
+ "first_seen": first_seen,
149
+ "first_seen_human": first_seen_human,
150
+ "current_owner": record.get("current_owner"),
151
+ "tx_hash": record.get("first_tx_hash"),
152
+ }
153
+
154
+ # ─────────────────────────────────────────────────────────────
155
+ # Ownership queries
156
+ # ─────────────────────────────────────────────────────────────
157
+
158
+ def get_owner(self, cid: str) -> Optional[Dict]:
159
+ """
160
+ Get current owner of a CID.
161
+ Returns None if not registered.
162
+ """
163
+ record = self.blockchain.storage.get_content_record(cid)
164
+ if not record:
165
+ return None
166
+
167
+ return {
168
+ "cid": cid,
169
+ "current_owner": record.get("current_owner"),
170
+ "creator": record.get("creator_wallet"),
171
+ "platform": record.get("first_platform"),
172
+ "royalty_pct": record.get("royalty_pct", 0),
173
+ "total_editions": record.get("total_editions", 1),
174
+ }
175
+
176
+ def verify_ownership(self, cid: str, wallet: str) -> bool:
177
+ """
178
+ Verify that a wallet is the current owner of a CID.
179
+ Used by any platform to verify ownership claims.
180
+ """
181
+ record = self.blockchain.storage.get_content_record(cid)
182
+ if not record:
183
+ return False
184
+ return (
185
+ record.get("current_owner", "").lower() == wallet.lower()
186
+ )
187
+
188
+ # ─────────────────────────────────────────────────────────────
189
+ # Transfer ownership
190
+ # ─────────────────────────────────────────────────────────────
191
+
192
+ def transfer_ownership(
193
+ self,
194
+ cid: str,
195
+ from_wallet: str,
196
+ to_wallet: str,
197
+ signature: str = None,
198
+ platform_id: str = "unknown",
199
+ sale_price: float = 0,
200
+ ) -> Tuple[bool, str, Optional[str]]:
201
+ """
202
+ Transfer ownership of a CID from one wallet to another.
203
+ If sale_price > 0, royalty transactions are created automatically.
204
+ Returns (success, reason, tx_hash).
205
+ """
206
+
207
+ # Verify current ownership
208
+ if not self.verify_ownership(cid, from_wallet):
209
+ return False, f"{from_wallet[:8]}... does not own {cid[:16]}...", None
210
+
211
+ from playweb.core.transaction import Transaction
212
+
213
+ # If this is a sale with price, use royalty engine
214
+ if sale_price > 0:
215
+ from playweb.core.royalty_engine import RoyaltyEngine
216
+ royalty_engine = RoyaltyEngine(self.blockchain.storage)
217
+ txs = royalty_engine.create_royalty_transactions(
218
+ cid = cid,
219
+ sale_price = sale_price,
220
+ buyer_wallet = to_wallet,
221
+ seller_wallet= from_wallet,
222
+ )
223
+ for tx in txs:
224
+ tx.signature = signature
225
+ success, result = self.blockchain.add_transaction(
226
+ tx = tx,
227
+ node_wallet = self.node_wallet,
228
+ )
229
+ if not success:
230
+ return False, result, None
231
+
232
+ main_tx_hash = txs[0].hash if txs else None
233
+ else:
234
+ # Simple transfer (gift, internal move)
235
+ tx = Transaction(
236
+ from_addr = from_wallet.lower(),
237
+ to_addr = to_wallet.lower(),
238
+ amount = 0,
239
+ tx_type = "ownership_transfer",
240
+ signature = signature,
241
+ cid = cid,
242
+ data = {
243
+ "platform_id": platform_id,
244
+ "transfer_type": "gift",
245
+ },
246
+ )
247
+ success, result = self.blockchain.add_transaction(
248
+ tx = tx,
249
+ node_wallet = self.node_wallet,
250
+ )
251
+ if not success:
252
+ return False, result, None
253
+ main_tx_hash = tx.hash
254
+
255
+ logger.info(
256
+ f"ContentRegistry: transfer {cid[:16]}... "
257
+ f"from {from_wallet[:8]}... to {to_wallet[:8]}..."
258
+ )
259
+ return True, "Transfer queued", main_tx_hash
@@ -0,0 +1,220 @@
1
+ """
2
+ PlayWebit Network — Edition Registry
3
+ Cross-platform edition tracking.
4
+ Edition #3 of 100 on MusicApp is visible and verifiable on ArtApp.
5
+ """
6
+
7
+ import time
8
+ import logging
9
+ from typing import List, Dict, Optional, Tuple
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class EditionRegistry:
15
+
16
+ def __init__(self, blockchain, node_wallet: str):
17
+ self.blockchain = blockchain
18
+ self.node_wallet = node_wallet
19
+
20
+ # ─────────────────────────────────────────────────────────────
21
+ # Create editions
22
+ # ─────────────────────────────────────────────────────────────
23
+
24
+ def create_editions(
25
+ self,
26
+ cid: str,
27
+ total: int,
28
+ owner_wallet: str,
29
+ platform_id: str,
30
+ signature: str = None,
31
+ ) -> Tuple[bool, str]:
32
+ """
33
+ Create edition records for a registered CID.
34
+ Called after content_register when editions > 1.
35
+ Creates edition_transfer txs for each edition.
36
+ Returns (success, reason).
37
+ """
38
+ # Verify CID is registered
39
+ record = self.blockchain.storage.get_content_record(cid)
40
+ if not record:
41
+ return False, f"CID {cid[:16]}... not registered"
42
+
43
+ if record.get("current_owner", "").lower() != owner_wallet.lower():
44
+ return False, "Only the owner can create editions"
45
+
46
+ from playweb.core.transaction import Transaction
47
+
48
+ for edition_num in range(1, total + 1):
49
+ tx = Transaction(
50
+ from_addr = owner_wallet.lower(),
51
+ to_addr = owner_wallet.lower(),
52
+ amount = 0,
53
+ tx_type = "edition_transfer",
54
+ signature = signature,
55
+ cid = cid,
56
+ edition_number = edition_num,
57
+ data = {
58
+ "platform_id": platform_id,
59
+ "edition_of": total,
60
+ "action": "create",
61
+ },
62
+ )
63
+ success, result = self.blockchain.add_transaction(
64
+ tx = tx,
65
+ node_wallet = self.node_wallet,
66
+ )
67
+ if not success:
68
+ logger.warning(
69
+ f"EditionRegistry: failed to create edition "
70
+ f"{edition_num}: {result}"
71
+ )
72
+
73
+ logger.info(
74
+ f"EditionRegistry: created {total} editions for "
75
+ f"{cid[:16]}... on {platform_id}"
76
+ )
77
+ return True, f"Created {total} editions"
78
+
79
+ # ─────────────────────────────────────────────────────────────
80
+ # Transfer edition
81
+ # ─────────────────────────────────────────────────────────────
82
+
83
+ def transfer_edition(
84
+ self,
85
+ cid: str,
86
+ edition_number: int,
87
+ from_wallet: str,
88
+ to_wallet: str,
89
+ platform_id: str,
90
+ signature: str = None,
91
+ sale_price: float = 0,
92
+ ) -> Tuple[bool, str, Optional[str]]:
93
+ """
94
+ Transfer a specific edition to a new owner.
95
+ If sale_price > 0, royalty is paid to original creator.
96
+ Returns (success, reason, tx_hash).
97
+ """
98
+
99
+ # Verify edition ownership
100
+ if not self.verify_edition_owner(cid, edition_number, from_wallet):
101
+ return (
102
+ False,
103
+ f"{from_wallet[:8]}... does not own edition "
104
+ f"#{edition_number} of {cid[:16]}...",
105
+ None,
106
+ )
107
+
108
+ from playweb.core.transaction import Transaction
109
+
110
+ # Create the edition transfer tx
111
+ tx = Transaction(
112
+ from_addr = from_wallet.lower(),
113
+ to_addr = to_wallet.lower(),
114
+ amount = sale_price,
115
+ tx_type = "edition_transfer",
116
+ signature = signature,
117
+ cid = cid,
118
+ edition_number = edition_number,
119
+ data = {
120
+ "platform_id": platform_id,
121
+ "sale_price": sale_price,
122
+ "action": "transfer",
123
+ },
124
+ )
125
+
126
+ success, result = self.blockchain.add_transaction(
127
+ tx = tx,
128
+ node_wallet = self.node_wallet,
129
+ )
130
+ if not success:
131
+ return False, result, None
132
+
133
+ # If sale, create royalty transactions
134
+ if sale_price > 0:
135
+ from playweb.core.royalty_engine import RoyaltyEngine
136
+ royalty_engine = RoyaltyEngine(self.blockchain.storage)
137
+ royalty_txs = royalty_engine.create_royalty_transactions(
138
+ cid = cid,
139
+ sale_price = sale_price,
140
+ buyer_wallet = to_wallet,
141
+ seller_wallet = from_wallet,
142
+ )
143
+ for rtx in royalty_txs:
144
+ rtx.signature = signature
145
+ self.blockchain.add_transaction(
146
+ tx = rtx,
147
+ node_wallet = self.node_wallet,
148
+ )
149
+
150
+ logger.info(
151
+ f"EditionRegistry: transfer edition #{edition_number} "
152
+ f"of {cid[:16]}... "
153
+ f"from {from_wallet[:8]}... to {to_wallet[:8]}..."
154
+ )
155
+ return True, "Edition transfer queued", tx.hash
156
+
157
+ # ─────────────────────────────────────────────────────────────
158
+ # Queries
159
+ # ─────────────────────────────────────────────────────────────
160
+
161
+ def get_edition(self, cid: str, edition_number: int) -> Optional[Dict]:
162
+ """
163
+ Get a specific edition record.
164
+ Returns None if not found.
165
+ """
166
+ return self.blockchain.storage.get_edition_record(cid, edition_number)
167
+
168
+ def get_all_editions(self, cid: str) -> List[Dict]:
169
+ """
170
+ Get all editions for a CID across all platforms.
171
+ Any platform can see all editions globally.
172
+ """
173
+ return self.blockchain.storage.get_all_edition_records(cid)
174
+
175
+ def verify_edition_owner(
176
+ self,
177
+ cid: str,
178
+ edition_number: int,
179
+ wallet: str,
180
+ ) -> bool:
181
+ """
182
+ Verify that a wallet owns a specific edition.
183
+ Used by any platform to verify edition ownership claims.
184
+ """
185
+ record = self.blockchain.storage.get_edition_record(cid, edition_number)
186
+ if not record:
187
+ # If no edition record, check content record (edition 1 = content owner)
188
+ if edition_number == 1:
189
+ content = self.blockchain.storage.get_content_record(cid)
190
+ if content:
191
+ return (
192
+ content.get("current_owner", "").lower()
193
+ == wallet.lower()
194
+ )
195
+ return False
196
+
197
+ return record.get("current_owner", "").lower() == wallet.lower()
198
+
199
+ def get_edition_summary(self, cid: str) -> Dict:
200
+ """
201
+ Get a summary of all editions for a CID.
202
+ Useful for marketplace displays.
203
+ """
204
+ content = self.blockchain.storage.get_content_record(cid)
205
+ if not content:
206
+ return {"cid": cid, "found": False}
207
+
208
+ editions = self.get_all_editions(cid)
209
+ total = content.get("total_editions", 1)
210
+
211
+ return {
212
+ "cid": cid,
213
+ "found": True,
214
+ "total_editions": total,
215
+ "editions_found": len(editions),
216
+ "creator": content.get("creator_wallet"),
217
+ "royalty_pct": content.get("royalty_pct", 0),
218
+ "first_platform": content.get("first_platform"),
219
+ "editions": editions,
220
+ }
@@ -0,0 +1,11 @@
1
+ from playweb.storage.base import ChainStorage
2
+ from playweb.storage.ram_storage import RAMStorage
3
+ from playweb.storage.sqlite_storage import SQLiteStorage
4
+ from playweb.storage.supabase_storage import SupabaseStorage
5
+
6
+ __all__ = [
7
+ "ChainStorage",
8
+ "RAMStorage",
9
+ "SQLiteStorage",
10
+ "SupabaseStorage",
11
+ ]
@@ -0,0 +1,154 @@
1
+ """
2
+ PlayWebit Network — ChainStorage Interface
3
+ Any storage backend implements this.
4
+ Blockchain core only calls these methods — never touches DB directly.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import List, Optional, Dict
9
+
10
+
11
+ class ChainStorage(ABC):
12
+
13
+ # ─────────────────────────────────────────────────────────────
14
+ # Blocks
15
+ # ─────────────────────────────────────────────────────────────
16
+
17
+ @abstractmethod
18
+ def save_block(self, block) -> bool:
19
+ """Save a block to storage. Returns True on success."""
20
+ ...
21
+
22
+ @abstractmethod
23
+ def get_block(self, block_hash: str):
24
+ """Get a block by hash. Returns Block or None."""
25
+ ...
26
+
27
+ @abstractmethod
28
+ def get_block_by_index(self, index: int):
29
+ """Get a block by index. Returns Block or None."""
30
+ ...
31
+
32
+ @abstractmethod
33
+ def get_chain_tip(self):
34
+ """Get the latest block. Returns Block or None."""
35
+ ...
36
+
37
+ @abstractmethod
38
+ def get_chain_length(self) -> int:
39
+ """Get total number of blocks in the chain."""
40
+ ...
41
+
42
+ @abstractmethod
43
+ def get_blocks_from(self, from_index: int, limit: int = 50) -> List:
44
+ """
45
+ Get blocks from from_index up to limit.
46
+ Used for chain sync between nodes.
47
+ Returns List[Block].
48
+ """
49
+ ...
50
+
51
+ # ─────────────────────────────────────────────────────────────
52
+ # Transactions
53
+ # ─────────────────────────────────────────────────────────────
54
+
55
+ @abstractmethod
56
+ def save_transaction(self, tx) -> bool:
57
+ """Save a confirmed transaction. Returns True on success."""
58
+ ...
59
+
60
+ @abstractmethod
61
+ def get_transaction(self, tx_hash: str):
62
+ """Get a transaction by hash. Returns Transaction or None."""
63
+ ...
64
+
65
+ # ─────────────────────────────────────────────────────────────
66
+ # Balances
67
+ # ─────────────────────────────────────────────────────────────
68
+
69
+ @abstractmethod
70
+ def get_balance(self, address: str) -> float:
71
+ """Get PLWB balance for an address. Returns 0.0 if not found."""
72
+ ...
73
+
74
+ @abstractmethod
75
+ def save_balance(self, address: str, balance: float) -> bool:
76
+ """Update balance for an address. Returns True on success."""
77
+ ...
78
+
79
+ @abstractmethod
80
+ def get_all_addresses(self) -> List[str]:
81
+ """Get all addresses that have ever had a balance."""
82
+ ...
83
+
84
+ # ─────────────────────────────────────────────────────────────
85
+ # Content Registry
86
+ # Cross-platform CID ownership — the core value of the network
87
+ # ─────────────────────────────────────────────────────────────
88
+
89
+ @staticmethod
90
+ def normalise_record(record: Dict) -> Dict:
91
+ """Normalise wallet addresses to lowercase before saving."""
92
+ normalised = dict(record)
93
+ for key in ("creator_wallet", "first_owner", "current_owner"):
94
+ if normalised.get(key):
95
+ normalised[key] = normalised[key].lower()
96
+ return normalised
97
+
98
+ @abstractmethod
99
+ def save_content_record(self, record: Dict) -> bool:
100
+ record = self.normalise_record(record)
101
+ """
102
+ Save a content registration record.
103
+ record keys:
104
+ cid, creator_wallet, first_owner, first_platform,
105
+ first_tx_hash, first_block, timestamp,
106
+ total_editions, royalty_pct, current_owner
107
+ """
108
+ ...
109
+
110
+ @abstractmethod
111
+ def get_content_record(self, cid: str) -> Optional[Dict]:
112
+ """Get content record by CID. Returns dict or None."""
113
+ ...
114
+
115
+ # ─────────────────────────────────────────────────────────────
116
+ # Edition Registry
117
+ # ─────────────────────────────────────────────────────────────
118
+
119
+ @abstractmethod
120
+ def save_edition_record(self, record: Dict) -> bool:
121
+ """
122
+ Save an edition record.
123
+ record keys:
124
+ cid, edition_number, edition_of, current_owner,
125
+ platform, tx_hash, timestamp, provenance (list)
126
+ """
127
+ ...
128
+
129
+ @abstractmethod
130
+ def get_edition_record(self, cid: str, edition_number: int) -> Optional[Dict]:
131
+ """Get a specific edition record."""
132
+ ...
133
+
134
+ @abstractmethod
135
+ def get_all_edition_records(self, cid: str) -> List[Dict]:
136
+ """Get all edition records for a CID."""
137
+ ...
138
+
139
+ @abstractmethod
140
+ def get_all_content_by_owner(self, address: str) -> List[Dict]:
141
+ """
142
+ Get all content records where current_owner = address.
143
+ Used by ERC721 balanceOf() to count NFTs.
144
+ """
145
+ ...
146
+
147
+ @abstractmethod
148
+ def get_cid_by_int_id(self, int_id: int) -> Optional[str]:
149
+ """
150
+ Get CID string from integer token ID.
151
+ Integer IDs are used by MetaMask (uint256).
152
+ Computed as: sha256(cid)[:16] as int.
153
+ """
154
+ ...