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.
- playweb/__init__.py +18 -0
- playweb/api/__init__.py +4 -0
- playweb/api/erc20.py +66 -0
- playweb/api/erc721.py +88 -0
- playweb/api/metadata.py +86 -0
- playweb/api/node_api.py +224 -0
- playweb/api/public_api.py +496 -0
- playweb/api/rpc.py +247 -0
- playweb/client.py +341 -0
- playweb/config.py +168 -0
- playweb/consensus/__init__.py +5 -0
- playweb/consensus/leader.py +50 -0
- playweb/consensus/nvf_bft.py +540 -0
- playweb/consensus/vote.py +108 -0
- playweb/core/__init__.py +6 -0
- playweb/core/block.py +156 -0
- playweb/core/blockchain.py +358 -0
- playweb/core/fee_engine.py +312 -0
- playweb/core/mempool.py +112 -0
- playweb/core/royalty_engine.py +207 -0
- playweb/core/transaction.py +270 -0
- playweb/network/__init__.py +6 -0
- playweb/network/bootstrap.py +217 -0
- playweb/network/gossip.py +222 -0
- playweb/network/peer_manager.py +164 -0
- playweb/network/sync.py +244 -0
- playweb/node.py +303 -0
- playweb/plugin/__init__.py +4 -0
- playweb/plugin/base_plugin.py +225 -0
- playweb/plugin/plugin_manager.py +131 -0
- playweb/registry/__init__.py +4 -0
- playweb/registry/content_registry.py +259 -0
- playweb/registry/edition_registry.py +220 -0
- playweb/storage/__init__.py +11 -0
- playweb/storage/base.py +154 -0
- playweb/storage/ram_storage.py +123 -0
- playweb/storage/sqlite_storage.py +369 -0
- playweb/storage/supabase_storage.py +389 -0
- playweb_node-1.0.0.dist-info/METADATA +416 -0
- playweb_node-1.0.0.dist-info/RECORD +43 -0
- playweb_node-1.0.0.dist-info/WHEEL +5 -0
- playweb_node-1.0.0.dist-info/licenses/LICENSE +201 -0
- 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
|