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,123 @@
1
+ """
2
+ PlayWebit Network — RAM Storage
3
+ Everything in Python dicts. Lost on restart.
4
+ Use for: testing, development, light nodes.
5
+ """
6
+
7
+ import logging
8
+ from typing import List, Optional, Dict
9
+
10
+ from playweb.storage.base import ChainStorage
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class RAMStorage(ChainStorage):
16
+
17
+ def __init__(self):
18
+ self._blocks_by_hash: Dict[str, object] = {}
19
+ self._blocks_by_index: Dict[int, object] = {}
20
+ self._transactions: Dict[str, object] = {}
21
+ self._balances: Dict[str, float] = {}
22
+ self._content_registry: Dict[str, Dict] = {}
23
+ self._edition_registry: Dict[str, Dict] = {}
24
+ self._chain_length: int = 0
25
+ logger.info("RAMStorage initialised (data lost on restart)")
26
+
27
+ # ─── Blocks ──────────────────────────────────────────────────
28
+
29
+ def save_block(self, block) -> bool:
30
+ self._blocks_by_hash[block.hash] = block
31
+ self._blocks_by_index[block.index] = block
32
+ self._chain_length = max(self._chain_length, block.index + 1)
33
+ return True
34
+
35
+ def get_block(self, block_hash: str):
36
+ return self._blocks_by_hash.get(block_hash)
37
+
38
+ def get_block_by_index(self, index: int):
39
+ return self._blocks_by_index.get(index)
40
+
41
+ def get_chain_tip(self):
42
+ if self._chain_length == 0:
43
+ return None
44
+ return self._blocks_by_index.get(self._chain_length - 1)
45
+
46
+ def get_chain_length(self) -> int:
47
+ return self._chain_length
48
+
49
+ def get_blocks_from(self, from_index: int, limit: int = 50) -> List:
50
+ blocks = []
51
+ for i in range(from_index, min(from_index + limit, self._chain_length)):
52
+ block = self._blocks_by_index.get(i)
53
+ if block:
54
+ blocks.append(block)
55
+ return blocks
56
+
57
+ # ─── Transactions ─────────────────────────────────────────────
58
+
59
+ def save_transaction(self, tx) -> bool:
60
+ self._transactions[tx.hash] = tx
61
+ return True
62
+
63
+ def get_transaction(self, tx_hash: str):
64
+ return self._transactions.get(tx_hash)
65
+
66
+ # ─── Balances ────────────────────────────────────────────────
67
+
68
+ def get_balance(self, address: str) -> float:
69
+ return self._balances.get(address.lower(), 0.0)
70
+
71
+ def save_balance(self, address: str, balance: float) -> bool:
72
+ self._balances[address.lower()] = max(0.0, balance)
73
+ return True
74
+
75
+ def get_all_addresses(self) -> List[str]:
76
+ return list(self._balances.keys())
77
+
78
+ # ─── Content Registry ─────────────────────────────────────────
79
+
80
+ def save_content_record(self, record: Dict) -> bool:
81
+ # Normalise wallet addresses to lowercase
82
+ normalised = dict(record)
83
+ for key in ("creator_wallet", "first_owner", "current_owner"):
84
+ if normalised.get(key):
85
+ normalised[key] = normalised[key].lower()
86
+ self._content_registry[normalised["cid"]] = normalised
87
+ return True
88
+
89
+ def get_content_record(self, cid: str) -> Optional[Dict]:
90
+ return self._content_registry.get(cid)
91
+
92
+ def get_all_content_by_owner(self, address: str) -> List[Dict]:
93
+ addr = address.lower()
94
+ return [
95
+ r for r in self._content_registry.values()
96
+ if r.get("current_owner", "").lower() == addr
97
+ ]
98
+
99
+ def get_cid_by_int_id(self, int_id: int) -> Optional[str]:
100
+ from playweb.api.erc721 import cid_to_int_id
101
+ for cid in self._content_registry:
102
+ if cid_to_int_id(cid) == int_id:
103
+ return cid
104
+ return None
105
+
106
+ # ─── Edition Registry ─────────────────────────────────────────
107
+
108
+ def save_edition_record(self, record: Dict) -> bool:
109
+ normalised = dict(record)
110
+ if normalised.get("current_owner"):
111
+ normalised["current_owner"] = normalised["current_owner"].lower()
112
+ key = f"{normalised['cid']}:{normalised['edition_number']}"
113
+ self._edition_registry[key] = normalised
114
+ return True
115
+
116
+ def get_edition_record(self, cid: str, edition_number: int) -> Optional[Dict]:
117
+ return self._edition_registry.get(f"{cid}:{edition_number}")
118
+
119
+ def get_all_edition_records(self, cid: str) -> List[Dict]:
120
+ return [
121
+ v for k, v in self._edition_registry.items()
122
+ if k.startswith(f"{cid}:")
123
+ ]
@@ -0,0 +1,369 @@
1
+ """
2
+ PlayWebit Network — SQLite Storage
3
+ Built into Python, no extra install needed.
4
+ Use for: VPS nodes, any server with persistent disk.
5
+ Fast reads/writes — standard blockchain approach.
6
+ """
7
+
8
+ import json
9
+ import sqlite3
10
+ import logging
11
+ from typing import List, Optional, Dict
12
+ from contextlib import contextmanager
13
+
14
+ from playweb.storage.base import ChainStorage
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class SQLiteStorage(ChainStorage):
20
+
21
+ def __init__(self, db_path: str = "./chain.db"):
22
+ self.db_path = db_path
23
+ self._init_db()
24
+ logger.info(f"SQLiteStorage initialised at {db_path}")
25
+
26
+ @contextmanager
27
+ def _conn(self):
28
+ conn = sqlite3.connect(self.db_path, check_same_thread=False)
29
+ conn.row_factory = sqlite3.Row
30
+ try:
31
+ yield conn
32
+ conn.commit()
33
+ except Exception:
34
+ conn.rollback()
35
+ raise
36
+ finally:
37
+ conn.close()
38
+
39
+ def _init_db(self):
40
+ with self._conn() as conn:
41
+ conn.executescript("""
42
+ CREATE TABLE IF NOT EXISTS blocks (
43
+ hash TEXT PRIMARY KEY,
44
+ idx INTEGER UNIQUE,
45
+ previous_hash TEXT,
46
+ merkle_root TEXT,
47
+ timestamp REAL,
48
+ nonce INTEGER,
49
+ validator_wallet TEXT,
50
+ consensus_round INTEGER,
51
+ votes TEXT,
52
+ transactions TEXT
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS transactions (
56
+ hash TEXT PRIMARY KEY,
57
+ block_hash TEXT,
58
+ block_index INTEGER,
59
+ data TEXT
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS balances (
63
+ address TEXT PRIMARY KEY,
64
+ balance REAL DEFAULT 0.0,
65
+ updated REAL
66
+ );
67
+
68
+ CREATE TABLE IF NOT EXISTS content_registry (
69
+ cid TEXT PRIMARY KEY,
70
+ creator_wallet TEXT,
71
+ first_owner TEXT,
72
+ current_owner TEXT,
73
+ first_platform TEXT,
74
+ first_tx_hash TEXT,
75
+ first_block INTEGER,
76
+ timestamp REAL,
77
+ total_editions INTEGER,
78
+ royalty_pct REAL,
79
+ extra TEXT
80
+ );
81
+
82
+ CREATE TABLE IF NOT EXISTS edition_registry (
83
+ cid TEXT,
84
+ edition_number INTEGER,
85
+ edition_of INTEGER,
86
+ current_owner TEXT,
87
+ platform TEXT,
88
+ tx_hash TEXT,
89
+ timestamp REAL,
90
+ provenance TEXT,
91
+ PRIMARY KEY (cid, edition_number)
92
+ );
93
+
94
+ CREATE INDEX IF NOT EXISTS idx_content_owner
95
+ ON content_registry(current_owner);
96
+
97
+ CREATE INDEX IF NOT EXISTS idx_content_platform
98
+ ON content_registry(first_platform);
99
+
100
+ CREATE INDEX IF NOT EXISTS idx_editions_owner
101
+ ON edition_registry(current_owner);
102
+ CREATE INDEX IF NOT EXISTS idx_blocks_index
103
+ ON blocks(idx);
104
+ CREATE INDEX IF NOT EXISTS idx_tx_block
105
+ ON transactions(block_hash);
106
+ CREATE INDEX IF NOT EXISTS idx_editions_cid
107
+ ON edition_registry(cid);
108
+ """)
109
+
110
+ # ─── Blocks ──────────────────────────────────────────────────
111
+
112
+ def save_block(self, block) -> bool:
113
+ try:
114
+ with self._conn() as conn:
115
+ conn.execute("""
116
+ INSERT OR REPLACE INTO blocks
117
+ (hash, idx, previous_hash, merkle_root, timestamp,
118
+ nonce, validator_wallet, consensus_round, votes, transactions)
119
+ VALUES (?,?,?,?,?,?,?,?,?,?)
120
+ """, (
121
+ block.hash,
122
+ block.index,
123
+ block.previous_hash,
124
+ block.merkle_root,
125
+ block.timestamp,
126
+ block.nonce,
127
+ block.validator_wallet,
128
+ block.consensus_round,
129
+ json.dumps(block.votes),
130
+ json.dumps([tx.to_dict() for tx in block.transactions]),
131
+ ))
132
+ return True
133
+ except Exception as e:
134
+ logger.error(f"save_block error: {e}")
135
+ return False
136
+
137
+ def get_block(self, block_hash: str):
138
+ with self._conn() as conn:
139
+ row = conn.execute(
140
+ "SELECT * FROM blocks WHERE hash = ?", (block_hash,)
141
+ ).fetchone()
142
+ return self._row_to_block(row) if row else None
143
+
144
+ def get_block_by_index(self, index: int):
145
+ with self._conn() as conn:
146
+ row = conn.execute(
147
+ "SELECT * FROM blocks WHERE idx = ?", (index,)
148
+ ).fetchone()
149
+ return self._row_to_block(row) if row else None
150
+
151
+ def get_chain_tip(self):
152
+ with self._conn() as conn:
153
+ row = conn.execute(
154
+ "SELECT * FROM blocks ORDER BY idx DESC LIMIT 1"
155
+ ).fetchone()
156
+ return self._row_to_block(row) if row else None
157
+
158
+ def get_chain_length(self) -> int:
159
+ with self._conn() as conn:
160
+ row = conn.execute("SELECT COUNT(*) as cnt FROM blocks").fetchone()
161
+ return row["cnt"] if row else 0
162
+
163
+ def get_blocks_from(self, from_index: int, limit: int = 50) -> List:
164
+ with self._conn() as conn:
165
+ rows = conn.execute(
166
+ "SELECT * FROM blocks WHERE idx >= ? ORDER BY idx ASC LIMIT ?",
167
+ (from_index, limit)
168
+ ).fetchall()
169
+ return [self._row_to_block(r) for r in rows if r]
170
+
171
+ def _row_to_block(self, row):
172
+ from playweb.core.block import Block
173
+ txs_data = json.loads(row["transactions"])
174
+ from playweb.core.transaction import Transaction
175
+ txs = [Transaction.from_dict(t) for t in txs_data]
176
+ block = Block(
177
+ index = row["idx"],
178
+ transactions = txs,
179
+ previous_hash = row["previous_hash"],
180
+ validator_wallet = row["validator_wallet"],
181
+ timestamp = row["timestamp"],
182
+ nonce = row["nonce"],
183
+ consensus_round = row["consensus_round"],
184
+ votes = json.loads(row["votes"] or "[]"),
185
+ )
186
+ block.hash = row["hash"]
187
+ block.merkle_root = row["merkle_root"]
188
+ return block
189
+
190
+ # ─── Transactions ─────────────────────────────────────────────
191
+
192
+ def save_transaction(self, tx) -> bool:
193
+ try:
194
+ with self._conn() as conn:
195
+ conn.execute("""
196
+ INSERT OR REPLACE INTO transactions (hash, data)
197
+ VALUES (?, ?)
198
+ """, (tx.hash, json.dumps(tx.to_dict())))
199
+ return True
200
+ except Exception as e:
201
+ logger.error(f"save_transaction error: {e}")
202
+ return False
203
+
204
+ def get_transaction(self, tx_hash: str):
205
+ with self._conn() as conn:
206
+ row = conn.execute(
207
+ "SELECT data FROM transactions WHERE hash = ?", (tx_hash,)
208
+ ).fetchone()
209
+ if not row:
210
+ return None
211
+ from playweb.core.transaction import Transaction
212
+ return Transaction.from_dict(json.loads(row["data"]))
213
+
214
+ # ─── Balances ────────────────────────────────────────────────
215
+
216
+ def get_balance(self, address: str) -> float:
217
+ with self._conn() as conn:
218
+ row = conn.execute(
219
+ "SELECT balance FROM balances WHERE address = ?",
220
+ (address.lower(),)
221
+ ).fetchone()
222
+ return float(row["balance"]) if row else 0.0
223
+
224
+ def save_balance(self, address: str, balance: float) -> bool:
225
+ import time
226
+ try:
227
+ with self._conn() as conn:
228
+ conn.execute("""
229
+ INSERT INTO balances (address, balance, updated)
230
+ VALUES (?, ?, ?)
231
+ ON CONFLICT(address) DO UPDATE SET
232
+ balance = excluded.balance,
233
+ updated = excluded.updated
234
+ """, (address.lower(), max(0.0, balance), time.time()))
235
+ return True
236
+ except Exception as e:
237
+ logger.error(f"save_balance error: {e}")
238
+ return False
239
+
240
+ def get_all_addresses(self) -> List[str]:
241
+ with self._conn() as conn:
242
+ rows = conn.execute("SELECT address FROM balances").fetchall()
243
+ return [r["address"] for r in rows]
244
+
245
+ # ─── Content Registry ─────────────────────────────────────────
246
+
247
+ def save_content_record(self, record: Dict) -> bool:
248
+ try:
249
+ extra = {k: v for k, v in record.items() if k not in (
250
+ "cid", "creator_wallet", "first_owner", "current_owner",
251
+ "first_platform", "first_tx_hash", "first_block",
252
+ "timestamp", "total_editions", "royalty_pct"
253
+ )}
254
+ with self._conn() as conn:
255
+ conn.execute("""
256
+ INSERT INTO content_registry
257
+ (cid, creator_wallet, first_owner, current_owner,
258
+ first_platform, first_tx_hash, first_block,
259
+ timestamp, total_editions, royalty_pct, extra)
260
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)
261
+ ON CONFLICT(cid) DO UPDATE SET
262
+ current_owner = excluded.current_owner,
263
+ extra = excluded.extra
264
+ """, (
265
+ record["cid"],
266
+ record.get("creator_wallet"),
267
+ record.get("first_owner"),
268
+ record.get("current_owner", record.get("first_owner", "")).lower(),
269
+ record.get("first_platform", "unknown"),
270
+ record.get("first_tx_hash"),
271
+ record.get("first_block"),
272
+ record.get("timestamp"),
273
+ record.get("total_editions", 1),
274
+ record.get("royalty_pct", 0),
275
+ json.dumps(extra),
276
+ ))
277
+ return True
278
+ except Exception as e:
279
+ logger.error(f"save_content_record error: {e}")
280
+ return False
281
+
282
+ def get_content_record(self, cid: str) -> Optional[Dict]:
283
+ with self._conn() as conn:
284
+ row = conn.execute(
285
+ "SELECT * FROM content_registry WHERE cid = ?", (cid,)
286
+ ).fetchone()
287
+ if not row:
288
+ return None
289
+ record = dict(row)
290
+ if record.get("extra"):
291
+ record.update(json.loads(record.pop("extra")))
292
+ return record
293
+
294
+ def get_all_content_by_owner(self, address: str) -> List[Dict]:
295
+ with self._conn() as conn:
296
+ rows = conn.execute(
297
+ "SELECT * FROM content_registry WHERE current_owner = ?",
298
+ (address.lower(),)
299
+ ).fetchall()
300
+ return [dict(r) for r in rows]
301
+
302
+ def get_cid_by_int_id(self, int_id: int) -> Optional[str]:
303
+ # Scan all CIDs and check int_id
304
+ # For better performance add int_token_id column (see note below)
305
+ from playweb.api.erc721 import cid_to_int_id
306
+ with self._conn() as conn:
307
+ rows = conn.execute(
308
+ "SELECT cid FROM content_registry"
309
+ ).fetchall()
310
+ for row in rows:
311
+ if cid_to_int_id(row["cid"]) == int_id:
312
+ return row["cid"]
313
+ return None
314
+
315
+ # ─── Edition Registry ─────────────────────────────────────────
316
+
317
+ def save_edition_record(self, record: Dict) -> bool:
318
+ try:
319
+ with self._conn() as conn:
320
+ conn.execute("""
321
+ INSERT INTO edition_registry
322
+ (cid, edition_number, edition_of, current_owner,
323
+ platform, tx_hash, timestamp, provenance)
324
+ VALUES (?,?,?,?,?,?,?,?)
325
+ ON CONFLICT(cid, edition_number) DO UPDATE SET
326
+ current_owner = excluded.current_owner,
327
+ platform = excluded.platform,
328
+ tx_hash = excluded.tx_hash,
329
+ timestamp = excluded.timestamp,
330
+ provenance = excluded.provenance
331
+ """, (
332
+ record["cid"],
333
+ record["edition_number"],
334
+ record.get("edition_of", 1),
335
+ record.get("current_owner", "").lower(),
336
+ record.get("platform", "unknown"),
337
+ record.get("tx_hash"),
338
+ record.get("timestamp"),
339
+ json.dumps(record.get("provenance", [])),
340
+ ))
341
+ return True
342
+ except Exception as e:
343
+ logger.error(f"save_edition_record error: {e}")
344
+ return False
345
+
346
+ def get_edition_record(self, cid: str, edition_number: int) -> Optional[Dict]:
347
+ with self._conn() as conn:
348
+ row = conn.execute(
349
+ "SELECT * FROM edition_registry WHERE cid=? AND edition_number=?",
350
+ (cid, edition_number)
351
+ ).fetchone()
352
+ if not row:
353
+ return None
354
+ record = dict(row)
355
+ record["provenance"] = json.loads(record.get("provenance") or "[]")
356
+ return record
357
+
358
+ def get_all_edition_records(self, cid: str) -> List[Dict]:
359
+ with self._conn() as conn:
360
+ rows = conn.execute(
361
+ "SELECT * FROM edition_registry WHERE cid=? ORDER BY edition_number",
362
+ (cid,)
363
+ ).fetchall()
364
+ records = []
365
+ for row in rows:
366
+ r = dict(row)
367
+ r["provenance"] = json.loads(r.get("provenance") or "[]")
368
+ records.append(r)
369
+ return records