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,540 @@
1
+ """
2
+ PlayWebit Network — NVF-BFT Consensus Engine
3
+ Based on NullVoid Framework paper concepts.
4
+
5
+ Round phases mapped to NVF paper:
6
+ PROPOSE → Spacetime Fabric activation (leader broadcasts)
7
+ PREPARE → Six Directional Progressions (peers validate)
8
+ VOTE → Planck Threshold approach (peers sign + broadcast)
9
+ COMMIT → False Vacuum → True Vacuum (2/3 quorum = finalised)
10
+ ANCHOR → Null Void Layer 0 anchoring (write to storage)
11
+ NOTIFY → Cyclic re-entry notification (plugins + peers notified)
12
+
13
+ Tolerates up to f < n/3 faulty nodes (standard BFT guarantee).
14
+
15
+ Sybil Resistance — Option A (Authority Whitelist):
16
+ Only nodes with a node_register tx signed by AUTHORITY_WALLET
17
+ can participate in consensus voting.
18
+ Attacker nodes are silently ignored even if connected.
19
+ """
20
+
21
+ import time
22
+ import threading
23
+ import logging
24
+ from typing import Dict, List, Optional, Callable, Set
25
+
26
+ from playweb.consensus.leader import LeaderElection
27
+ from playweb.consensus.vote import Vote
28
+ from playweb.config import (
29
+ CONSENSUS_QUORUM,
30
+ BLOCK_TIME,
31
+ CONSENSUS_TIMEOUT,
32
+ BATCH_TIMEOUT,
33
+ MAX_TX_PER_BLOCK,
34
+ AUTHORITY_WALLET,
35
+ )
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class ConsensusRound:
41
+ """State for a single consensus round."""
42
+
43
+ def __init__(self, block_index: int, block):
44
+ self.block_index = block_index
45
+ self.block = block
46
+ self.phase = "PROPOSE"
47
+ self.votes: Dict[str, Vote] = {} # voter_wallet → Vote
48
+ self.started_at = time.time()
49
+ self.finalised = False
50
+ self.failed = False
51
+
52
+ def add_vote(self, vote: Vote) -> bool:
53
+ """Add a vote. Returns True if new vote added."""
54
+ if vote.voter_wallet in self.votes:
55
+ return False
56
+ if not vote.verify():
57
+ logger.warning(
58
+ f"Invalid vote signature from {vote.voter_wallet[:8]}"
59
+ )
60
+ return False
61
+ self.votes[vote.voter_wallet] = vote
62
+ return True
63
+
64
+ def has_quorum(
65
+ self,
66
+ active_node_count: int,
67
+ node_wallet: str = "",
68
+ ) -> bool:
69
+ """
70
+ Check if 2/3 quorum has been reached.
71
+
72
+ Single node rules:
73
+ - Authority node alone → can mine (bootstrapping phase)
74
+ - Non-authority node alone → must wait for peers (security)
75
+ """
76
+ if active_node_count == 0:
77
+ return False
78
+
79
+ # Single node special case
80
+ if active_node_count == 1:
81
+ if node_wallet.lower() == AUTHORITY_WALLET.lower():
82
+ return len(self.votes) >= 1
83
+ else:
84
+ return False
85
+
86
+ # Multiple nodes — standard 2/3 quorum
87
+ return len(self.votes) / active_node_count >= CONSENSUS_QUORUM
88
+
89
+ def is_timed_out(self) -> bool:
90
+ return time.time() - self.started_at > CONSENSUS_TIMEOUT
91
+
92
+
93
+ class NVFBFTConsensus:
94
+
95
+ def __init__(
96
+ self,
97
+ blockchain,
98
+ peer_manager,
99
+ gossip,
100
+ node_wallet: str,
101
+ node_private_key: str,
102
+ plugin_manager = None,
103
+ on_block_finalised: Optional[Callable] = None,
104
+ ):
105
+ self.blockchain = blockchain
106
+ self.peer_manager = peer_manager
107
+ self.gossip = gossip
108
+ self.node_wallet = node_wallet.lower()
109
+ self.node_private_key = node_private_key
110
+ self.plugin_manager = plugin_manager
111
+ self.on_block_finalised = on_block_finalised
112
+
113
+ self.leader_election = LeaderElection()
114
+ self.current_round: Optional[ConsensusRound] = None
115
+ self._lock = threading.Lock()
116
+ self._running = False
117
+ self._thread: Optional[threading.Thread] = None
118
+
119
+ # Batch mining timer
120
+ self._batch_timer_start: Optional[float] = None
121
+
122
+ # Sybil resistance — registered node wallet cache
123
+ # Loaded from chain on startup, updated on each new block
124
+ self._registered_nodes: Set[str] = set()
125
+ self._registered_nodes_loaded: bool = False
126
+
127
+ # ─────────────────────────────────────────────────────────────
128
+ # Lifecycle
129
+ # ─────────────────────────────────────────────────────────────
130
+
131
+ def start(self):
132
+ """Start the consensus loop in a background thread."""
133
+ # Load registered nodes from chain before starting
134
+ self._load_registered_nodes()
135
+
136
+ self._running = True
137
+ self._thread = threading.Thread(
138
+ target = self._consensus_loop,
139
+ daemon = True,
140
+ name = "nvf-bft-consensus",
141
+ )
142
+ self._thread.start()
143
+ logger.info(
144
+ f"NVF-BFT consensus started — "
145
+ f"{len(self._registered_nodes)} registered validators"
146
+ )
147
+
148
+ def stop(self):
149
+ self._running = False
150
+ if self._thread:
151
+ self._thread.join(timeout=5)
152
+ logger.info("NVF-BFT consensus stopped")
153
+
154
+ # ─────────────────────────────────────────────────────────────
155
+ # Sybil Resistance — Registered Node Registry
156
+ # ─────────────────────────────────────────────────────────────
157
+
158
+ def _load_registered_nodes(self):
159
+ """
160
+ Load all registered validator wallets from chain.
161
+ Scans for node_register transactions signed by AUTHORITY_WALLET.
162
+ Authority wallet is always included (bootstrapping).
163
+ Called on startup and when new blocks arrive.
164
+ """
165
+ registered = {AUTHORITY_WALLET.lower()}
166
+
167
+ try:
168
+ length = self.blockchain.get_chain_length()
169
+ scan_from = max(0, length - 100000)
170
+ blocks = self.blockchain.get_blocks_from(scan_from, 100000)
171
+
172
+ for block in blocks:
173
+ for tx in block.transactions:
174
+ if (
175
+ tx.tx_type == "node_register"
176
+ and tx.from_addr == AUTHORITY_WALLET.lower()
177
+ ):
178
+ registered.add(tx.to_addr.lower())
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error loading registered nodes: {e}")
182
+
183
+ self._registered_nodes = registered
184
+ self._registered_nodes_loaded = True
185
+ logger.info(
186
+ f"Registered validators: {len(registered)} "
187
+ f"({', '.join(list(registered)[:3])}...)"
188
+ )
189
+
190
+ def _is_registered_node(self, wallet: str) -> bool:
191
+ """
192
+ Check if a wallet is a registered validator.
193
+ Authority wallet is always registered.
194
+ Other nodes must have a node_register tx on chain.
195
+ """
196
+ if not self._registered_nodes_loaded:
197
+ self._load_registered_nodes()
198
+ return wallet.lower() in self._registered_nodes
199
+
200
+ def _update_registered_nodes(self, block):
201
+ """
202
+ Update registered nodes cache from a newly finalised block.
203
+ Called after ANCHOR phase so new validators take effect
204
+ from the very next block.
205
+ """
206
+ for tx in block.transactions:
207
+ if (
208
+ tx.tx_type == "node_register"
209
+ and tx.from_addr == AUTHORITY_WALLET.lower()
210
+ ):
211
+ self._registered_nodes.add(tx.to_addr.lower())
212
+ logger.info(
213
+ f"New validator registered on chain: "
214
+ f"{tx.to_addr[:12]}... "
215
+ f"(platform: {tx.data.get('platform_id', 'unknown') if tx.data else 'unknown'})"
216
+ )
217
+
218
+ # ─────────────────────────────────────────────────────────────
219
+ # Main consensus loop — Batch Mining
220
+ # ─────────────────────────────────────────────────────────────
221
+
222
+ def _consensus_loop(self):
223
+ """
224
+ Batch mining loop:
225
+ - Never mines empty blocks
226
+ - Starts timer when first tx hits mempool
227
+ - Mines immediately at MAX_TX_PER_BLOCK
228
+ - Mines with pending txs when BATCH_TIMEOUT expires
229
+ """
230
+ while self._running:
231
+ try:
232
+ pending = self.blockchain.mempool.size()
233
+
234
+ if pending == 0:
235
+ # No transactions — reset timer, wait
236
+ self._batch_timer_start = None
237
+ time.sleep(5)
238
+ continue
239
+
240
+ # First tx arrived — start timer
241
+ if self._batch_timer_start is None:
242
+ self._batch_timer_start = time.time()
243
+ logger.info(
244
+ f"Batch timer started — "
245
+ f"{pending} tx(s) in mempool, "
246
+ f"mining in {BATCH_TIMEOUT}s "
247
+ f"or at {MAX_TX_PER_BLOCK} txs"
248
+ )
249
+
250
+ elapsed = time.time() - self._batch_timer_start
251
+ should_mine = (
252
+ pending >= MAX_TX_PER_BLOCK # max txs → mine now
253
+ or elapsed >= BATCH_TIMEOUT # timeout → mine now
254
+ )
255
+
256
+ if should_mine:
257
+ reason = (
258
+ "max_txs" if pending >= MAX_TX_PER_BLOCK
259
+ else "timeout"
260
+ )
261
+ logger.info(
262
+ f"Mining: {pending} txs | "
263
+ f"elapsed={elapsed:.0f}s | "
264
+ f"reason={reason}"
265
+ )
266
+ self._run_round()
267
+ self._batch_timer_start = None # reset after mining
268
+
269
+ except Exception as e:
270
+ logger.error(f"Consensus error: {e}", exc_info=True)
271
+ self._batch_timer_start = None
272
+
273
+ time.sleep(5) # check every 5 seconds
274
+
275
+ def _run_round(self):
276
+ """Execute one consensus round."""
277
+ tip = self.blockchain.get_chain_tip()
278
+ next_index = (tip.index + 1) if tip else 1
279
+
280
+ # Only registered nodes count as active validators
281
+ active_nodes = [
282
+ p.wallet
283
+ for p in self.peer_manager.get_active_peers()
284
+ if self._is_registered_node(p.wallet)
285
+ ]
286
+
287
+ # Include self if registered
288
+ if self._is_registered_node(self.node_wallet):
289
+ if self.node_wallet not in active_nodes:
290
+ active_nodes.append(self.node_wallet)
291
+
292
+ if not active_nodes:
293
+ logger.warning("No registered validators available")
294
+ return
295
+
296
+ # PHASE: PROPOSE — NVF: Spacetime Fabric activation
297
+ am_leader = self.leader_election.is_leader(
298
+ self.node_wallet, next_index, active_nodes
299
+ )
300
+
301
+ if am_leader:
302
+ logger.info(
303
+ f"Round {next_index}: I am leader "
304
+ f"({self.node_wallet[:8]}...) "
305
+ f"[{len(active_nodes)} validators]"
306
+ )
307
+ self._propose(next_index)
308
+ else:
309
+ leader = self.leader_election.get_leader(next_index, active_nodes)
310
+ logger.debug(
311
+ f"Round {next_index}: waiting for leader "
312
+ f"{leader[:8] if leader else 'unknown'}..."
313
+ )
314
+
315
+ # ─────────────────────────────────────────────────────────────
316
+ # Phase: PROPOSE — NVF: Spacetime Fabric activation
317
+ # ─────────────────────────────────────────────────────────────
318
+
319
+ def _propose(self, block_index: int):
320
+ """Leader creates and broadcasts a block proposal."""
321
+ block = self.blockchain.create_block(self.node_wallet)
322
+ if not block:
323
+ logger.debug(f"Round {block_index}: no transactions to propose")
324
+ return
325
+
326
+ with self._lock:
327
+ self.current_round = ConsensusRound(block_index, block)
328
+ self.current_round.phase = "PREPARE"
329
+
330
+ logger.info(
331
+ f"PROPOSE: block {block_index}, "
332
+ f"{len(block.transactions)} txs, "
333
+ f"hash={block.hash[:12]}..."
334
+ )
335
+
336
+ # Broadcast to all peers
337
+ self.gossip.broadcast_block_proposal(
338
+ block = block,
339
+ round_number = block_index,
340
+ peers = self.peer_manager.get_active_peers(),
341
+ )
342
+
343
+ # Vote for own proposal
344
+ self._prepare_and_vote(block, block_index)
345
+
346
+ # ─────────────────────────────────────────────────────────────
347
+ # Phase: PREPARE — NVF: Six Directional Progressions
348
+ # ─────────────────────────────────────────────────────────────
349
+
350
+ def on_propose(self, block, round_number: int, from_peer: str):
351
+ """
352
+ Called when we receive a block proposal from the leader.
353
+ Validates and moves to VOTE phase.
354
+ """
355
+ # Only accept proposals from registered nodes
356
+ if not self._is_registered_node(from_peer):
357
+ logger.warning(
358
+ f"Proposal from unregistered node "
359
+ f"{from_peer[:8]}... ignored (Sybil protection)"
360
+ )
361
+ return
362
+
363
+ logger.info(
364
+ f"PREPARE: received proposal for block {round_number} "
365
+ f"from {from_peer[:8]}..."
366
+ )
367
+
368
+ tip = self.blockchain.get_chain_tip()
369
+ valid, reason = block.validate(previous_block=tip)
370
+
371
+ if not valid:
372
+ logger.warning(f"Proposal rejected: {reason}")
373
+ return
374
+
375
+ with self._lock:
376
+ self.current_round = ConsensusRound(round_number, block)
377
+ self.current_round.phase = "VOTE"
378
+
379
+ self._prepare_and_vote(block, round_number)
380
+
381
+ def _prepare_and_vote(self, block, round_number: int):
382
+ """Create and broadcast our vote for this block."""
383
+ vote = Vote(
384
+ block_hash = block.hash,
385
+ voter_wallet = self.node_wallet,
386
+ round_number = round_number,
387
+ phase = "VOTE",
388
+ )
389
+ vote.sign(self.node_private_key)
390
+
391
+ self.gossip.broadcast_vote(
392
+ vote = vote,
393
+ peers = self.peer_manager.get_active_peers(),
394
+ )
395
+
396
+ self.on_vote(vote)
397
+
398
+ # ─────────────────────────────────────────────────────────────
399
+ # Phase: VOTE → COMMIT
400
+ # NVF: False Vacuum → True Vacuum (quorum = finality)
401
+ # ─────────────────────────────────────────────────────────────
402
+
403
+ def on_vote(self, vote: Vote):
404
+ """
405
+ Called when we receive a vote from any peer (or self).
406
+ If 2/3 quorum reached → COMMIT.
407
+ """
408
+ with self._lock:
409
+ if not self.current_round:
410
+ return
411
+
412
+ # Sybil resistance — only registered nodes can vote
413
+ if not self._is_registered_node(vote.voter_wallet):
414
+ logger.warning(
415
+ f"Vote ignored — unregistered node: "
416
+ f"{vote.voter_wallet[:12]}... (Sybil protection)"
417
+ )
418
+ return
419
+
420
+ if vote.block_hash != self.current_round.block.hash:
421
+ logger.debug(
422
+ f"Vote for unknown block "
423
+ f"{vote.block_hash[:12]}... ignored"
424
+ )
425
+ return
426
+
427
+ added = self.current_round.add_vote(vote)
428
+ if not added:
429
+ return
430
+
431
+ # Count only registered active nodes
432
+ registered_peers = [
433
+ p for p in self.peer_manager.get_active_peers()
434
+ if self._is_registered_node(p.wallet)
435
+ ]
436
+ active_count = len(registered_peers) + 1 # +1 for self
437
+ vote_count = len(self.current_round.votes)
438
+
439
+ logger.debug(
440
+ f"VOTE: {vote_count}/{active_count} registered votes "
441
+ f"for block {self.current_round.block_index}"
442
+ )
443
+
444
+ if self.current_round.has_quorum(active_count, self.node_wallet):
445
+ if not self.current_round.finalised:
446
+ self.current_round.finalised = True
447
+ self._commit(
448
+ self.current_round.block,
449
+ list(self.current_round.votes.values()),
450
+ )
451
+
452
+ # ─────────────────────────────────────────────────────────────
453
+ # Phase: COMMIT → ANCHOR → NOTIFY
454
+ # NVF: True Vacuum → Null Void anchoring → Cyclic re-entry
455
+ # ─────────────────────────────────────────────────────────────
456
+
457
+ def _commit(self, block, votes: List[Vote]):
458
+ """
459
+ 2/3 quorum reached. Finalise the block.
460
+ COMMIT → ANCHOR → NOTIFY
461
+ """
462
+ logger.info(
463
+ f"COMMIT: block {block.index} finalised with "
464
+ f"{len(votes)} votes"
465
+ )
466
+
467
+ # ANCHOR — NVF Layer 0
468
+ vote_dicts = [v.to_dict() for v in votes]
469
+ success, reason = self.blockchain.add_block(
470
+ block = block,
471
+ votes = vote_dicts,
472
+ node_wallet = self.node_wallet,
473
+ )
474
+
475
+ if not success:
476
+ logger.error(f"Block commit failed: {reason}")
477
+ return
478
+
479
+ logger.info(
480
+ f"ANCHOR: block {block.index} written "
481
+ f"({block.hash[:16]}...)"
482
+ )
483
+
484
+ # Update registered nodes cache from this block
485
+ # New node_register txs take effect immediately
486
+ self._update_registered_nodes(block)
487
+
488
+ # NOTIFY — Cyclic re-entry
489
+ # 1. Notify plugins
490
+ if self.plugin_manager:
491
+ self.plugin_manager.notify_block_finalised(block)
492
+
493
+ # 2. Notify node (broadcasts to peers)
494
+ if self.on_block_finalised:
495
+ self.on_block_finalised(block, vote_dicts)
496
+
497
+ logger.info(
498
+ f"NOTIFY: block {block.index} notifications sent"
499
+ )
500
+
501
+ # ─────────────────────────────────────────────────────────────
502
+ # Timeout handling
503
+ # ─────────────────────────────────────────────────────────────
504
+
505
+ def check_timeout(self):
506
+ """Check if current round timed out and reset if so."""
507
+ with self._lock:
508
+ if (
509
+ self.current_round
510
+ and self.current_round.is_timed_out()
511
+ ):
512
+ logger.warning(
513
+ f"Consensus round {self.current_round.block_index} "
514
+ f"timed out — resetting"
515
+ )
516
+ self.current_round = None
517
+
518
+ # ─────────────────────────────────────────────────────────────
519
+ # Status
520
+ # ─────────────────────────────────────────────────────────────
521
+
522
+ def get_status(self) -> Dict:
523
+ with self._lock:
524
+ base = {
525
+ "registered_validators": len(self._registered_nodes),
526
+ }
527
+ if not self.current_round:
528
+ return {**base, "status": "idle"}
529
+ return {
530
+ **base,
531
+ "status": "in_round",
532
+ "block_index": self.current_round.block_index,
533
+ "phase": self.current_round.phase,
534
+ "votes": len(self.current_round.votes),
535
+ "finalised": self.current_round.finalised,
536
+ }
537
+
538
+ def get_registered_validators(self) -> List[str]:
539
+ """Get list of all registered validator wallets."""
540
+ return list(self._registered_nodes)
@@ -0,0 +1,108 @@
1
+ """
2
+ PlayWebit Network — Vote
3
+ NVF-BFT vote message. Signed by each validator.
4
+ """
5
+
6
+ import hashlib
7
+ import json
8
+ import time
9
+ import logging
10
+ from typing import Dict
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Vote:
16
+
17
+ def __init__(
18
+ self,
19
+ block_hash: str,
20
+ voter_wallet: str,
21
+ round_number: int,
22
+ phase: str, # PREPARE | VOTE | COMMIT
23
+ timestamp: float = None,
24
+ signature: str = None,
25
+ ):
26
+ self.block_hash = block_hash
27
+ self.voter_wallet = voter_wallet.lower()
28
+ self.round_number = round_number
29
+ self.phase = phase
30
+ self.timestamp = timestamp or time.time()
31
+ self.signature = signature
32
+ self.hash = self._calculate_hash()
33
+
34
+ def _calculate_hash(self) -> str:
35
+ raw = json.dumps({
36
+ "block_hash": self.block_hash,
37
+ "voter_wallet": self.voter_wallet,
38
+ "round_number": self.round_number,
39
+ "phase": self.phase,
40
+ "timestamp": self.timestamp,
41
+ }, sort_keys=True)
42
+ return hashlib.sha256(raw.encode()).hexdigest()
43
+
44
+ def sign(self, private_key: str) -> bool:
45
+ """Sign the vote with the node's private key."""
46
+ try:
47
+ from eth_account import Account
48
+ from eth_account.messages import encode_defunct
49
+
50
+ msg = encode_defunct(text=self.hash)
51
+ signed = Account.sign_message(msg, private_key=private_key)
52
+ self.signature = signed.signature.hex()
53
+ return True
54
+ except ImportError:
55
+ # Basic placeholder if eth_account not available
56
+ self.signature = f"0x{'0' * 130}"
57
+ return True
58
+ except Exception as e:
59
+ logger.error(f"Vote signing failed: {e}")
60
+ return False
61
+
62
+ def verify(self) -> bool:
63
+ """Verify the vote signature."""
64
+ if not self.signature:
65
+ return False
66
+ try:
67
+ from eth_account import Account
68
+ from eth_account.messages import encode_defunct
69
+
70
+ msg = encode_defunct(text=self.hash)
71
+ recovered = Account.recover_message(msg, signature=self.signature)
72
+ return recovered.lower() == self.voter_wallet
73
+ except ImportError:
74
+ return bool(self.signature)
75
+ except Exception as e:
76
+ logger.error(f"Vote verification failed: {e}")
77
+ return False
78
+
79
+ def to_dict(self) -> Dict:
80
+ return {
81
+ "hash": self.hash,
82
+ "block_hash": self.block_hash,
83
+ "voter_wallet": self.voter_wallet,
84
+ "round_number": self.round_number,
85
+ "phase": self.phase,
86
+ "timestamp": self.timestamp,
87
+ "signature": self.signature,
88
+ }
89
+
90
+ @classmethod
91
+ def from_dict(cls, d: Dict) -> "Vote":
92
+ v = cls(
93
+ block_hash = d["block_hash"],
94
+ voter_wallet = d["voter_wallet"],
95
+ round_number = d["round_number"],
96
+ phase = d["phase"],
97
+ timestamp = d.get("timestamp"),
98
+ signature = d.get("signature"),
99
+ )
100
+ v.hash = d.get("hash", v.hash)
101
+ return v
102
+
103
+ def __repr__(self):
104
+ return (
105
+ f"Vote(phase={self.phase}, "
106
+ f"voter={self.voter_wallet[:8]}..., "
107
+ f"block={self.block_hash[:12]}...)"
108
+ )
@@ -0,0 +1,6 @@
1
+ from playweb.core.transaction import Transaction
2
+ from playweb.core.block import Block
3
+ from playweb.core.blockchain import Blockchain
4
+ from playweb.core.mempool import Mempool
5
+
6
+ __all__ = ["Transaction", "Block", "Blockchain", "Mempool"]