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
playweb/node.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — PlayWebitNode
|
|
3
|
+
Main entry point for running a full validator node.
|
|
4
|
+
Wires all components together.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from playweb import PlayWebitNode
|
|
8
|
+
from playweb.storage.supabase_storage import SupabaseStorage
|
|
9
|
+
|
|
10
|
+
storage = SupabaseStorage(url=..., key=...)
|
|
11
|
+
node = PlayWebitNode(
|
|
12
|
+
storage = storage,
|
|
13
|
+
node_wallet = "0xYourWallet",
|
|
14
|
+
node_private_key = os.getenv("NODE_WALLET_PRIVATE_KEY"),
|
|
15
|
+
node_public_url = "https://your-node.com",
|
|
16
|
+
)
|
|
17
|
+
node.start()
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
import logging
|
|
23
|
+
import threading
|
|
24
|
+
from typing import Optional, TYPE_CHECKING
|
|
25
|
+
|
|
26
|
+
from playweb.core.blockchain import Blockchain
|
|
27
|
+
from playweb.core.fee_engine import FeeEngine
|
|
28
|
+
from playweb.core.royalty_engine import RoyaltyEngine
|
|
29
|
+
from playweb.consensus.nvf_bft import NVFBFTConsensus
|
|
30
|
+
from playweb.network.peer_manager import PeerManager
|
|
31
|
+
from playweb.network.bootstrap import Bootstrap
|
|
32
|
+
from playweb.network.gossip import GossipProtocol
|
|
33
|
+
from playweb.network.sync import ChainSync
|
|
34
|
+
from playweb.registry.content_registry import ContentRegistry
|
|
35
|
+
from playweb.registry.edition_registry import EditionRegistry
|
|
36
|
+
from playweb.plugin.plugin_manager import PluginManager
|
|
37
|
+
from playweb.config import LOG_LEVEL, NODE_PORT
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from playweb.plugin.base_plugin import BasePlugin
|
|
41
|
+
from playweb.storage.base import ChainStorage
|
|
42
|
+
|
|
43
|
+
logging.basicConfig(
|
|
44
|
+
level = getattr(logging, LOG_LEVEL, logging.INFO),
|
|
45
|
+
format = "%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
|
46
|
+
)
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PlayWebitNode:
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
storage,
|
|
55
|
+
node_wallet: str,
|
|
56
|
+
node_private_key: str,
|
|
57
|
+
node_public_url: str,
|
|
58
|
+
platform: str = "unknown",
|
|
59
|
+
plugin: Optional["BasePlugin"] = None,
|
|
60
|
+
port: int = NODE_PORT,
|
|
61
|
+
):
|
|
62
|
+
self.node_wallet = node_wallet.lower()
|
|
63
|
+
self.node_private_key = node_private_key
|
|
64
|
+
self.node_url = node_public_url.rstrip("/")
|
|
65
|
+
self.platform = platform
|
|
66
|
+
self.port = port
|
|
67
|
+
|
|
68
|
+
# ── Core ────────────────────────────────────────────────
|
|
69
|
+
self.blockchain = Blockchain(storage)
|
|
70
|
+
|
|
71
|
+
# ── Registries ──────────────────────────────────────────
|
|
72
|
+
self.content_registry = ContentRegistry(self.blockchain, self.node_wallet)
|
|
73
|
+
self.edition_registry = EditionRegistry(self.blockchain, self.node_wallet)
|
|
74
|
+
|
|
75
|
+
# ── Network ─────────────────────────────────────────────
|
|
76
|
+
self.peer_manager = PeerManager(self.node_url, self.node_wallet)
|
|
77
|
+
self.gossip = GossipProtocol(self.node_wallet)
|
|
78
|
+
self.bootstrap = Bootstrap(
|
|
79
|
+
node_url = self.node_url,
|
|
80
|
+
node_wallet = self.node_wallet,
|
|
81
|
+
node_private_key = self.node_private_key,
|
|
82
|
+
platform = self.platform,
|
|
83
|
+
)
|
|
84
|
+
self.sync = ChainSync(self.blockchain, self.peer_manager)
|
|
85
|
+
|
|
86
|
+
# ── Plugins ─────────────────────────────────────────────
|
|
87
|
+
self.plugin_manager = PluginManager()
|
|
88
|
+
self.plugin_manager.set_node(self)
|
|
89
|
+
|
|
90
|
+
# ── Consensus ───────────────────────────────────────────
|
|
91
|
+
self.consensus = NVFBFTConsensus(
|
|
92
|
+
blockchain = self.blockchain,
|
|
93
|
+
peer_manager = self.peer_manager,
|
|
94
|
+
gossip = self.gossip,
|
|
95
|
+
node_wallet = self.node_wallet,
|
|
96
|
+
node_private_key = self.node_private_key,
|
|
97
|
+
plugin_manager = self.plugin_manager,
|
|
98
|
+
on_block_finalised = self._on_block_finalised,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Register plugin if provided
|
|
102
|
+
if plugin:
|
|
103
|
+
self.register_plugin(plugin)
|
|
104
|
+
|
|
105
|
+
self._flask_app = None
|
|
106
|
+
self._flask_thread: Optional[threading.Thread] = None
|
|
107
|
+
self._running = False
|
|
108
|
+
|
|
109
|
+
logger.info(
|
|
110
|
+
f"PlayWebitNode initialised: "
|
|
111
|
+
f"wallet={self.node_wallet[:12]}... "
|
|
112
|
+
f"url={self.node_url}"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# ─────────────────────────────────────────────────────────────
|
|
116
|
+
# Plugin registration
|
|
117
|
+
# ─────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
def register_plugin(self, plugin: "BasePlugin"):
|
|
120
|
+
"""Register a L2 plugin with this node."""
|
|
121
|
+
self.plugin_manager.register(plugin)
|
|
122
|
+
|
|
123
|
+
# ─────────────────────────────────────────────────────────────
|
|
124
|
+
# Start
|
|
125
|
+
# ─────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def start(self, blocking: bool = True):
|
|
128
|
+
"""
|
|
129
|
+
Start the node:
|
|
130
|
+
1. Discover peers via Cloudflare bootstrap
|
|
131
|
+
2. Register self with bootstrap directory
|
|
132
|
+
3. Sync chain from peers
|
|
133
|
+
4. Start Flask API server
|
|
134
|
+
5. Start NVF-BFT consensus loop
|
|
135
|
+
6. Start heartbeat
|
|
136
|
+
7. Notify plugins
|
|
137
|
+
"""
|
|
138
|
+
self._running = True
|
|
139
|
+
logger.info("=" * 50)
|
|
140
|
+
logger.info("PlayWebit Network Node Starting...")
|
|
141
|
+
logger.info(f" Wallet: {self.node_wallet}")
|
|
142
|
+
logger.info(f" URL: {self.node_url}")
|
|
143
|
+
logger.info(f" Platform: {self.platform}")
|
|
144
|
+
logger.info("=" * 50)
|
|
145
|
+
|
|
146
|
+
# Step 1: Discover peers
|
|
147
|
+
logger.info("Step 1/6: Discovering peers via bootstrap...")
|
|
148
|
+
peers = self.bootstrap.discover_peers()
|
|
149
|
+
self.peer_manager.load_peers_from_list(peers)
|
|
150
|
+
logger.info(f" Found {len(peers)} peers")
|
|
151
|
+
|
|
152
|
+
# Step 2: Register with Cloudflare bootstrap
|
|
153
|
+
logger.info("Step 2/6: Registering with bootstrap directory...")
|
|
154
|
+
self.bootstrap.register_node()
|
|
155
|
+
|
|
156
|
+
# Step 3: Sync chain
|
|
157
|
+
logger.info("Step 3/6: Syncing chain from peers...")
|
|
158
|
+
self.sync.sync()
|
|
159
|
+
tip = self.blockchain.get_chain_tip()
|
|
160
|
+
logger.info(
|
|
161
|
+
f" Chain synced. Height: {tip.index if tip else 0}"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Step 4: Start Flask API
|
|
165
|
+
logger.info(f"Step 4/6: Starting API server on port {self.port}...")
|
|
166
|
+
self._start_flask()
|
|
167
|
+
|
|
168
|
+
# Step 5: Start consensus
|
|
169
|
+
logger.info("Step 5/6: Starting NVF-BFT consensus...")
|
|
170
|
+
self.consensus.start()
|
|
171
|
+
|
|
172
|
+
# Step 6: Start heartbeat
|
|
173
|
+
logger.info("Step 6/6: Starting bootstrap heartbeat...")
|
|
174
|
+
self.bootstrap.start_heartbeat()
|
|
175
|
+
|
|
176
|
+
# Notify plugins
|
|
177
|
+
self.plugin_manager.notify_start(self)
|
|
178
|
+
|
|
179
|
+
logger.info("=" * 50)
|
|
180
|
+
logger.info("Node is live and participating in consensus!")
|
|
181
|
+
logger.info("=" * 50)
|
|
182
|
+
|
|
183
|
+
if blocking:
|
|
184
|
+
try:
|
|
185
|
+
while self._running:
|
|
186
|
+
time.sleep(1)
|
|
187
|
+
except KeyboardInterrupt:
|
|
188
|
+
self.stop()
|
|
189
|
+
|
|
190
|
+
def stop(self):
|
|
191
|
+
"""Graceful shutdown."""
|
|
192
|
+
logger.info("Node shutting down...")
|
|
193
|
+
self._running = False
|
|
194
|
+
self.consensus.stop()
|
|
195
|
+
self.bootstrap.deregister()
|
|
196
|
+
self.plugin_manager.notify_stop()
|
|
197
|
+
logger.info("Node stopped.")
|
|
198
|
+
|
|
199
|
+
# ─────────────────────────────────────────────────────────────
|
|
200
|
+
# Flask API server
|
|
201
|
+
# ─────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
def _start_flask(self):
|
|
204
|
+
"""Start Flask in a background thread."""
|
|
205
|
+
from flask import Flask
|
|
206
|
+
from playweb.api.node_api import create_node_api
|
|
207
|
+
from playweb.api.public_api import create_public_api
|
|
208
|
+
from playweb.api.rpc import create_rpc
|
|
209
|
+
from playweb.api.metadata import create_metadata_api
|
|
210
|
+
|
|
211
|
+
app = Flask(__name__)
|
|
212
|
+
app.register_blueprint(create_node_api(self))
|
|
213
|
+
app.register_blueprint(create_public_api(self))
|
|
214
|
+
app.register_blueprint(create_rpc(self))
|
|
215
|
+
app.register_blueprint(create_metadata_api(self))
|
|
216
|
+
|
|
217
|
+
self._flask_app = app
|
|
218
|
+
|
|
219
|
+
self._flask_thread = threading.Thread(
|
|
220
|
+
target=lambda: app.run(
|
|
221
|
+
host = "0.0.0.0",
|
|
222
|
+
port = self.port,
|
|
223
|
+
debug = False,
|
|
224
|
+
use_reloader = False,
|
|
225
|
+
),
|
|
226
|
+
daemon = True,
|
|
227
|
+
name = "flask-api",
|
|
228
|
+
)
|
|
229
|
+
self._flask_thread.start()
|
|
230
|
+
logger.info(f"API server started on port {self.port}")
|
|
231
|
+
|
|
232
|
+
def get_flask_app(self):
|
|
233
|
+
"""
|
|
234
|
+
Get the Flask app for external mounting.
|
|
235
|
+
Use this if you want to add your own routes on top.
|
|
236
|
+
Returns the Flask app instance.
|
|
237
|
+
"""
|
|
238
|
+
if not self._flask_app:
|
|
239
|
+
from flask import Flask
|
|
240
|
+
from playweb.api.node_api import create_node_api
|
|
241
|
+
from playweb.api.public_api import create_public_api
|
|
242
|
+
|
|
243
|
+
app = Flask(__name__)
|
|
244
|
+
app.register_blueprint(create_node_api(self))
|
|
245
|
+
app.register_blueprint(create_public_api(self))
|
|
246
|
+
self._flask_app = app
|
|
247
|
+
|
|
248
|
+
return self._flask_app
|
|
249
|
+
|
|
250
|
+
# ─────────────────────────────────────────────────────────────
|
|
251
|
+
# Consensus callback
|
|
252
|
+
# ─────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
def _on_block_finalised(self, block, votes):
|
|
255
|
+
"""
|
|
256
|
+
Called by consensus after a block is finalised.
|
|
257
|
+
Broadcasts to peers so they can add it to their chain.
|
|
258
|
+
NVF: Cyclic re-entry notification complete.
|
|
259
|
+
"""
|
|
260
|
+
self.gossip.broadcast_finalised_block(
|
|
261
|
+
block = block,
|
|
262
|
+
votes = votes,
|
|
263
|
+
peers = self.peer_manager.get_active_peers(),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# ─────────────────────────────────────────────────────────────
|
|
267
|
+
# Convenience methods
|
|
268
|
+
# ─────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
def submit_transaction(self, tx, broadcast: bool = True):
|
|
271
|
+
"""Submit a transaction and optionally broadcast to peers."""
|
|
272
|
+
success, result = self.blockchain.add_transaction(
|
|
273
|
+
tx = tx,
|
|
274
|
+
node_wallet = self.node_wallet,
|
|
275
|
+
)
|
|
276
|
+
if success and broadcast:
|
|
277
|
+
self.gossip.broadcast_transaction(
|
|
278
|
+
tx = tx,
|
|
279
|
+
peers = self.peer_manager.get_active_peers(),
|
|
280
|
+
)
|
|
281
|
+
return success, result
|
|
282
|
+
|
|
283
|
+
def get_balance(self, address: str) -> float:
|
|
284
|
+
return self.blockchain.get_balance(address)
|
|
285
|
+
|
|
286
|
+
def get_block(self, block_hash: str):
|
|
287
|
+
return self.blockchain.get_block(block_hash)
|
|
288
|
+
|
|
289
|
+
def get_chain_tip(self):
|
|
290
|
+
return self.blockchain.get_chain_tip()
|
|
291
|
+
|
|
292
|
+
def get_status(self) -> dict:
|
|
293
|
+
tip = self.blockchain.get_chain_tip()
|
|
294
|
+
return {
|
|
295
|
+
"node_wallet": self.node_wallet,
|
|
296
|
+
"node_url": self.node_url,
|
|
297
|
+
"platform": self.platform,
|
|
298
|
+
"block_height": tip.index if tip else 0,
|
|
299
|
+
"peer_count": self.peer_manager.peer_count(),
|
|
300
|
+
"consensus": self.consensus.get_status(),
|
|
301
|
+
"sync": self.sync.get_sync_status(),
|
|
302
|
+
"plugins": self.plugin_manager.get_status(),
|
|
303
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — Base Plugin Interface
|
|
3
|
+
L2 apps implement this to plug into a PlayWebit node.
|
|
4
|
+
CipherVault, MusicApp, ArtApp etc all implement BasePlugin.
|
|
5
|
+
|
|
6
|
+
The plugin gets:
|
|
7
|
+
- Event hooks from L1 (on_block_finalised, on_transaction etc)
|
|
8
|
+
- SDK interface to call L1 (submit_tx, get_balance etc)
|
|
9
|
+
- Clean separation — plugin cannot touch L1 internals directly
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from abc import ABC, abstractmethod
|
|
13
|
+
from typing import Optional, Dict, TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from playweb.core.block import Block
|
|
17
|
+
from playweb.core.transaction import Transaction
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BasePlugin(ABC):
|
|
21
|
+
|
|
22
|
+
# ─────────────────────────────────────────────────────────────
|
|
23
|
+
# Plugin identity — subclass must set these
|
|
24
|
+
# ─────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
plugin_id: str = "base" # e.g. "ciphervault"
|
|
27
|
+
plugin_name: str = "Base Plugin" # e.g. "CipherVault"
|
|
28
|
+
plugin_version: str = "1.0.0"
|
|
29
|
+
platform_wallet: str = "" # plugin's PLWB treasury wallet
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
self._node = None # set by plugin_manager on register
|
|
33
|
+
|
|
34
|
+
def _set_node(self, node):
|
|
35
|
+
"""Called by PluginManager when plugin is registered."""
|
|
36
|
+
self._node = node
|
|
37
|
+
|
|
38
|
+
# ─────────────────────────────────────────────────────────────
|
|
39
|
+
# Lifecycle hooks — L1 calls these
|
|
40
|
+
# ─────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
def on_start(self, node):
|
|
43
|
+
"""
|
|
44
|
+
Called when the node starts up.
|
|
45
|
+
Use this to initialise your plugin's own state.
|
|
46
|
+
"""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def on_stop(self):
|
|
50
|
+
"""Called when the node shuts down gracefully."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def on_block_finalised(self, block: "Block"):
|
|
54
|
+
"""
|
|
55
|
+
Called after every block is finalised by consensus.
|
|
56
|
+
NVF: Cyclic re-entry notification.
|
|
57
|
+
Use this to update your L2 state from confirmed transactions.
|
|
58
|
+
"""
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def on_transaction(self, tx: "Transaction"):
|
|
62
|
+
"""
|
|
63
|
+
Called when a new transaction is confirmed on chain.
|
|
64
|
+
Note: also fires via on_block_finalised for each tx in block.
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def on_content_registered(self, cid: str, owner: str, platform: str):
|
|
69
|
+
"""Called when new content is registered on the network."""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
def on_edition_transferred(
|
|
73
|
+
self,
|
|
74
|
+
cid: str,
|
|
75
|
+
edition_number: int,
|
|
76
|
+
from_wallet: str,
|
|
77
|
+
to_wallet: str,
|
|
78
|
+
):
|
|
79
|
+
"""Called when an edition changes hands."""
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# ─────────────────────────────────────────────────────────────
|
|
83
|
+
# SDK interface — plugin calls L1 via these
|
|
84
|
+
# These are convenience wrappers around node methods
|
|
85
|
+
# ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
def submit_transaction(self, tx: "Transaction") -> tuple:
|
|
88
|
+
"""Submit a transaction to L1. Returns (success, tx_hash_or_reason)."""
|
|
89
|
+
if not self._node:
|
|
90
|
+
return False, "Plugin not attached to node"
|
|
91
|
+
return self._node.blockchain.add_transaction(
|
|
92
|
+
tx = tx,
|
|
93
|
+
node_wallet = self._node.node_wallet,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def get_balance(self, address: str) -> float:
|
|
97
|
+
"""Get PLWB balance for any address."""
|
|
98
|
+
if not self._node:
|
|
99
|
+
return 0.0
|
|
100
|
+
return self._node.blockchain.get_balance(address)
|
|
101
|
+
|
|
102
|
+
def register_content(
|
|
103
|
+
self,
|
|
104
|
+
cid: str,
|
|
105
|
+
owner_wallet: str,
|
|
106
|
+
editions: int = 1,
|
|
107
|
+
royalty_pct: float = 0,
|
|
108
|
+
signature: str = None,
|
|
109
|
+
extra_data: Dict = None,
|
|
110
|
+
) -> tuple:
|
|
111
|
+
"""Register a CID on the network. Returns (success, reason, tx_hash)."""
|
|
112
|
+
if not self._node:
|
|
113
|
+
return False, "Plugin not attached to node", None
|
|
114
|
+
return self._node.content_registry.register(
|
|
115
|
+
cid = cid,
|
|
116
|
+
owner_wallet = owner_wallet,
|
|
117
|
+
platform_id = self.plugin_id,
|
|
118
|
+
editions = editions,
|
|
119
|
+
royalty_pct = royalty_pct,
|
|
120
|
+
signature = signature,
|
|
121
|
+
extra_data = extra_data,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def transfer_ownership(
|
|
125
|
+
self,
|
|
126
|
+
cid: str,
|
|
127
|
+
from_wallet: str,
|
|
128
|
+
to_wallet: str,
|
|
129
|
+
signature: str = None,
|
|
130
|
+
sale_price: float = 0,
|
|
131
|
+
) -> tuple:
|
|
132
|
+
"""Transfer ownership of a CID. Returns (success, reason, tx_hash)."""
|
|
133
|
+
if not self._node:
|
|
134
|
+
return False, "Plugin not attached to node", None
|
|
135
|
+
return self._node.content_registry.transfer_ownership(
|
|
136
|
+
cid = cid,
|
|
137
|
+
from_wallet = from_wallet,
|
|
138
|
+
to_wallet = to_wallet,
|
|
139
|
+
signature = signature,
|
|
140
|
+
platform_id = self.plugin_id,
|
|
141
|
+
sale_price = sale_price,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def verify_ownership(self, cid: str, wallet: str) -> bool:
|
|
145
|
+
"""Verify wallet owns a CID."""
|
|
146
|
+
if not self._node:
|
|
147
|
+
return False
|
|
148
|
+
return self._node.content_registry.verify_ownership(cid, wallet)
|
|
149
|
+
|
|
150
|
+
def check_duplicate(self, cid: str) -> Dict:
|
|
151
|
+
"""Check if CID is already registered anywhere on the network."""
|
|
152
|
+
if not self._node:
|
|
153
|
+
return {"exists": False}
|
|
154
|
+
return self._node.content_registry.check_duplicate(cid)
|
|
155
|
+
|
|
156
|
+
def anchor_spider_hash(
|
|
157
|
+
self,
|
|
158
|
+
chain_name: str,
|
|
159
|
+
spider_hash: str,
|
|
160
|
+
event_type: str = "integrity_check",
|
|
161
|
+
metadata: Dict = None,
|
|
162
|
+
signature: str = None,
|
|
163
|
+
) -> tuple:
|
|
164
|
+
"""
|
|
165
|
+
Anchor a SpiderWeave hash on the chain.
|
|
166
|
+
This is what spiderweave-sdk calls internally.
|
|
167
|
+
Returns (success, tx_hash).
|
|
168
|
+
"""
|
|
169
|
+
if not self._node:
|
|
170
|
+
return False, "Plugin not attached to node"
|
|
171
|
+
|
|
172
|
+
from playweb.core.transaction import Transaction
|
|
173
|
+
tx = Transaction(
|
|
174
|
+
from_addr = self.platform_wallet.lower(),
|
|
175
|
+
to_addr = self.platform_wallet.lower(),
|
|
176
|
+
amount = 0,
|
|
177
|
+
tx_type = "spider_hash_anchor",
|
|
178
|
+
signature = signature,
|
|
179
|
+
spider_hash = spider_hash,
|
|
180
|
+
chain_name = chain_name,
|
|
181
|
+
data = {
|
|
182
|
+
"event_type": event_type,
|
|
183
|
+
"platform_id": self.plugin_id,
|
|
184
|
+
**(metadata or {}),
|
|
185
|
+
},
|
|
186
|
+
)
|
|
187
|
+
success, result = self.submit_transaction(tx)
|
|
188
|
+
return success, result
|
|
189
|
+
|
|
190
|
+
def get_editions(self, cid: str) -> list:
|
|
191
|
+
"""Get all editions for a CID."""
|
|
192
|
+
if not self._node:
|
|
193
|
+
return []
|
|
194
|
+
return self._node.edition_registry.get_all_editions(cid)
|
|
195
|
+
|
|
196
|
+
def get_nft_metadata(self, cid: str, chain_record: dict) -> dict:
|
|
197
|
+
"""
|
|
198
|
+
Optional: enrich NFT metadata for MetaMask display.
|
|
199
|
+
Called by /api/metadata/{cid} endpoint.
|
|
200
|
+
|
|
201
|
+
Return dict to merge with default metadata:
|
|
202
|
+
{
|
|
203
|
+
"name": "My File Name",
|
|
204
|
+
"image": "data:image/jpeg;base64,...", or IPFS URL
|
|
205
|
+
"attributes": [...additional attributes...]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Return None to use default chain metadata only.
|
|
209
|
+
"""
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
# ─────────────────────────────────────────────────────────────
|
|
213
|
+
# Plugin info
|
|
214
|
+
# ─────────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
def get_info(self) -> Dict:
|
|
217
|
+
return {
|
|
218
|
+
"plugin_id": self.plugin_id,
|
|
219
|
+
"plugin_name": self.plugin_name,
|
|
220
|
+
"plugin_version": self.plugin_version,
|
|
221
|
+
"platform_wallet": self.platform_wallet,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
def __repr__(self):
|
|
225
|
+
return f"Plugin({self.plugin_id} v{self.plugin_version})"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlayWebit Network — Plugin Manager
|
|
3
|
+
Loads and manages L2 plugins.
|
|
4
|
+
Notifies all registered plugins when L1 events happen.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, List, Optional, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from playweb.plugin.base_plugin import BasePlugin
|
|
12
|
+
from playweb.core.block import Block
|
|
13
|
+
from playweb.core.transaction import Transaction
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PluginManager:
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._plugins: Dict[str, "BasePlugin"] = {}
|
|
22
|
+
self._node = None
|
|
23
|
+
|
|
24
|
+
def set_node(self, node):
|
|
25
|
+
self._node = node
|
|
26
|
+
|
|
27
|
+
# ─────────────────────────────────────────────────────────────
|
|
28
|
+
# Register plugins
|
|
29
|
+
# ─────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
def register(self, plugin: "BasePlugin"):
|
|
32
|
+
"""Register a L2 plugin with this node."""
|
|
33
|
+
if plugin.plugin_id in self._plugins:
|
|
34
|
+
logger.warning(
|
|
35
|
+
f"Plugin {plugin.plugin_id} already registered — replacing"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
plugin._set_node(self._node)
|
|
39
|
+
self._plugins[plugin.plugin_id] = plugin
|
|
40
|
+
logger.info(
|
|
41
|
+
f"Plugin registered: {plugin.plugin_name} "
|
|
42
|
+
f"(v{plugin.plugin_version})"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def get_plugin(self, plugin_id: str) -> Optional["BasePlugin"]:
|
|
46
|
+
return self._plugins.get(plugin_id)
|
|
47
|
+
|
|
48
|
+
def get_all_plugins(self) -> List["BasePlugin"]:
|
|
49
|
+
return list(self._plugins.values())
|
|
50
|
+
|
|
51
|
+
# ─────────────────────────────────────────────────────────────
|
|
52
|
+
# Lifecycle notifications
|
|
53
|
+
# ─────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def notify_start(self, node):
|
|
56
|
+
"""Notify all plugins that the node has started."""
|
|
57
|
+
for plugin in self._plugins.values():
|
|
58
|
+
try:
|
|
59
|
+
plugin.on_start(node)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(
|
|
62
|
+
f"Plugin {plugin.plugin_id} on_start error: {e}",
|
|
63
|
+
exc_info=True
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def notify_stop(self):
|
|
67
|
+
"""Notify all plugins that the node is stopping."""
|
|
68
|
+
for plugin in self._plugins.values():
|
|
69
|
+
try:
|
|
70
|
+
plugin.on_stop()
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.error(f"Plugin {plugin.plugin_id} on_stop error: {e}")
|
|
73
|
+
|
|
74
|
+
# ─────────────────────────────────────────────────────────────
|
|
75
|
+
# Block / Transaction notifications
|
|
76
|
+
# NVF: Cyclic re-entry
|
|
77
|
+
# ─────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
def notify_block_finalised(self, block: "Block"):
|
|
80
|
+
"""
|
|
81
|
+
Notify all plugins that a block has been finalised.
|
|
82
|
+
NVF paper: Cyclic re-entry notification after ANCHOR phase.
|
|
83
|
+
Each plugin processes transactions relevant to their platform.
|
|
84
|
+
"""
|
|
85
|
+
for plugin in self._plugins.values():
|
|
86
|
+
try:
|
|
87
|
+
plugin.on_block_finalised(block)
|
|
88
|
+
|
|
89
|
+
# Also notify per-transaction
|
|
90
|
+
for tx in block.transactions:
|
|
91
|
+
plugin.on_transaction(tx)
|
|
92
|
+
|
|
93
|
+
# Special hooks for content events
|
|
94
|
+
if tx.tx_type == "content_register" and tx.cid:
|
|
95
|
+
plugin.on_content_registered(
|
|
96
|
+
cid = tx.cid,
|
|
97
|
+
owner = tx.to_addr,
|
|
98
|
+
platform = tx.data.get("platform_id", "unknown")
|
|
99
|
+
if tx.data else "unknown",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
elif tx.tx_type == "edition_transfer" and tx.cid:
|
|
103
|
+
plugin.on_edition_transferred(
|
|
104
|
+
cid = tx.cid,
|
|
105
|
+
edition_number = tx.edition_number or 0,
|
|
106
|
+
from_wallet = tx.from_addr,
|
|
107
|
+
to_wallet = tx.to_addr,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.error(
|
|
112
|
+
f"Plugin {plugin.plugin_id} block notification error: {e}",
|
|
113
|
+
exc_info=True,
|
|
114
|
+
)
|
|
115
|
+
# Never let a plugin crash the node
|
|
116
|
+
|
|
117
|
+
def notify_transaction(self, tx: "Transaction"):
|
|
118
|
+
"""Notify all plugins of a new transaction (before block)."""
|
|
119
|
+
for plugin in self._plugins.values():
|
|
120
|
+
try:
|
|
121
|
+
plugin.on_transaction(tx)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error(
|
|
124
|
+
f"Plugin {plugin.plugin_id} tx notification error: {e}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def get_status(self) -> Dict:
|
|
128
|
+
return {
|
|
129
|
+
"plugin_count": len(self._plugins),
|
|
130
|
+
"plugins": [p.get_info() for p in self._plugins.values()],
|
|
131
|
+
}
|