lunalib 1.5.1__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.
Potentially problematic release.
This version of lunalib might be problematic. Click here for more details.
- core/__init__.py +0 -0
- core/blockchain.py +172 -0
- core/crypto.py +32 -0
- core/wallet.py +408 -0
- gtx/__init__.py +0 -0
- gtx/bill_registry.py +122 -0
- gtx/digital_bill.py +273 -0
- gtx/genesis.py +338 -0
- lunalib/__init__.py +21 -0
- lunalib/cli.py +18 -0
- lunalib/core/__init__.py +0 -0
- lunalib/core/blockchain.py +803 -0
- lunalib/core/crypto.py +270 -0
- lunalib/core/mempool.py +342 -0
- lunalib/core/sm2.py +723 -0
- lunalib/core/wallet.py +1342 -0
- lunalib/core/wallet_manager.py +638 -0
- lunalib/core/wallet_sync_helper.py +163 -0
- lunalib/gtx/__init__.py +0 -0
- lunalib/gtx/bill_registry.py +122 -0
- lunalib/gtx/digital_bill.py +273 -0
- lunalib/gtx/genesis.py +349 -0
- lunalib/luna_lib.py +87 -0
- lunalib/mining/__init__.py +0 -0
- lunalib/mining/cuda_manager.py +137 -0
- lunalib/mining/difficulty.py +106 -0
- lunalib/mining/miner.py +617 -0
- lunalib/requirements.txt +44 -0
- lunalib/storage/__init__.py +0 -0
- lunalib/storage/cache.py +148 -0
- lunalib/storage/database.py +222 -0
- lunalib/storage/encryption.py +105 -0
- lunalib/transactions/__init__.py +0 -0
- lunalib/transactions/security.py +234 -0
- lunalib/transactions/transactions.py +399 -0
- lunalib/transactions/validator.py +71 -0
- lunalib-1.5.1.dist-info/METADATA +283 -0
- lunalib-1.5.1.dist-info/RECORD +53 -0
- lunalib-1.5.1.dist-info/WHEEL +5 -0
- lunalib-1.5.1.dist-info/entry_points.txt +2 -0
- lunalib-1.5.1.dist-info/top_level.txt +6 -0
- mining/__init__.py +0 -0
- mining/cuda_manager.py +137 -0
- mining/difficulty.py +106 -0
- mining/miner.py +107 -0
- storage/__init__.py +0 -0
- storage/cache.py +148 -0
- storage/database.py +222 -0
- storage/encryption.py +105 -0
- transactions/__init__.py +0 -0
- transactions/security.py +172 -0
- transactions/transactions.py +424 -0
- transactions/validator.py +71 -0
|
@@ -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
|