lunalib 1.5.0__py3-none-any.whl → 1.5.2__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.
@@ -0,0 +1,638 @@
1
+ # lunalib/wallet_manager.py
2
+ """
3
+ Unified Wallet State Manager
4
+
5
+ Provides a single source of truth for wallet balances, transaction history,
6
+ pending transactions, transfers, and rewards. Scans the blockchain once and
7
+ efficiently distributes transactions to multiple wallets while maintaining
8
+ real-time updates available to the UI.
9
+ """
10
+
11
+ import threading
12
+ import time
13
+ from typing import Dict, List, Optional, Callable, Set, Tuple
14
+ from dataclasses import dataclass, field, asdict
15
+ from enum import Enum
16
+ import json
17
+ from datetime import datetime
18
+
19
+
20
+ class TransactionType(Enum):
21
+ """Types of transactions"""
22
+ TRANSFER = "transfer"
23
+ REWARD = "reward"
24
+ GTX_GENESIS = "gtx_genesis"
25
+ INCOMING = "incoming"
26
+ OUTGOING = "outgoing"
27
+
28
+
29
+ class TransactionStatus(Enum):
30
+ """Status of a transaction"""
31
+ PENDING = "pending"
32
+ CONFIRMED = "confirmed"
33
+ FAILED = "failed"
34
+
35
+
36
+ @dataclass
37
+ class Transaction:
38
+ """Represents a single transaction"""
39
+ hash: str
40
+ type: str
41
+ from_address: str
42
+ to_address: str
43
+ amount: float
44
+ fee: float = 0.0
45
+ timestamp: int = 0
46
+ status: str = TransactionStatus.PENDING.value
47
+ block_height: Optional[int] = None
48
+ confirmations: int = 0
49
+ memo: str = ""
50
+ direction: str = "" # 'incoming', 'outgoing', or '' for self
51
+
52
+ def to_dict(self) -> Dict:
53
+ """Convert transaction to dictionary"""
54
+ return asdict(self)
55
+
56
+
57
+ @dataclass
58
+ class WalletBalance:
59
+ """Wallet balance information"""
60
+ total_balance: float = 0.0
61
+ available_balance: float = 0.0
62
+ pending_incoming: float = 0.0
63
+ pending_outgoing: float = 0.0
64
+ confirmed_balance: float = 0.0
65
+
66
+ def to_dict(self) -> Dict:
67
+ """Convert to dictionary"""
68
+ return asdict(self)
69
+
70
+
71
+ @dataclass
72
+ class WalletState:
73
+ """Complete state for a single wallet"""
74
+ address: str
75
+ balance: WalletBalance = field(default_factory=WalletBalance)
76
+ confirmed_transactions: List[Transaction] = field(default_factory=list)
77
+ pending_transactions: List[Transaction] = field(default_factory=list)
78
+ confirmed_transfers: List[Transaction] = field(default_factory=list)
79
+ pending_transfers: List[Transaction] = field(default_factory=list)
80
+ rewards: List[Transaction] = field(default_factory=list)
81
+ genesis_transactions: List[Transaction] = field(default_factory=list)
82
+ last_updated: float = 0.0
83
+
84
+ def to_dict(self) -> Dict:
85
+ """Convert to dictionary, converting transactions to dicts"""
86
+ return {
87
+ 'address': self.address,
88
+ 'balance': self.balance.to_dict(),
89
+ 'confirmed_transactions': [tx.to_dict() for tx in self.confirmed_transactions],
90
+ 'pending_transactions': [tx.to_dict() for tx in self.pending_transactions],
91
+ 'confirmed_transfers': [tx.to_dict() for tx in self.confirmed_transfers],
92
+ 'pending_transfers': [tx.to_dict() for tx in self.pending_transfers],
93
+ 'rewards': [tx.to_dict() for tx in self.rewards],
94
+ 'genesis_transactions': [tx.to_dict() for tx in self.genesis_transactions],
95
+ 'last_updated': self.last_updated
96
+ }
97
+
98
+
99
+ class WalletStateManager:
100
+ """
101
+ Unified wallet manager that:
102
+ 1. Scans blockchain once for all wallets
103
+ 2. Efficiently categorizes transactions (transfers, rewards, genesis)
104
+ 3. Tracks pending transactions from mempool
105
+ 4. Maintains real-time balance calculations
106
+ 5. Provides immediate UI updates via callbacks
107
+ """
108
+
109
+ def __init__(self):
110
+ self.wallet_states: Dict[str, WalletState] = {}
111
+ self.state_lock = threading.RLock()
112
+ self.balance_callbacks: List[Callable] = []
113
+ self.transaction_callbacks: List[Callable] = []
114
+
115
+ # Cache for pending transactions to avoid duplicate processing
116
+ self.processed_pending_hashes: Set[str] = set()
117
+ self.processed_confirmed_hashes: Set[str] = set()
118
+
119
+ # Sync control
120
+ self.last_sync_time = 0
121
+ self.sync_in_progress = False
122
+ self.last_blockchain_height = 0
123
+
124
+ # =========================================================================
125
+ # Address normalization
126
+ # =========================================================================
127
+
128
+ def _normalize_address(self, addr: str) -> str:
129
+ """Normalize address for comparison"""
130
+ if not addr:
131
+ return ''
132
+ addr_str = str(addr).strip("'\" ").lower()
133
+ return addr_str[4:] if addr_str.startswith('lun_') else addr_str
134
+
135
+ def _addresses_match(self, addr1: str, addr2: str) -> bool:
136
+ """Check if two addresses match (after normalization)"""
137
+ return self._normalize_address(addr1) == self._normalize_address(addr2)
138
+
139
+ # =========================================================================
140
+ # Wallet management
141
+ # =========================================================================
142
+
143
+ def register_wallet(self, address: str) -> WalletState:
144
+ """Register a new wallet to track"""
145
+ with self.state_lock:
146
+ if address not in self.wallet_states:
147
+ self.wallet_states[address] = WalletState(address=address)
148
+ print(f"📱 Registered wallet: {address}")
149
+ return self.wallet_states[address]
150
+
151
+ def register_wallets(self, addresses: List[str]) -> Dict[str, WalletState]:
152
+ """Register multiple wallets"""
153
+ states = {}
154
+ for addr in addresses:
155
+ states[addr] = self.register_wallet(addr)
156
+ return states
157
+
158
+ def get_wallet_state(self, address: str) -> Optional[WalletState]:
159
+ """Get current state of a wallet"""
160
+ with self.state_lock:
161
+ return self.wallet_states.get(address)
162
+
163
+ def get_all_wallet_states(self) -> Dict[str, WalletState]:
164
+ """Get states for all registered wallets"""
165
+ with self.state_lock:
166
+ return dict(self.wallet_states)
167
+
168
+ # =========================================================================
169
+ # Transaction categorization and processing
170
+ # =========================================================================
171
+
172
+ def _categorize_confirmed_transaction(self, tx: Transaction, address: str) -> List[str]:
173
+ """
174
+ Categorize a confirmed transaction and return which lists to add it to.
175
+ Returns list of category names.
176
+ """
177
+ categories = ['confirmed_transactions']
178
+
179
+ tx_type_lower = (tx.type or '').lower()
180
+
181
+ # Categorize by type
182
+ if tx_type_lower == 'reward' or tx.from_address == 'network':
183
+ categories.append('rewards')
184
+ elif tx_type_lower == 'gtx_genesis':
185
+ categories.append('genesis_transactions')
186
+ elif tx_type_lower in ['transfer', '']:
187
+ categories.append('confirmed_transfers')
188
+
189
+ return categories
190
+
191
+ def _categorize_pending_transaction(self, tx: Transaction, address: str) -> List[str]:
192
+ """
193
+ Categorize a pending transaction and return which lists to add it to.
194
+ Returns list of category names.
195
+ """
196
+ categories = ['pending_transactions']
197
+
198
+ tx_type_lower = (tx.type or '').lower()
199
+
200
+ # Categorize by type
201
+ if tx_type_lower == 'reward':
202
+ categories.append('rewards')
203
+ elif tx_type_lower == 'gtx_genesis':
204
+ categories.append('genesis_transactions')
205
+ elif tx_type_lower in ['transfer', '']:
206
+ categories.append('pending_transfers')
207
+
208
+ return categories
209
+
210
+ def _process_blockchain_transactions(self, raw_transactions: Dict[str, List[Dict]]) -> Dict[str, List[Transaction]]:
211
+ """
212
+ Convert raw blockchain transactions to Transaction objects and
213
+ categorize them by address and type.
214
+
215
+ Returns mapping of address -> list of Transaction objects
216
+ """
217
+ processed = {}
218
+
219
+ for address, raw_txs in raw_transactions.items():
220
+ transactions = []
221
+
222
+ for raw_tx in raw_txs:
223
+ try:
224
+ # Determine transaction direction
225
+ direction = ""
226
+ if self._addresses_match(raw_tx.get('from'), address):
227
+ direction = 'outgoing'
228
+ if self._addresses_match(raw_tx.get('to'), address):
229
+ direction = 'incoming'
230
+
231
+ # Create Transaction object
232
+ tx = Transaction(
233
+ hash=raw_tx.get('hash', ''),
234
+ type=raw_tx.get('type', 'transfer'),
235
+ from_address=raw_tx.get('from', ''),
236
+ to_address=raw_tx.get('to', ''),
237
+ amount=float(raw_tx.get('amount', 0)),
238
+ fee=float(raw_tx.get('fee', 0)),
239
+ timestamp=int(raw_tx.get('timestamp', 0)),
240
+ status=TransactionStatus.CONFIRMED.value,
241
+ block_height=raw_tx.get('block_height'),
242
+ confirmations=raw_tx.get('confirmations', 1),
243
+ memo=raw_tx.get('memo', ''),
244
+ direction=direction
245
+ )
246
+
247
+ # Skip if we've already processed this
248
+ if tx.hash not in self.processed_confirmed_hashes:
249
+ transactions.append(tx)
250
+ self.processed_confirmed_hashes.add(tx.hash)
251
+
252
+ except Exception as e:
253
+ print(f"⚠️ Error processing transaction {raw_tx.get('hash')}: {e}")
254
+ continue
255
+
256
+ if transactions:
257
+ processed[address] = transactions
258
+
259
+ return processed
260
+
261
+ def _process_mempool_transactions(self, raw_transactions: Dict[str, List[Dict]]) -> Dict[str, List[Transaction]]:
262
+ """
263
+ Convert raw mempool transactions to Transaction objects.
264
+
265
+ Returns mapping of address -> list of Transaction objects
266
+ """
267
+ processed = {}
268
+
269
+ for address, raw_txs in raw_transactions.items():
270
+ transactions = []
271
+
272
+ for raw_tx in raw_txs:
273
+ try:
274
+ # Determine transaction direction
275
+ direction = ""
276
+ if self._addresses_match(raw_tx.get('from'), address):
277
+ direction = 'outgoing'
278
+ if self._addresses_match(raw_tx.get('to'), address):
279
+ direction = 'incoming'
280
+
281
+ # Create Transaction object
282
+ tx = Transaction(
283
+ hash=raw_tx.get('hash', ''),
284
+ type=raw_tx.get('type', 'transfer'),
285
+ from_address=raw_tx.get('from', ''),
286
+ to_address=raw_tx.get('to', ''),
287
+ amount=float(raw_tx.get('amount', 0)),
288
+ fee=float(raw_tx.get('fee', 0)),
289
+ timestamp=int(raw_tx.get('timestamp', 0)),
290
+ status=TransactionStatus.PENDING.value,
291
+ memo=raw_tx.get('memo', ''),
292
+ direction=direction
293
+ )
294
+
295
+ # Skip if we've already processed this
296
+ if tx.hash not in self.processed_pending_hashes:
297
+ transactions.append(tx)
298
+ self.processed_pending_hashes.add(tx.hash)
299
+
300
+ except Exception as e:
301
+ print(f"⚠️ Error processing pending transaction {raw_tx.get('hash')}: {e}")
302
+ continue
303
+
304
+ if transactions:
305
+ processed[address] = transactions
306
+
307
+ return processed
308
+
309
+ # =========================================================================
310
+ # Balance calculation
311
+ # =========================================================================
312
+
313
+ def _calculate_balance_from_transactions(self, address: str,
314
+ confirmed_txs: List[Transaction],
315
+ pending_txs: List[Transaction]) -> WalletBalance:
316
+ """Calculate wallet balance from confirmed and pending transactions"""
317
+
318
+ confirmed_balance = 0.0
319
+ pending_incoming = 0.0
320
+ pending_outgoing = 0.0
321
+
322
+ # Process confirmed transactions
323
+ for tx in confirmed_txs:
324
+ if self._addresses_match(tx.from_address, address):
325
+ # Outgoing: subtract amount and fee
326
+ confirmed_balance -= (tx.amount + tx.fee)
327
+ elif self._addresses_match(tx.to_address, address):
328
+ # Incoming: add amount
329
+ confirmed_balance += tx.amount
330
+ elif tx.type == 'reward' or tx.from_address == 'network':
331
+ # Reward received
332
+ confirmed_balance += tx.amount
333
+
334
+ # Process pending transactions
335
+ for tx in pending_txs:
336
+ if self._addresses_match(tx.from_address, address):
337
+ # Pending outgoing
338
+ pending_outgoing += (tx.amount + tx.fee)
339
+ elif self._addresses_match(tx.to_address, address):
340
+ # Pending incoming
341
+ pending_incoming += tx.amount
342
+
343
+ # Calculate derived balances
344
+ confirmed_balance = max(0.0, confirmed_balance)
345
+ total_balance = confirmed_balance + pending_incoming
346
+ available_balance = max(0.0, confirmed_balance - pending_outgoing)
347
+
348
+ return WalletBalance(
349
+ total_balance=total_balance,
350
+ available_balance=available_balance,
351
+ pending_incoming=pending_incoming,
352
+ pending_outgoing=pending_outgoing,
353
+ confirmed_balance=confirmed_balance
354
+ )
355
+
356
+ # =========================================================================
357
+ # Sync operations (blockchain + mempool)
358
+ # =========================================================================
359
+
360
+ def sync_wallets_from_sources(self, blockchain_transactions: Dict[str, List[Dict]],
361
+ mempool_transactions: Dict[str, List[Dict]]):
362
+ """
363
+ Main sync method: takes raw blockchain and mempool data and updates all wallets.
364
+
365
+ This is designed to be called with data from:
366
+ - blockchain.scan_transactions_for_addresses()
367
+ - mempool.get_pending_transactions_for_addresses()
368
+
369
+ The method efficiently:
370
+ 1. Processes and categorizes transactions
371
+ 2. Updates all wallet states
372
+ 3. Calculates balances
373
+ 4. Triggers callbacks for UI updates
374
+ """
375
+
376
+ with self.state_lock:
377
+ print(f"\n🔄 Syncing wallets...")
378
+ sync_start = time.time()
379
+
380
+ # Process blockchain transactions
381
+ confirmed_map = self._process_blockchain_transactions(blockchain_transactions)
382
+
383
+ # Process mempool transactions
384
+ pending_map = self._process_mempool_transactions(mempool_transactions)
385
+
386
+ # Get all addresses we're tracking
387
+ all_addresses = set(self.wallet_states.keys())
388
+
389
+ # Update each wallet
390
+ for address in all_addresses:
391
+ state = self.wallet_states[address]
392
+
393
+ # Get transactions for this wallet
394
+ confirmed_txs = confirmed_map.get(address, [])
395
+ pending_txs = pending_map.get(address, [])
396
+
397
+ # Clear old transactions (we'll repopulate from fresh data)
398
+ state.confirmed_transactions = confirmed_txs.copy()
399
+ state.pending_transactions = pending_txs.copy()
400
+
401
+ # Clear specialized lists and repopulate
402
+ state.confirmed_transfers = []
403
+ state.pending_transfers = []
404
+ state.rewards = []
405
+ state.genesis_transactions = []
406
+
407
+ # Categorize confirmed transactions
408
+ for tx in confirmed_txs:
409
+ categories = self._categorize_confirmed_transaction(tx, address)
410
+ for category in categories:
411
+ if category == 'confirmed_transfers':
412
+ state.confirmed_transfers.append(tx)
413
+ elif category == 'rewards':
414
+ state.rewards.append(tx)
415
+ elif category == 'genesis_transactions':
416
+ state.genesis_transactions.append(tx)
417
+
418
+ # Categorize pending transactions
419
+ for tx in pending_txs:
420
+ categories = self._categorize_pending_transaction(tx, address)
421
+ for category in categories:
422
+ if category == 'pending_transfers':
423
+ state.pending_transfers.append(tx)
424
+ elif category == 'rewards':
425
+ if tx not in state.rewards:
426
+ state.rewards.append(tx)
427
+ elif category == 'genesis_transactions':
428
+ if tx not in state.genesis_transactions:
429
+ state.genesis_transactions.append(tx)
430
+
431
+ # Calculate new balance
432
+ state.balance = self._calculate_balance_from_transactions(
433
+ address, confirmed_txs, pending_txs
434
+ )
435
+
436
+ # Update timestamp
437
+ state.last_updated = time.time()
438
+
439
+ sync_time = time.time() - sync_start
440
+ print(f"✅ Sync complete in {sync_time:.2f}s")
441
+
442
+ # Trigger callbacks
443
+ self._trigger_balance_updates()
444
+ self._trigger_transaction_updates()
445
+
446
+ def sync_wallets_background(self, get_blockchain_data: Callable,
447
+ get_mempool_data: Callable,
448
+ poll_interval: int = 30):
449
+ """
450
+ Start a background thread that periodically syncs wallet data.
451
+
452
+ Parameters:
453
+ get_blockchain_data: Callable that returns Dict[str, List[Dict]]
454
+ get_mempool_data: Callable that returns Dict[str, List[Dict]]
455
+ poll_interval: Seconds between syncs
456
+ """
457
+
458
+ def sync_loop():
459
+ while True:
460
+ try:
461
+ addresses = list(self.wallet_states.keys())
462
+ if addresses:
463
+ blockchain_txs = get_blockchain_data(addresses)
464
+ mempool_txs = get_mempool_data(addresses)
465
+ self.sync_wallets_from_sources(blockchain_txs, mempool_txs)
466
+
467
+ time.sleep(poll_interval)
468
+ except Exception as e:
469
+ print(f"⚠️ Background sync error: {e}")
470
+ time.sleep(poll_interval)
471
+
472
+ thread = threading.Thread(target=sync_loop, daemon=True)
473
+ thread.start()
474
+ print(f"🔄 Started background sync thread (interval: {poll_interval}s)")
475
+
476
+ # =========================================================================
477
+ # Callbacks for UI updates
478
+ # =========================================================================
479
+
480
+ def on_balance_update(self, callback: Callable):
481
+ """Register callback for balance updates"""
482
+ self.balance_callbacks.append(callback)
483
+
484
+ def on_transaction_update(self, callback: Callable):
485
+ """Register callback for transaction updates"""
486
+ self.transaction_callbacks.append(callback)
487
+
488
+ def _trigger_balance_updates(self):
489
+ """Trigger all balance update callbacks"""
490
+ with self.state_lock:
491
+ balance_data = {}
492
+ for addr, state in self.wallet_states.items():
493
+ balance_data[addr] = state.balance.to_dict()
494
+
495
+ for callback in self.balance_callbacks:
496
+ try:
497
+ callback(balance_data)
498
+ except Exception as e:
499
+ print(f"⚠️ Balance callback error: {e}")
500
+
501
+ def _trigger_transaction_updates(self):
502
+ """Trigger all transaction update callbacks"""
503
+ with self.state_lock:
504
+ transaction_data = {}
505
+ for addr, state in self.wallet_states.items():
506
+ transaction_data[addr] = {
507
+ 'confirmed': [tx.to_dict() for tx in state.confirmed_transactions],
508
+ 'pending': [tx.to_dict() for tx in state.pending_transactions],
509
+ 'transfers': {
510
+ 'confirmed': [tx.to_dict() for tx in state.confirmed_transfers],
511
+ 'pending': [tx.to_dict() for tx in state.pending_transfers],
512
+ },
513
+ 'rewards': [tx.to_dict() for tx in state.rewards],
514
+ }
515
+
516
+ for callback in self.transaction_callbacks:
517
+ try:
518
+ callback(transaction_data)
519
+ except Exception as e:
520
+ print(f"⚠️ Transaction callback error: {e}")
521
+
522
+ # =========================================================================
523
+ # Query methods for UI
524
+ # =========================================================================
525
+
526
+ def get_balance(self, address: str) -> Optional[Dict]:
527
+ """Get balance for a specific wallet"""
528
+ state = self.get_wallet_state(address)
529
+ if state:
530
+ return state.balance.to_dict()
531
+ return None
532
+
533
+ def get_all_balances(self) -> Dict[str, Dict]:
534
+ """Get balances for all wallets"""
535
+ with self.state_lock:
536
+ balances = {}
537
+ for addr, state in self.wallet_states.items():
538
+ balances[addr] = state.balance.to_dict()
539
+ return balances
540
+
541
+ def get_transactions(self, address: str, transaction_type: str = 'all') -> List[Dict]:
542
+ """
543
+ Get transactions for a wallet.
544
+
545
+ Types: 'all', 'confirmed', 'pending', 'transfers', 'rewards', 'genesis'
546
+ """
547
+ state = self.get_wallet_state(address)
548
+ if not state:
549
+ return []
550
+
551
+ result = []
552
+
553
+ if transaction_type in ['all', 'confirmed']:
554
+ result.extend([tx.to_dict() for tx in state.confirmed_transactions])
555
+
556
+ if transaction_type in ['all', 'pending']:
557
+ result.extend([tx.to_dict() for tx in state.pending_transactions])
558
+
559
+ if transaction_type in ['transfers']:
560
+ result.extend([tx.to_dict() for tx in state.confirmed_transfers])
561
+ result.extend([tx.to_dict() for tx in state.pending_transfers])
562
+
563
+ if transaction_type in ['all', 'rewards']:
564
+ result.extend([tx.to_dict() for tx in state.rewards])
565
+
566
+ if transaction_type in ['all', 'genesis']:
567
+ result.extend([tx.to_dict() for tx in state.genesis_transactions])
568
+
569
+ # Sort by timestamp (most recent first)
570
+ result.sort(key=lambda x: x['timestamp'], reverse=True)
571
+
572
+ return result
573
+
574
+ def get_wallet_summary(self, address: str) -> Optional[Dict]:
575
+ """Get complete summary for a wallet (balance + all transaction types)"""
576
+ state = self.get_wallet_state(address)
577
+ if not state:
578
+ return None
579
+
580
+ return {
581
+ 'address': address,
582
+ 'balance': state.balance.to_dict(),
583
+ 'transaction_counts': {
584
+ 'confirmed': len(state.confirmed_transactions),
585
+ 'pending': len(state.pending_transactions),
586
+ 'transfers_confirmed': len(state.confirmed_transfers),
587
+ 'transfers_pending': len(state.pending_transfers),
588
+ 'rewards': len(state.rewards),
589
+ 'genesis': len(state.genesis_transactions),
590
+ },
591
+ 'transactions': {
592
+ 'confirmed': [tx.to_dict() for tx in state.confirmed_transactions[-10:]], # Last 10
593
+ 'pending': [tx.to_dict() for tx in state.pending_transactions],
594
+ 'rewards': [tx.to_dict() for tx in state.rewards[-10:]],
595
+ },
596
+ 'last_updated': state.last_updated
597
+ }
598
+
599
+ def get_all_summaries(self) -> Dict[str, Dict]:
600
+ """Get summaries for all registered wallets"""
601
+ with self.state_lock:
602
+ summaries = {}
603
+ for address in self.wallet_states:
604
+ summary = self.get_wallet_summary(address)
605
+ if summary:
606
+ summaries[address] = summary
607
+ return summaries
608
+
609
+ # =========================================================================
610
+ # Utility methods
611
+ # =========================================================================
612
+
613
+ def clear_all_caches(self):
614
+ """Clear all transaction caches (for testing)"""
615
+ with self.state_lock:
616
+ self.processed_pending_hashes.clear()
617
+ self.processed_confirmed_hashes.clear()
618
+
619
+ def reset_wallet(self, address: str):
620
+ """Reset a wallet to empty state"""
621
+ with self.state_lock:
622
+ if address in self.wallet_states:
623
+ self.wallet_states[address] = WalletState(address=address)
624
+
625
+ def remove_wallet(self, address: str):
626
+ """Remove a wallet from tracking"""
627
+ with self.state_lock:
628
+ if address in self.wallet_states:
629
+ del self.wallet_states[address]
630
+
631
+
632
+ # Global singleton instance
633
+ _wallet_state_manager = WalletStateManager()
634
+
635
+
636
+ def get_wallet_manager() -> WalletStateManager:
637
+ """Get the global wallet state manager instance"""
638
+ return _wallet_state_manager