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,389 @@
1
+ """
2
+ PlayWebit Network — Supabase Storage
3
+ For nodes running on HuggingFace or anywhere without persistent disk.
4
+ Uses Supabase as the persistent layer.
5
+
6
+ Required Supabase tables (run this SQL in your Supabase SQL editor):
7
+
8
+ CREATE TABLE IF NOT EXISTS pw_blocks (
9
+ hash TEXT PRIMARY KEY,
10
+ idx INTEGER UNIQUE,
11
+ previous_hash TEXT,
12
+ merkle_root TEXT,
13
+ timestamp FLOAT,
14
+ nonce INTEGER DEFAULT 0,
15
+ validator_wallet TEXT,
16
+ consensus_round INTEGER DEFAULT 0,
17
+ votes JSONB DEFAULT '[]',
18
+ transactions JSONB DEFAULT '[]'
19
+ );
20
+
21
+ CREATE TABLE IF NOT EXISTS pw_transactions (
22
+ hash TEXT PRIMARY KEY,
23
+ data JSONB
24
+ );
25
+
26
+ CREATE TABLE IF NOT EXISTS pw_balances (
27
+ address TEXT PRIMARY KEY,
28
+ balance FLOAT DEFAULT 0.0,
29
+ updated FLOAT
30
+ );
31
+
32
+ CREATE TABLE IF NOT EXISTS pw_content_registry (
33
+ cid TEXT PRIMARY KEY,
34
+ creator_wallet TEXT,
35
+ first_owner TEXT,
36
+ current_owner TEXT,
37
+ first_platform TEXT,
38
+ first_tx_hash TEXT,
39
+ first_block INTEGER,
40
+ timestamp FLOAT,
41
+ total_editions INTEGER DEFAULT 1,
42
+ royalty_pct FLOAT DEFAULT 0,
43
+ extra JSONB DEFAULT '{}'
44
+ );
45
+
46
+ CREATE TABLE IF NOT EXISTS pw_edition_registry (
47
+ cid TEXT,
48
+ edition_number INTEGER,
49
+ edition_of INTEGER DEFAULT 1,
50
+ current_owner TEXT,
51
+ platform TEXT,
52
+ tx_hash TEXT,
53
+ timestamp FLOAT,
54
+ provenance JSONB DEFAULT '[]',
55
+ PRIMARY KEY (cid, edition_number)
56
+ );
57
+ """
58
+
59
+ import json
60
+ import time
61
+ import logging
62
+ from typing import List, Optional, Dict
63
+
64
+ from playweb.storage.base import ChainStorage
65
+
66
+ logger = logging.getLogger(__name__)
67
+
68
+
69
+ class SupabaseStorage(ChainStorage):
70
+
71
+ def __init__(self, url: str, key: str):
72
+ try:
73
+ from supabase import create_client, ClientOptions
74
+ import httpx
75
+
76
+ # HTTP/2 is the supabase-py default, but idle HTTP/2 streams get
77
+ # cleanly terminated by Supabase's edge on hosts like HF Spaces,
78
+ # surfacing as ConnectionTerminated / "Server disconnected" on
79
+ # the next reused request. Force HTTP/1.1 and recycle idle
80
+ # connections before the remote does it for us.
81
+ httpx_client = httpx.Client(
82
+ http2=False,
83
+ timeout=httpx.Timeout(connect=10.0, read=30.0, write=30.0, pool=10.0),
84
+ limits=httpx.Limits(
85
+ max_connections=20,
86
+ max_keepalive_connections=5,
87
+ keepalive_expiry=15.0,
88
+ ),
89
+ )
90
+ options = ClientOptions(httpx_client=httpx_client)
91
+ self.sb = create_client(url, key, options=options)
92
+ logger.info("SupabaseStorage initialised (HTTP/1.1, keepalive_expiry=15s)")
93
+ except ImportError:
94
+ raise ImportError(
95
+ "supabase package not installed. "
96
+ "Run: pip install supabase"
97
+ )
98
+
99
+ def _t(self, name: str):
100
+ """Shorthand for table access."""
101
+ return self.sb.table(name)
102
+
103
+ # ─── Blocks ──────────────────────────────────────────────────
104
+
105
+ def save_block(self, block) -> bool:
106
+ try:
107
+ data = {
108
+ "hash": block.hash,
109
+ "idx": block.index,
110
+ "previous_hash": block.previous_hash,
111
+ "merkle_root": block.merkle_root,
112
+ "timestamp": block.timestamp,
113
+ "nonce": block.nonce,
114
+ "validator_wallet": block.validator_wallet,
115
+ "consensus_round": block.consensus_round,
116
+ "votes": block.votes,
117
+ "transactions": [tx.to_dict() for tx in block.transactions],
118
+ }
119
+ self._t("pw_blocks").upsert(data).execute()
120
+ return True
121
+ except Exception as e:
122
+ logger.error(f"save_block error: {e}")
123
+ return False
124
+
125
+ def get_block(self, block_hash: str):
126
+ try:
127
+ res = self._t("pw_blocks").select("*").eq("hash", block_hash).execute()
128
+ return self._row_to_block(res.data[0]) if res.data else None
129
+ except Exception as e:
130
+ logger.error(f"get_block error: {e}")
131
+ return None
132
+
133
+ def get_block_by_index(self, index: int):
134
+ try:
135
+ res = self._t("pw_blocks").select("*").eq("idx", index).execute()
136
+ return self._row_to_block(res.data[0]) if res.data else None
137
+ except Exception as e:
138
+ logger.error(f"get_block_by_index error: {e}")
139
+ return None
140
+
141
+ def get_chain_tip(self):
142
+ try:
143
+ res = (
144
+ self._t("pw_blocks")
145
+ .select("*")
146
+ .order("idx", desc=True)
147
+ .limit(1)
148
+ .execute()
149
+ )
150
+ return self._row_to_block(res.data[0]) if res.data else None
151
+ except Exception as e:
152
+ logger.error(f"get_chain_tip error: {e}")
153
+ return None
154
+
155
+ def get_chain_length(self) -> int:
156
+ try:
157
+ res = self._t("pw_blocks").select("idx", count="exact").execute()
158
+ return res.count or 0
159
+ except Exception as e:
160
+ logger.error(f"get_chain_length error: {e}")
161
+ return 0
162
+
163
+ def get_blocks_from(self, from_index: int, limit: int = 50) -> List:
164
+ try:
165
+ res = (
166
+ self._t("pw_blocks")
167
+ .select("*")
168
+ .gte("idx", from_index)
169
+ .order("idx", desc=False)
170
+ .limit(limit)
171
+ .execute()
172
+ )
173
+ return [self._row_to_block(r) for r in (res.data or [])]
174
+ except Exception as e:
175
+ logger.error(f"get_blocks_from error: {e}")
176
+ return []
177
+
178
+ def _row_to_block(self, row: Dict):
179
+ from playweb.core.block import Block
180
+ from playweb.core.transaction import Transaction
181
+
182
+ txs_data = row.get("transactions", [])
183
+ if isinstance(txs_data, str):
184
+ txs_data = json.loads(txs_data)
185
+
186
+ txs = [Transaction.from_dict(t) for t in txs_data]
187
+ votes = row.get("votes", [])
188
+ if isinstance(votes, str):
189
+ votes = json.loads(votes)
190
+
191
+ block = Block(
192
+ index = row["idx"],
193
+ transactions = txs,
194
+ previous_hash = row["previous_hash"],
195
+ validator_wallet = row["validator_wallet"],
196
+ timestamp = row["timestamp"],
197
+ nonce = row.get("nonce", 0),
198
+ consensus_round = row.get("consensus_round", 0),
199
+ votes = votes,
200
+ )
201
+ block.hash = row["hash"]
202
+ block.merkle_root = row["merkle_root"]
203
+ return block
204
+
205
+ # ─── Transactions ─────────────────────────────────────────────
206
+
207
+ def save_transaction(self, tx) -> bool:
208
+ try:
209
+ self._t("pw_transactions").upsert({
210
+ "hash": tx.hash,
211
+ "data": tx.to_dict(),
212
+ }).execute()
213
+ return True
214
+ except Exception as e:
215
+ logger.error(f"save_transaction error: {e}")
216
+ return False
217
+
218
+ def get_transaction(self, tx_hash: str):
219
+ try:
220
+ res = (
221
+ self._t("pw_transactions")
222
+ .select("data")
223
+ .eq("hash", tx_hash)
224
+ .execute()
225
+ )
226
+ if not res.data:
227
+ return None
228
+ from playweb.core.transaction import Transaction
229
+ data = res.data[0]["data"]
230
+ if isinstance(data, str):
231
+ data = json.loads(data)
232
+ return Transaction.from_dict(data)
233
+ except Exception as e:
234
+ logger.error(f"get_transaction error: {e}")
235
+ return None
236
+
237
+ # ─── Balances ────────────────────────────────────────────────
238
+
239
+ def get_balance(self, address: str) -> float:
240
+ try:
241
+ res = (
242
+ self._t("pw_balances")
243
+ .select("balance")
244
+ .eq("address", address.lower())
245
+ .execute()
246
+ )
247
+ return float(res.data[0]["balance"]) if res.data else 0.0
248
+ except Exception as e:
249
+ logger.error(f"get_balance error: {e}")
250
+ return 0.0
251
+
252
+ def save_balance(self, address: str, balance: float) -> bool:
253
+ try:
254
+ self._t("pw_balances").upsert({
255
+ "address": address.lower(),
256
+ "balance": max(0.0, balance),
257
+ "updated": time.time(),
258
+ }).execute()
259
+ return True
260
+ except Exception as e:
261
+ logger.error(f"save_balance error: {e}")
262
+ return False
263
+
264
+ def get_all_addresses(self) -> List[str]:
265
+ try:
266
+ res = self._t("pw_balances").select("address").execute()
267
+ return [r["address"] for r in (res.data or [])]
268
+ except Exception as e:
269
+ logger.error(f"get_all_addresses error: {e}")
270
+ return []
271
+
272
+ # ─── Content Registry ─────────────────────────────────────────
273
+
274
+ def save_content_record(self, record: Dict) -> bool:
275
+ try:
276
+ extra = {k: v for k, v in record.items() if k not in (
277
+ "cid", "creator_wallet", "first_owner", "current_owner",
278
+ "first_platform", "first_tx_hash", "first_block",
279
+ "timestamp", "total_editions", "royalty_pct"
280
+ )}
281
+ self._t("pw_content_registry").upsert({
282
+ "cid": record["cid"],
283
+ "creator_wallet": record.get("creator_wallet"),
284
+ "first_owner": record.get("first_owner"),
285
+ "current_owner": record.get("current_owner", record.get("first_owner", "")).lower(),
286
+ "first_platform": record.get("first_platform", "unknown"),
287
+ "first_tx_hash": record.get("first_tx_hash"),
288
+ "first_block": record.get("first_block"),
289
+ "timestamp": record.get("timestamp"),
290
+ "total_editions": record.get("total_editions", 1),
291
+ "royalty_pct": record.get("royalty_pct", 0),
292
+ "extra": extra,
293
+ }).execute()
294
+ return True
295
+ except Exception as e:
296
+ logger.error(f"save_content_record error: {e}")
297
+ return False
298
+
299
+ def get_content_record(self, cid: str) -> Optional[Dict]:
300
+ try:
301
+ res = (
302
+ self._t("pw_content_registry")
303
+ .select("*")
304
+ .eq("cid", cid)
305
+ .execute()
306
+ )
307
+ if not res.data:
308
+ return None
309
+ record = res.data[0]
310
+ extra = record.pop("extra", {}) or {}
311
+ if isinstance(extra, str):
312
+ extra = json.loads(extra)
313
+ record.update(extra)
314
+ return record
315
+ except Exception as e:
316
+ logger.error(f"get_content_record error: {e}")
317
+ return None
318
+
319
+ def get_all_content_by_owner(self, address: str) -> List[Dict]:
320
+ try:
321
+ res = (
322
+ self._t("pw_content_registry")
323
+ .select("*")
324
+ .eq("current_owner", address.lower())
325
+ .execute()
326
+ )
327
+ return res.data or []
328
+ except Exception as e:
329
+ logger.error(f"get_all_content_by_owner error: {e}")
330
+ return []
331
+
332
+ def get_cid_by_int_id(self, int_id: int) -> Optional[str]:
333
+ try:
334
+ from playweb.api.erc721 import cid_to_int_id
335
+ res = self._t("pw_content_registry").select("cid").execute()
336
+ for r in (res.data or []):
337
+ if cid_to_int_id(r["cid"]) == int_id:
338
+ return r["cid"]
339
+ return None
340
+ except Exception as e:
341
+ logger.error(f"get_cid_by_int_id error: {e}")
342
+ return None
343
+
344
+ # ─── Edition Registry ─────────────────────────────────────────
345
+
346
+ def save_edition_record(self, record: Dict) -> bool:
347
+ try:
348
+ self._t("pw_edition_registry").upsert({
349
+ "cid": record["cid"],
350
+ "edition_number": record["edition_number"],
351
+ "edition_of": record.get("edition_of", 1),
352
+ "current_owner": record.get("current_owner", "").lower(),
353
+ "platform": record.get("platform", "unknown"),
354
+ "tx_hash": record.get("tx_hash"),
355
+ "timestamp": record.get("timestamp"),
356
+ "provenance": record.get("provenance", []),
357
+ }).execute()
358
+ return True
359
+ except Exception as e:
360
+ logger.error(f"save_edition_record error: {e}")
361
+ return False
362
+
363
+ def get_edition_record(self, cid: str, edition_number: int) -> Optional[Dict]:
364
+ try:
365
+ res = (
366
+ self._t("pw_edition_registry")
367
+ .select("*")
368
+ .eq("cid", cid)
369
+ .eq("edition_number", edition_number)
370
+ .execute()
371
+ )
372
+ return res.data[0] if res.data else None
373
+ except Exception as e:
374
+ logger.error(f"get_edition_record error: {e}")
375
+ return None
376
+
377
+ def get_all_edition_records(self, cid: str) -> List[Dict]:
378
+ try:
379
+ res = (
380
+ self._t("pw_edition_registry")
381
+ .select("*")
382
+ .eq("cid", cid)
383
+ .order("edition_number")
384
+ .execute()
385
+ )
386
+ return res.data or []
387
+ except Exception as e:
388
+ logger.error(f"get_all_edition_records error: {e}")
389
+ return []