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,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
|
+
]
|
playweb/storage/base.py
ADDED
|
@@ -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
|
+
...
|