lunalib 1.2.3__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.
- lunalib/core/blockchain.py +315 -150
- lunalib/core/crypto.py +265 -27
- lunalib/core/mempool.py +104 -102
- lunalib/core/sm2.py +723 -0
- lunalib/core/wallet.py +1026 -319
- lunalib/core/wallet_manager.py +638 -0
- lunalib/core/wallet_sync_helper.py +163 -0
- lunalib/mining/miner.py +24 -15
- lunalib/storage/cache.py +13 -4
- lunalib/storage/database.py +14 -5
- lunalib/storage/encryption.py +11 -2
- lunalib/transactions/security.py +96 -25
- lunalib/transactions/transactions.py +256 -431
- lunalib-1.5.2.dist-info/METADATA +20 -0
- lunalib-1.5.2.dist-info/RECORD +31 -0
- lunalib-1.5.2.dist-info/top_level.txt +1 -0
- core/__init__.py +0 -0
- core/blockchain.py +0 -172
- core/crypto.py +0 -32
- core/wallet.py +0 -347
- gtx/__init__.py +0 -0
- gtx/bill_registry.py +0 -122
- gtx/digital_bill.py +0 -273
- gtx/genesis.py +0 -338
- lunalib/requirements.txt +0 -44
- lunalib-1.2.3.dist-info/METADATA +0 -283
- lunalib-1.2.3.dist-info/RECORD +0 -50
- lunalib-1.2.3.dist-info/entry_points.txt +0 -2
- lunalib-1.2.3.dist-info/top_level.txt +0 -6
- mining/__init__.py +0 -0
- mining/cuda_manager.py +0 -137
- mining/difficulty.py +0 -106
- mining/miner.py +0 -107
- storage/__init__.py +0 -0
- storage/cache.py +0 -148
- storage/database.py +0 -222
- storage/encryption.py +0 -105
- transactions/__init__.py +0 -0
- transactions/security.py +0 -172
- transactions/transactions.py +0 -379
- transactions/validator.py +0 -71
- {lunalib-1.2.3.dist-info → lunalib-1.5.2.dist-info}/WHEEL +0 -0
lunalib/core/wallet.py
CHANGED
|
@@ -1,21 +1,43 @@
|
|
|
1
|
-
# wallet.py
|
|
2
1
|
import time
|
|
3
2
|
import hashlib
|
|
4
3
|
import json
|
|
5
|
-
from cryptography.fernet import Fernet
|
|
6
|
-
import base64
|
|
7
4
|
import os
|
|
5
|
+
import secrets
|
|
6
|
+
import binascii
|
|
7
|
+
import base64
|
|
8
|
+
import threading
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
from typing import Optional, Callable, Dict, List, Tuple
|
|
11
|
+
from cryptography.fernet import Fernet
|
|
12
|
+
from ..core.crypto import KeyManager
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
class LunaWallet:
|
|
10
|
-
"""Luna wallet implementation with proper key management and balance tracking"""
|
|
11
|
-
|
|
12
16
|
def __init__(self, data_dir=None):
|
|
13
17
|
self.data_dir = data_dir
|
|
14
|
-
self.wallets = {}
|
|
15
|
-
self.current_wallet_address = None
|
|
18
|
+
self.wallets = {}
|
|
19
|
+
self.current_wallet_address = None
|
|
20
|
+
self.key_manager = KeyManager()
|
|
16
21
|
|
|
17
|
-
#
|
|
22
|
+
# Threading for asynchronous balance loading
|
|
23
|
+
self.balance_thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=3)
|
|
24
|
+
self.balance_callbacks = []
|
|
25
|
+
self.balance_loading = False
|
|
26
|
+
self.last_balance_update = 0
|
|
27
|
+
self.balance_update_interval = 30 # seconds
|
|
18
28
|
self._reset_current_wallet()
|
|
29
|
+
self._confirmed_tx_cache: Dict[str, List[Dict]] = {}
|
|
30
|
+
self._pending_tx_cache: Dict[str, List[Dict]] = {}
|
|
31
|
+
|
|
32
|
+
# ----------------------
|
|
33
|
+
# Address normalization
|
|
34
|
+
# ----------------------
|
|
35
|
+
def _normalize_address(self, addr: str) -> str:
|
|
36
|
+
if not addr:
|
|
37
|
+
return ''
|
|
38
|
+
addr_str = str(addr).strip("'\" ").lower()
|
|
39
|
+
return addr_str[4:] if addr_str.startswith('lun_') else addr_str
|
|
40
|
+
|
|
19
41
|
|
|
20
42
|
def _reset_current_wallet(self):
|
|
21
43
|
"""Reset current wallet to empty state"""
|
|
@@ -23,110 +45,452 @@ class LunaWallet:
|
|
|
23
45
|
self.balance = 0.0 # Total balance (confirmed transactions)
|
|
24
46
|
self.available_balance = 0.0 # Available balance (total - pending outgoing)
|
|
25
47
|
self.created = time.time()
|
|
26
|
-
self.private_key = None
|
|
27
|
-
self.public_key = None
|
|
48
|
+
self.private_key = None # Will store REAL SM2 private key
|
|
49
|
+
self.public_key = None # Will store REAL SM2 public key
|
|
28
50
|
self.encrypted_private_key = None
|
|
29
51
|
self.label = "New Wallet"
|
|
30
52
|
self.is_locked = True
|
|
53
|
+
|
|
54
|
+
# ============================================================================
|
|
55
|
+
# REAL CRYPTOGRAPHIC KEY GENERATION (SM2 EAST ASIAN STANDARD)
|
|
56
|
+
# ============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sign_transaction_data(self, transaction_data):
|
|
61
|
+
"""Sign transaction data with SM2 private key"""
|
|
62
|
+
if not self.private_key or self.is_locked:
|
|
63
|
+
print("DEBUG: Cannot sign - wallet locked or no private key")
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
# Convert transaction data to string for signing
|
|
67
|
+
tx_string = json.dumps(transaction_data, sort_keys=True)
|
|
68
|
+
|
|
69
|
+
# Sign with SM2
|
|
70
|
+
signature = self.sm2.sign_message(self.private_key, tx_string)
|
|
71
|
+
|
|
72
|
+
if signature:
|
|
73
|
+
print(f"DEBUG: Transaction signed successfully, signature: {signature[:16]}...")
|
|
74
|
+
else:
|
|
75
|
+
print("DEBUG: Failed to sign transaction")
|
|
76
|
+
|
|
77
|
+
return signature
|
|
78
|
+
|
|
79
|
+
def _verify_wallet_integrity(self) -> bool:
|
|
80
|
+
"""Basic cryptographic key verification"""
|
|
81
|
+
# Check if keys exist
|
|
82
|
+
if not self.private_key or not self.public_key:
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
# Validate key formats
|
|
86
|
+
if len(self.private_key) != 64: # 256-bit hex
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
if not self.public_key.startswith('04') or len(self.public_key) != 130:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
def _generate_key_pair(self):
|
|
95
|
+
"""Generate REAL SM2 key pair"""
|
|
96
|
+
return self.key_manager.generate_keypair()
|
|
31
97
|
|
|
32
|
-
def
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
98
|
+
def create_wallet(self, name, password):
|
|
99
|
+
"""Create a new wallet and set it as current"""
|
|
100
|
+
print(f"DEBUG: Creating wallet '{name}'")
|
|
101
|
+
|
|
102
|
+
# Generate REAL SM2 keys
|
|
103
|
+
private_key, public_key, address = self._generate_key_pair()
|
|
104
|
+
|
|
105
|
+
# Encrypt private key
|
|
106
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
107
|
+
fernet = Fernet(key)
|
|
108
|
+
encrypted_private_key = fernet.encrypt(private_key.encode())
|
|
109
|
+
|
|
110
|
+
# Create wallet data
|
|
111
|
+
wallet_data = {
|
|
112
|
+
'address': address,
|
|
113
|
+
'balance': 0.0,
|
|
114
|
+
'available_balance': 0.0,
|
|
115
|
+
'created': time.time(),
|
|
116
|
+
'private_key': private_key, # REAL SM2 private key
|
|
117
|
+
'public_key': public_key, # REAL SM2 public key
|
|
118
|
+
'encrypted_private_key': encrypted_private_key,
|
|
119
|
+
'label': name,
|
|
120
|
+
'is_locked': True,
|
|
121
|
+
'crypto_standard': 'SM2_GB/T_32918'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# Add to wallets collection
|
|
125
|
+
self.wallets[address] = wallet_data
|
|
126
|
+
|
|
127
|
+
# Set as current wallet
|
|
128
|
+
self._set_current_wallet(wallet_data)
|
|
129
|
+
|
|
130
|
+
print(f"DEBUG: Created and set current wallet {address}")
|
|
131
|
+
|
|
132
|
+
return wallet_data
|
|
41
133
|
|
|
134
|
+
|
|
135
|
+
def _generate_address(self, public_key_hex):
|
|
136
|
+
"""Generate address from SM2 public key"""
|
|
137
|
+
return self.key_manager.derive_address(public_key_hex)
|
|
138
|
+
def create_new_wallet(self, name, password):
|
|
139
|
+
"""
|
|
140
|
+
Create a new wallet without switching to it
|
|
141
|
+
Returns: wallet_data dict
|
|
142
|
+
"""
|
|
143
|
+
print(f"DEBUG: Creating additional wallet '{name}'")
|
|
144
|
+
|
|
145
|
+
# Generate REAL cryptographic keys
|
|
146
|
+
private_key_hex, public_key_hex, address = self._generate_key_pair()
|
|
147
|
+
|
|
148
|
+
# Encrypt private key
|
|
149
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
150
|
+
fernet = Fernet(key)
|
|
151
|
+
encrypted_private_key = fernet.encrypt(private_key_hex.encode())
|
|
152
|
+
|
|
153
|
+
# Create wallet data
|
|
154
|
+
new_wallet_data = {
|
|
155
|
+
'address': address,
|
|
156
|
+
'balance': 0.0,
|
|
157
|
+
'available_balance': 0.0,
|
|
158
|
+
'created': time.time(),
|
|
159
|
+
'private_key': private_key_hex,
|
|
160
|
+
'public_key': public_key_hex,
|
|
161
|
+
'encrypted_private_key': encrypted_private_key,
|
|
162
|
+
'label': name,
|
|
163
|
+
'is_locked': True,
|
|
164
|
+
'crypto_standard': 'SM2_GB/T_32918'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Add to wallets collection
|
|
168
|
+
self.wallets[address] = new_wallet_data
|
|
169
|
+
|
|
170
|
+
print(f"DEBUG: Created wallet {address}, total wallets: {len(self.wallets)}")
|
|
171
|
+
|
|
172
|
+
return new_wallet_data
|
|
173
|
+
|
|
174
|
+
def _set_current_wallet(self, wallet_data):
|
|
175
|
+
"""Set the current wallet from wallet data"""
|
|
176
|
+
self.current_wallet_address = wallet_data['address']
|
|
177
|
+
self.address = wallet_data['address']
|
|
178
|
+
self.balance = wallet_data['balance']
|
|
179
|
+
self.available_balance = wallet_data['available_balance']
|
|
180
|
+
self.created = wallet_data['created']
|
|
181
|
+
self.private_key = wallet_data['private_key']
|
|
182
|
+
self.public_key = wallet_data['public_key']
|
|
183
|
+
self.encrypted_private_key = wallet_data['encrypted_private_key']
|
|
184
|
+
self.label = wallet_data['label']
|
|
185
|
+
self.is_locked = wallet_data.get('is_locked', True)
|
|
186
|
+
|
|
187
|
+
print(f"DEBUG: Set current wallet to {self.address}")
|
|
188
|
+
|
|
189
|
+
def unlock_wallet(self, address, password):
|
|
190
|
+
"""Unlock wallet with password"""
|
|
191
|
+
if address not in self.wallets:
|
|
192
|
+
print(f"DEBUG: Wallet {address} not found in collection")
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
wallet_data = self.wallets[address]
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
if wallet_data.get('encrypted_private_key'):
|
|
199
|
+
# Decrypt private key
|
|
200
|
+
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
201
|
+
fernet = Fernet(key)
|
|
202
|
+
decrypted_key = fernet.decrypt(wallet_data['encrypted_private_key'])
|
|
203
|
+
|
|
204
|
+
# Update wallet data
|
|
205
|
+
wallet_data['private_key'] = decrypted_key.decode()
|
|
206
|
+
wallet_data['is_locked'] = False
|
|
207
|
+
|
|
208
|
+
# If this is the current wallet, update current state
|
|
209
|
+
if self.current_wallet_address == address:
|
|
210
|
+
self.private_key = wallet_data['private_key']
|
|
211
|
+
self.is_locked = False
|
|
212
|
+
|
|
213
|
+
print(f"DEBUG: Wallet {address} unlocked successfully")
|
|
214
|
+
print(f"DEBUG: Private key available: {bool(self.private_key)}")
|
|
215
|
+
|
|
216
|
+
# Verify cryptographic integrity after unlock
|
|
217
|
+
if self._verify_wallet_integrity():
|
|
218
|
+
print(f"DEBUG: Wallet cryptographic integrity verified")
|
|
219
|
+
else:
|
|
220
|
+
print(f"DEBUG: WARNING: Wallet unlocked but cryptographic verification failed")
|
|
221
|
+
|
|
222
|
+
return True
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"DEBUG: Unlock failed: {e}")
|
|
225
|
+
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
def switch_wallet(self, address, password=None):
|
|
229
|
+
"""Switch to a different wallet in the collection"""
|
|
230
|
+
if address in self.wallets:
|
|
231
|
+
wallet_data = self.wallets[address]
|
|
232
|
+
self._set_current_wallet(wallet_data)
|
|
233
|
+
|
|
234
|
+
# If password provided, unlock the wallet
|
|
235
|
+
if password:
|
|
236
|
+
return self.unlock_wallet(address, password)
|
|
237
|
+
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
print(f"DEBUG: Cannot switch to {address} - not in wallet collection")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
# ============================================================================
|
|
244
|
+
# BALANCE AND TRANSACTION METHODS
|
|
245
|
+
# ============================================================================
|
|
246
|
+
|
|
42
247
|
def calculate_available_balance(self) -> float:
|
|
43
248
|
"""Calculate available balance (total balance minus pending outgoing transactions)"""
|
|
44
249
|
try:
|
|
45
|
-
from lunalib.core.mempool import MempoolManager
|
|
46
|
-
from lunalib.core.blockchain import BlockchainManager
|
|
47
|
-
|
|
48
250
|
# Get total balance from blockchain
|
|
49
251
|
total_balance = self._get_total_balance_from_blockchain()
|
|
50
252
|
|
|
51
|
-
# Get pending outgoing
|
|
52
|
-
|
|
53
|
-
pending_txs = mempool.get_pending_transactions(self.address)
|
|
253
|
+
# Get pending outgoing amount
|
|
254
|
+
pending_outgoing = self._get_pending_balance()
|
|
54
255
|
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
for tx in pending_txs:
|
|
58
|
-
if tx.get('from') == self.address:
|
|
59
|
-
pending_outgoing += float(tx.get('amount', 0)) + float(tx.get('fee', 0))
|
|
256
|
+
# Also check for any incoming pending transactions
|
|
257
|
+
incoming_pending = self._get_pending_incoming_balance()
|
|
60
258
|
|
|
61
259
|
available_balance = max(0.0, total_balance - pending_outgoing)
|
|
62
260
|
|
|
63
261
|
# Update both current wallet and wallets collection
|
|
64
262
|
self.available_balance = available_balance
|
|
263
|
+
self.balance = total_balance # Also update total balance
|
|
264
|
+
|
|
65
265
|
if self.current_wallet_address in self.wallets:
|
|
66
266
|
self.wallets[self.current_wallet_address]['available_balance'] = available_balance
|
|
267
|
+
self.wallets[self.current_wallet_address]['balance'] = total_balance
|
|
268
|
+
|
|
269
|
+
print(f"DEBUG: Balance calculated - Total: {total_balance}, Pending Out: {pending_outgoing}, Available: {available_balance}")
|
|
270
|
+
|
|
271
|
+
if incoming_pending > 0:
|
|
272
|
+
print(f"DEBUG: Also {incoming_pending} LUN incoming (pending)")
|
|
67
273
|
|
|
68
|
-
print(f"DEBUG: Available balance calculated - Total: {total_balance}, Pending Out: {pending_outgoing}, Available: {available_balance}")
|
|
69
274
|
return available_balance
|
|
70
275
|
|
|
71
276
|
except Exception as e:
|
|
72
277
|
print(f"DEBUG: Error calculating available balance: {e}")
|
|
73
278
|
return self.balance # Fallback to total balance
|
|
74
279
|
|
|
75
|
-
def
|
|
76
|
-
"""
|
|
280
|
+
def _compute_confirmed_balance(self, transactions: List[Dict]) -> float:
|
|
281
|
+
"""Compute confirmed balance from a list of transactions."""
|
|
282
|
+
total_balance = 0.0
|
|
283
|
+
for tx in transactions:
|
|
284
|
+
tx_type = (tx.get('type') or '').lower()
|
|
285
|
+
direction = tx.get('direction', '')
|
|
286
|
+
amount = float(tx.get('amount', 0) or 0)
|
|
287
|
+
|
|
288
|
+
if tx_type == 'reward' or tx.get('from') == 'network':
|
|
289
|
+
total_balance += amount
|
|
290
|
+
elif direction == 'incoming':
|
|
291
|
+
total_balance += amount
|
|
292
|
+
elif direction == 'outgoing':
|
|
293
|
+
fee = float(tx.get('fee', 0) or 0)
|
|
294
|
+
total_balance -= amount
|
|
295
|
+
total_balance -= fee
|
|
296
|
+
|
|
297
|
+
return max(0.0, total_balance)
|
|
298
|
+
|
|
299
|
+
def _compute_pending_totals(self, pending_txs: List[Dict], address: str) -> Tuple[float, float]:
|
|
300
|
+
"""Return (pending_outgoing, pending_incoming) for an address."""
|
|
301
|
+
pending_out = 0.0
|
|
302
|
+
pending_in = 0.0
|
|
303
|
+
|
|
304
|
+
target_norm = self._normalize_address(address)
|
|
305
|
+
for tx in pending_txs:
|
|
306
|
+
from_norm = self._normalize_address(tx.get('from') or tx.get('sender'))
|
|
307
|
+
to_norm = self._normalize_address(tx.get('to') or tx.get('receiver'))
|
|
308
|
+
|
|
309
|
+
if from_norm == target_norm:
|
|
310
|
+
amount = float(tx.get('amount', 0) or 0)
|
|
311
|
+
fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
|
|
312
|
+
pending_out += amount + fee
|
|
313
|
+
if to_norm == target_norm:
|
|
314
|
+
amount = float(tx.get('amount', 0) or 0)
|
|
315
|
+
pending_in += amount
|
|
316
|
+
|
|
317
|
+
return pending_out, pending_in
|
|
318
|
+
|
|
319
|
+
def _recompute_balances_from_cache(self) -> Dict[str, Dict[str, float]]:
|
|
320
|
+
"""Recompute balances for all wallets from cached tx sets."""
|
|
321
|
+
updated: Dict[str, Dict[str, float]] = {}
|
|
322
|
+
|
|
323
|
+
for addr, wallet_data in self.wallets.items():
|
|
324
|
+
confirmed = self._confirmed_tx_cache.get(addr, [])
|
|
325
|
+
pending = self._pending_tx_cache.get(addr, [])
|
|
326
|
+
|
|
327
|
+
total_balance = max(0.0, self._compute_confirmed_balance(confirmed))
|
|
328
|
+
pending_out, pending_in = self._compute_pending_totals(pending, addr)
|
|
329
|
+
available_balance = max(0.0, total_balance - pending_out)
|
|
330
|
+
|
|
331
|
+
wallet_data['balance'] = total_balance
|
|
332
|
+
wallet_data['available_balance'] = available_balance
|
|
333
|
+
|
|
334
|
+
if addr == self.current_wallet_address:
|
|
335
|
+
self.balance = total_balance
|
|
336
|
+
self.available_balance = available_balance
|
|
337
|
+
|
|
338
|
+
updated[addr] = {
|
|
339
|
+
'balance': total_balance,
|
|
340
|
+
'available_balance': available_balance,
|
|
341
|
+
'pending_out': pending_out,
|
|
342
|
+
'pending_in': pending_in
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return updated
|
|
346
|
+
|
|
347
|
+
def refresh_all_wallet_balances(self) -> Dict[str, Dict[str, float]]:
|
|
348
|
+
"""Scan blockchain once for all wallets, then update balances with pending mempool data."""
|
|
349
|
+
addresses = list(self.wallets.keys())
|
|
350
|
+
if not addresses:
|
|
351
|
+
return {}
|
|
352
|
+
|
|
77
353
|
try:
|
|
78
354
|
from lunalib.core.blockchain import BlockchainManager
|
|
79
|
-
|
|
355
|
+
from lunalib.core.mempool import MempoolManager
|
|
356
|
+
|
|
80
357
|
blockchain = BlockchainManager()
|
|
81
|
-
|
|
358
|
+
mempool = MempoolManager()
|
|
359
|
+
|
|
360
|
+
confirmed_map = blockchain.scan_transactions_for_addresses(addresses)
|
|
361
|
+
pending_map = mempool.get_pending_transactions_for_addresses(addresses, fetch_remote=True)
|
|
362
|
+
|
|
363
|
+
# Reset caches with the latest snapshots
|
|
364
|
+
self._confirmed_tx_cache = confirmed_map
|
|
365
|
+
self._pending_tx_cache = pending_map
|
|
366
|
+
|
|
367
|
+
return self._recompute_balances_from_cache()
|
|
368
|
+
|
|
369
|
+
except Exception as e:
|
|
370
|
+
print(f"DEBUG: Error refreshing all wallet balances: {e}")
|
|
371
|
+
return {}
|
|
372
|
+
|
|
373
|
+
def sync_all_wallets_once(self) -> Dict[str, Dict[str, float]]:
|
|
374
|
+
"""Convenience: scan blockchain once and mempool once, update all wallet balances."""
|
|
375
|
+
return self.refresh_all_wallet_balances()
|
|
376
|
+
|
|
377
|
+
def get_all_wallets_overview(self, include_transactions: bool = True) -> Dict[str, Dict]:
|
|
378
|
+
"""Return balances and (optionally) cached transactions for all wallets."""
|
|
379
|
+
overview: Dict[str, Dict] = {}
|
|
380
|
+
for addr, wallet_data in self.wallets.items():
|
|
381
|
+
confirmed = self._confirmed_tx_cache.get(addr, [])
|
|
382
|
+
pending = self._pending_tx_cache.get(addr, [])
|
|
383
|
+
|
|
384
|
+
overview[addr] = {
|
|
385
|
+
'balance': wallet_data.get('balance', 0.0),
|
|
386
|
+
'available_balance': wallet_data.get('available_balance', 0.0),
|
|
387
|
+
'pending_out': self._compute_pending_totals(pending, addr)[0],
|
|
388
|
+
'pending_in': self._compute_pending_totals(pending, addr)[1],
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if include_transactions:
|
|
392
|
+
overview[addr]['confirmed_transactions'] = confirmed
|
|
393
|
+
overview[addr]['pending_transactions'] = pending
|
|
394
|
+
|
|
395
|
+
return overview
|
|
396
|
+
|
|
397
|
+
def _apply_transaction_updates(self, confirmed_map: Dict[str, List[Dict]], pending_map: Dict[str, List[Dict]]):
|
|
398
|
+
"""Update caches from monitor callbacks and recompute balances."""
|
|
399
|
+
for addr, txs in confirmed_map.items():
|
|
400
|
+
self._confirmed_tx_cache.setdefault(addr, []).extend(txs)
|
|
401
|
+
|
|
402
|
+
for addr, txs in pending_map.items():
|
|
403
|
+
self._pending_tx_cache[addr] = txs
|
|
404
|
+
|
|
405
|
+
return self._recompute_balances_from_cache()
|
|
406
|
+
|
|
407
|
+
def start_wallet_monitoring(self, poll_interval: int = 15):
|
|
408
|
+
"""Start monitoring blockchain and mempool for all wallets."""
|
|
409
|
+
addresses = list(self.wallets.keys())
|
|
410
|
+
if not addresses:
|
|
411
|
+
print("DEBUG: No wallets to monitor")
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
from lunalib.core.blockchain import BlockchainManager
|
|
415
|
+
blockchain = BlockchainManager()
|
|
416
|
+
|
|
417
|
+
# Run an initial refresh to seed caches and balances
|
|
418
|
+
self.refresh_all_wallet_balances()
|
|
419
|
+
|
|
420
|
+
def _on_update(payload: Dict):
|
|
421
|
+
confirmed_map = payload.get('confirmed', {}) or {}
|
|
422
|
+
pending_map = payload.get('pending', {}) or {}
|
|
423
|
+
self._apply_transaction_updates(confirmed_map, pending_map)
|
|
424
|
+
|
|
425
|
+
self._monitor_stop_event = blockchain.monitor_addresses(addresses, _on_update, poll_interval=poll_interval)
|
|
426
|
+
return self._monitor_stop_event
|
|
427
|
+
|
|
428
|
+
def stop_wallet_monitoring(self):
|
|
429
|
+
"""Stop the background monitor if running."""
|
|
430
|
+
stop_event = getattr(self, '_monitor_stop_event', None)
|
|
431
|
+
if stop_event:
|
|
432
|
+
stop_event.set()
|
|
433
|
+
|
|
434
|
+
def start_sync_and_monitor(self, poll_interval: int = 15):
|
|
435
|
+
"""Run a one-time sync (blockchain + mempool) then start live monitoring."""
|
|
436
|
+
self.sync_all_wallets_once()
|
|
437
|
+
return self.start_wallet_monitoring(poll_interval=poll_interval)
|
|
438
|
+
|
|
439
|
+
def get_all_balances_after_sync(self, include_transactions: bool = True) -> Dict[str, Dict]:
|
|
440
|
+
"""One-shot sync then return per-wallet balances (and transactions if requested)."""
|
|
441
|
+
self.sync_all_wallets_once()
|
|
442
|
+
return self.get_all_wallets_overview(include_transactions=include_transactions)
|
|
443
|
+
def _get_pending_balance(self) -> float:
|
|
444
|
+
"""Get total pending balance from mempool (outgoing pending transactions)"""
|
|
445
|
+
try:
|
|
446
|
+
from lunalib.core.mempool import MempoolManager
|
|
82
447
|
|
|
83
|
-
|
|
84
|
-
for tx in transactions:
|
|
85
|
-
tx_type = tx.get('type', '')
|
|
86
|
-
|
|
87
|
-
# Handle incoming transactions
|
|
88
|
-
if tx.get('to') == self.address:
|
|
89
|
-
if tx_type in ['transfer', 'reward', 'fee_distribution', 'gtx_genesis']:
|
|
90
|
-
total_balance += float(tx.get('amount', 0))
|
|
91
|
-
|
|
92
|
-
# Handle outgoing transactions
|
|
93
|
-
elif tx.get('from') == self.address:
|
|
94
|
-
if tx_type in ['transfer', 'stake', 'delegate']:
|
|
95
|
-
total_balance -= float(tx.get('amount', 0))
|
|
96
|
-
total_balance -= float(tx.get('fee', 0))
|
|
448
|
+
mempool = MempoolManager()
|
|
97
449
|
|
|
98
|
-
|
|
450
|
+
# Get pending transactions for this address
|
|
451
|
+
pending_txs = mempool.get_pending_transactions(self.address, fetch_remote=True)
|
|
452
|
+
|
|
453
|
+
total_pending_outgoing = 0.0
|
|
454
|
+
|
|
455
|
+
for tx in pending_txs:
|
|
456
|
+
# Only count outgoing transactions (where we're the sender)
|
|
457
|
+
if tx.get('from') == self.address:
|
|
458
|
+
amount = float(tx.get('amount', 0))
|
|
459
|
+
fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
|
|
460
|
+
total_pending_outgoing += amount + fee
|
|
461
|
+
print(f"🔍 Found pending outgoing: {amount} + {fee} fee = {amount + fee}")
|
|
462
|
+
|
|
463
|
+
return total_pending_outgoing
|
|
99
464
|
|
|
100
465
|
except Exception as e:
|
|
101
|
-
print(f"DEBUG: Error
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
def
|
|
105
|
-
"""
|
|
466
|
+
print(f"DEBUG: Error calculating pending balance: {e}")
|
|
467
|
+
return 0.0
|
|
468
|
+
|
|
469
|
+
def _get_pending_incoming_balance(self) -> float:
|
|
470
|
+
"""Get total pending incoming balance from mempool"""
|
|
106
471
|
try:
|
|
107
|
-
|
|
108
|
-
available_balance = self.calculate_available_balance()
|
|
472
|
+
from lunalib.core.mempool import MempoolManager
|
|
109
473
|
|
|
110
|
-
|
|
111
|
-
self.balance = total_balance
|
|
112
|
-
self.available_balance = available_balance
|
|
474
|
+
mempool = MempoolManager()
|
|
113
475
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
self.wallets[self.current_wallet_address]['balance'] = total_balance
|
|
117
|
-
self.wallets[self.current_wallet_address]['available_balance'] = available_balance
|
|
476
|
+
# Get pending transactions for this address
|
|
477
|
+
pending_txs = mempool.get_pending_transactions(self.address, fetch_remote=True)
|
|
118
478
|
|
|
119
|
-
|
|
120
|
-
|
|
479
|
+
total_pending_incoming = 0.0
|
|
480
|
+
|
|
481
|
+
for tx in pending_txs:
|
|
482
|
+
# Only count incoming transactions (where we're the receiver)
|
|
483
|
+
if tx.get('to') == self.address:
|
|
484
|
+
amount = float(tx.get('amount', 0))
|
|
485
|
+
total_pending_incoming += amount
|
|
486
|
+
print(f"🔍 Found pending incoming: +{amount}")
|
|
487
|
+
|
|
488
|
+
return total_pending_incoming
|
|
121
489
|
|
|
122
490
|
except Exception as e:
|
|
123
|
-
print(f"DEBUG: Error
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
def get_available_balance(self) -> float:
|
|
127
|
-
"""Get current wallet available balance"""
|
|
128
|
-
return self.available_balance
|
|
129
|
-
|
|
491
|
+
print(f"DEBUG: Error calculating pending incoming balance: {e}")
|
|
492
|
+
return 0.0
|
|
493
|
+
|
|
130
494
|
def _get_total_balance_from_blockchain(self) -> float:
|
|
131
495
|
"""Get total balance by scanning blockchain for confirmed transactions"""
|
|
132
496
|
try:
|
|
@@ -134,28 +498,60 @@ class LunaWallet:
|
|
|
134
498
|
|
|
135
499
|
blockchain = BlockchainManager()
|
|
136
500
|
transactions = blockchain.scan_transactions_for_address(self.address)
|
|
137
|
-
|
|
138
|
-
total_balance
|
|
139
|
-
for tx in transactions:
|
|
140
|
-
tx_type = tx.get('type', '')
|
|
141
|
-
|
|
142
|
-
# Handle incoming transactions
|
|
143
|
-
if tx.get('to') == self.address:
|
|
144
|
-
if tx_type in ['transfer', 'reward', 'fee_distribution', 'gtx_genesis']:
|
|
145
|
-
total_balance += float(tx.get('amount', 0))
|
|
146
|
-
|
|
147
|
-
# Handle outgoing transactions
|
|
148
|
-
elif tx.get('from') == self.address:
|
|
149
|
-
if tx_type in ['transfer', 'stake', 'delegate']:
|
|
150
|
-
total_balance -= float(tx.get('amount', 0))
|
|
151
|
-
total_balance -= float(tx.get('fee', 0))
|
|
152
|
-
|
|
153
|
-
return max(0.0, total_balance)
|
|
501
|
+
total_balance = self._compute_confirmed_balance(transactions)
|
|
502
|
+
return total_balance
|
|
154
503
|
|
|
155
504
|
except Exception as e:
|
|
156
505
|
print(f"DEBUG: Error getting blockchain balance: {e}")
|
|
157
506
|
return self.balance
|
|
158
|
-
|
|
507
|
+
|
|
508
|
+
def register_balance_callback(self, callback: Callable[[float, float], None]):
|
|
509
|
+
"""Register a callback to be called when balance is updated asynchronously"""
|
|
510
|
+
self.balance_callbacks.append(callback)
|
|
511
|
+
|
|
512
|
+
def start_async_balance_loading(self):
|
|
513
|
+
"""Start asynchronous balance loading in background thread"""
|
|
514
|
+
if self.balance_loading:
|
|
515
|
+
return # Already loading
|
|
516
|
+
|
|
517
|
+
if time.time() - self.last_balance_update < self.balance_update_interval:
|
|
518
|
+
return # Too soon since last update
|
|
519
|
+
|
|
520
|
+
self.balance_loading = True
|
|
521
|
+
|
|
522
|
+
def _async_balance_update():
|
|
523
|
+
try:
|
|
524
|
+
# Load balances asynchronously
|
|
525
|
+
total_balance = self._get_total_balance_from_blockchain()
|
|
526
|
+
available_balance = self.calculate_available_balance()
|
|
527
|
+
|
|
528
|
+
# Update wallet state
|
|
529
|
+
self.balance = total_balance
|
|
530
|
+
self.available_balance = available_balance
|
|
531
|
+
self.last_balance_update = time.time()
|
|
532
|
+
|
|
533
|
+
# Update in wallets collection
|
|
534
|
+
if self.current_wallet_address in self.wallets:
|
|
535
|
+
self.wallets[self.current_wallet_address]['balance'] = total_balance
|
|
536
|
+
self.wallets[self.current_wallet_address]['available_balance'] = available_balance
|
|
537
|
+
|
|
538
|
+
# Notify callbacks
|
|
539
|
+
for callback in self.balance_callbacks:
|
|
540
|
+
try:
|
|
541
|
+
callback(total_balance, available_balance)
|
|
542
|
+
except Exception as e:
|
|
543
|
+
print(f"Error in balance callback: {e}")
|
|
544
|
+
|
|
545
|
+
print(f"DEBUG: Async balance updated - Total: {total_balance}, Available: {available_balance}")
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
print(f"DEBUG: Error in async balance loading: {e}")
|
|
549
|
+
finally:
|
|
550
|
+
self.balance_loading = False
|
|
551
|
+
|
|
552
|
+
# Start in background thread
|
|
553
|
+
self.balance_thread_pool.submit(_async_balance_update)
|
|
554
|
+
|
|
159
555
|
def refresh_balance(self) -> bool:
|
|
160
556
|
"""Refresh both total and available balance from blockchain and mempool"""
|
|
161
557
|
try:
|
|
@@ -177,32 +573,32 @@ class LunaWallet:
|
|
|
177
573
|
except Exception as e:
|
|
178
574
|
print(f"DEBUG: Error refreshing balance: {e}")
|
|
179
575
|
return False
|
|
180
|
-
|
|
576
|
+
|
|
181
577
|
def send_transaction(self, to_address: str, amount: float, memo: str = "", password: str = None) -> bool:
|
|
182
|
-
"""Send transaction using
|
|
578
|
+
"""Send transaction using REAL SM2 signatures"""
|
|
183
579
|
try:
|
|
184
|
-
print(f"
|
|
580
|
+
print(f"[SM2] Sending {amount} to {to_address}")
|
|
185
581
|
|
|
186
|
-
#
|
|
187
|
-
self.
|
|
582
|
+
# 1. Basic validation
|
|
583
|
+
if self.is_locked or not self.private_key:
|
|
584
|
+
print("[SM2] Wallet locked or no private key")
|
|
585
|
+
return False
|
|
188
586
|
|
|
189
|
-
# Check
|
|
190
|
-
if
|
|
191
|
-
print(
|
|
587
|
+
# 2. Check key integrity
|
|
588
|
+
if not self._verify_wallet_integrity():
|
|
589
|
+
print("[SM2] Invalid cryptographic keys")
|
|
192
590
|
return False
|
|
193
591
|
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
592
|
+
# 3. Balance check
|
|
593
|
+
self.refresh_balance()
|
|
594
|
+
if amount > self.get_available_balance():
|
|
595
|
+
print(f"[SM2] Insufficient balance: {self.get_available_balance()} < {amount}")
|
|
197
596
|
return False
|
|
198
597
|
|
|
199
|
-
#
|
|
598
|
+
# 4. Create and sign transaction
|
|
200
599
|
from lunalib.transactions.transactions import TransactionManager
|
|
201
|
-
|
|
202
|
-
# Create transaction manager
|
|
203
600
|
tx_manager = TransactionManager()
|
|
204
601
|
|
|
205
|
-
# Create and sign transaction
|
|
206
602
|
transaction = tx_manager.create_transaction(
|
|
207
603
|
from_address=self.address,
|
|
208
604
|
to_address=to_address,
|
|
@@ -212,244 +608,305 @@ class LunaWallet:
|
|
|
212
608
|
transaction_type="transfer"
|
|
213
609
|
)
|
|
214
610
|
|
|
215
|
-
print(f"
|
|
611
|
+
print(f"[SM2] Transaction created: {transaction.get('hash', 'no_hash')[:16]}")
|
|
216
612
|
|
|
217
|
-
# Validate
|
|
613
|
+
# 5. Validate
|
|
218
614
|
is_valid, message = tx_manager.validate_transaction(transaction)
|
|
219
615
|
if not is_valid:
|
|
220
|
-
print(f"
|
|
616
|
+
print(f"[SM2] Validation failed: {message}")
|
|
221
617
|
return False
|
|
222
618
|
|
|
223
|
-
#
|
|
619
|
+
# 6. Broadcast
|
|
224
620
|
success, message = tx_manager.send_transaction(transaction)
|
|
225
621
|
if success:
|
|
226
|
-
print(f"
|
|
622
|
+
print(f"[SM2] Transaction broadcast: {message}")
|
|
227
623
|
|
|
228
|
-
# Update
|
|
229
|
-
|
|
230
|
-
self.available_balance -=
|
|
624
|
+
# Update balance immediately
|
|
625
|
+
total_cost = amount + transaction.get('fee', 0)
|
|
626
|
+
self.available_balance -= total_cost
|
|
231
627
|
if self.current_wallet_address in self.wallets:
|
|
232
628
|
self.wallets[self.current_wallet_address]['available_balance'] = self.available_balance
|
|
233
629
|
|
|
234
|
-
|
|
630
|
+
# Save state
|
|
631
|
+
self.save_wallet_data()
|
|
632
|
+
|
|
633
|
+
# Trigger async balance refresh to get accurate data
|
|
634
|
+
self.start_async_balance_loading()
|
|
635
|
+
|
|
235
636
|
return True
|
|
236
637
|
else:
|
|
237
|
-
print(f"
|
|
638
|
+
print(f"[SM2] Broadcast failed: {message}")
|
|
238
639
|
return False
|
|
239
640
|
|
|
240
641
|
except Exception as e:
|
|
241
|
-
print(f"
|
|
642
|
+
print(f"[SM2] Error: {e}")
|
|
643
|
+
return False
|
|
644
|
+
|
|
645
|
+
def save_wallet_data(self):
|
|
646
|
+
"""Save wallet data to file"""
|
|
647
|
+
try:
|
|
648
|
+
print(f"[WALLET] Saving wallet data...")
|
|
649
|
+
|
|
650
|
+
# Prepare wallet data
|
|
651
|
+
wallet_data = {
|
|
652
|
+
'version': '2.0',
|
|
653
|
+
'address': self.address,
|
|
654
|
+
'public_key': self.public_key,
|
|
655
|
+
'private_key': self.private_key, # WARNING: In production, encrypt this!
|
|
656
|
+
'balance': self.balance,
|
|
657
|
+
'available_balance': self.available_balance,
|
|
658
|
+
'transactions': self.transactions,
|
|
659
|
+
'created_at': self.created_at,
|
|
660
|
+
'last_sync': time.time(),
|
|
661
|
+
'network': self.network,
|
|
662
|
+
'key_type': 'SM2' # Indicate SM2 key type
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
# Determine save path
|
|
666
|
+
if hasattr(self, 'wallet_file'):
|
|
667
|
+
save_path = self.wallet_file
|
|
668
|
+
else:
|
|
669
|
+
# Default save location
|
|
670
|
+
import os
|
|
671
|
+
wallet_dir = os.path.expanduser('~/.lunawallet')
|
|
672
|
+
os.makedirs(wallet_dir, exist_ok=True)
|
|
673
|
+
save_path = os.path.join(wallet_dir, f'{self.address}.json')
|
|
674
|
+
|
|
675
|
+
# Save to file
|
|
676
|
+
import json
|
|
677
|
+
with open(save_path, 'w') as f:
|
|
678
|
+
json.dump(wallet_data, f, indent=2)
|
|
679
|
+
|
|
680
|
+
print(f"[WALLET] Wallet data saved to {save_path}")
|
|
681
|
+
return True
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
print(f"[WALLET ERROR] Failed to save wallet data: {e}")
|
|
242
685
|
import traceback
|
|
243
686
|
traceback.print_exc()
|
|
244
687
|
return False
|
|
245
|
-
|
|
246
|
-
def
|
|
247
|
-
"""
|
|
688
|
+
|
|
689
|
+
def load_wallet_data(self, wallet_file=None):
|
|
690
|
+
"""Load wallet data from file"""
|
|
248
691
|
try:
|
|
249
|
-
print(f"
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
else:
|
|
265
|
-
print(f"DEBUG: Wallet not found: {from_address}")
|
|
692
|
+
print(f"[WALLET] Loading wallet data...")
|
|
693
|
+
|
|
694
|
+
if wallet_file:
|
|
695
|
+
self.wallet_file = wallet_file
|
|
696
|
+
load_path = wallet_file
|
|
697
|
+
elif hasattr(self, 'wallet_file'):
|
|
698
|
+
load_path = self.wallet_file
|
|
699
|
+
else:
|
|
700
|
+
# Try to find wallet file
|
|
701
|
+
import os
|
|
702
|
+
wallet_dir = os.path.expanduser('~/.lunawallet')
|
|
703
|
+
# Look for any .json file
|
|
704
|
+
wallet_files = [f for f in os.listdir(wallet_dir) if f.endswith('.json')]
|
|
705
|
+
if not wallet_files:
|
|
706
|
+
print("[WALLET] No wallet file found")
|
|
266
707
|
return False
|
|
708
|
+
load_path = os.path.join(wallet_dir, wallet_files[0])
|
|
709
|
+
self.wallet_file = load_path
|
|
710
|
+
|
|
711
|
+
# Load from file
|
|
712
|
+
import json
|
|
713
|
+
with open(load_path, 'r') as f:
|
|
714
|
+
wallet_data = json.load(f)
|
|
715
|
+
|
|
716
|
+
# Restore wallet data
|
|
717
|
+
self.address = wallet_data.get('address', '')
|
|
718
|
+
self.public_key = wallet_data.get('public_key', '')
|
|
719
|
+
self.private_key = wallet_data.get('private_key', '')
|
|
720
|
+
self.balance = wallet_data.get('balance', 0.0)
|
|
721
|
+
self.available_balance = wallet_data.get('available_balance', 0.0)
|
|
722
|
+
self.transactions = wallet_data.get('transactions', [])
|
|
723
|
+
self.created_at = wallet_data.get('created_at', time.time())
|
|
724
|
+
self.network = wallet_data.get('network', 'mainnet')
|
|
267
725
|
|
|
268
|
-
|
|
269
|
-
|
|
726
|
+
print(f"[WALLET] Wallet loaded: {self.address}")
|
|
727
|
+
print(f"[WALLET] Balance: {self.balance}")
|
|
728
|
+
|
|
729
|
+
# Initialize SM2 if we have keys
|
|
730
|
+
if self.public_key and self.private_key:
|
|
731
|
+
self._initialize_sm2()
|
|
732
|
+
|
|
733
|
+
# Start async balance loading
|
|
734
|
+
self.start_async_balance_loading()
|
|
735
|
+
|
|
736
|
+
return True
|
|
270
737
|
|
|
271
738
|
except Exception as e:
|
|
272
|
-
print(f"
|
|
273
|
-
import traceback
|
|
274
|
-
traceback.print_exc()
|
|
739
|
+
print(f"[WALLET ERROR] Failed to load wallet data: {e}")
|
|
275
740
|
return False
|
|
276
|
-
|
|
741
|
+
|
|
742
|
+
def _initialize_sm2(self):
|
|
743
|
+
"""Initialize SM2 crypto if keys exist"""
|
|
744
|
+
try:
|
|
745
|
+
if self.public_key and self.private_key:
|
|
746
|
+
from ..core.crypto import KeyManager
|
|
747
|
+
self.key_manager = KeyManager()
|
|
748
|
+
print(f"[WALLET] SM2 initialized with existing keys")
|
|
749
|
+
return True
|
|
750
|
+
except Exception as e:
|
|
751
|
+
print(f"[WALLET ERROR] Failed to initialize SM2: {e}")
|
|
752
|
+
return False
|
|
277
753
|
def get_transaction_history(self) -> dict:
|
|
278
|
-
"""Get complete transaction history (both pending and confirmed)
|
|
754
|
+
"""Get complete transaction history (both pending and confirmed).
|
|
755
|
+
Uses cached transactions if available, otherwise scans blockchain/mempool.
|
|
756
|
+
Includes ALL reward transactions (mining rewards and explicit reward txs).
|
|
757
|
+
"""
|
|
279
758
|
try:
|
|
280
|
-
from
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
blockchain = BlockchainManager()
|
|
284
|
-
mempool = MempoolManager()
|
|
759
|
+
# Try to use cache first (from monitoring or sync)
|
|
760
|
+
confirmed_txs = self._confirmed_tx_cache.get(self.address, [])
|
|
761
|
+
pending_txs = self._pending_tx_cache.get(self.address, [])
|
|
285
762
|
|
|
286
|
-
#
|
|
287
|
-
|
|
763
|
+
# If cache is empty, perform fresh scan
|
|
764
|
+
if not confirmed_txs:
|
|
765
|
+
from lunalib.core.blockchain import BlockchainManager
|
|
766
|
+
from lunalib.core.mempool import MempoolManager
|
|
767
|
+
|
|
768
|
+
blockchain = BlockchainManager()
|
|
769
|
+
mempool = MempoolManager()
|
|
770
|
+
|
|
771
|
+
# Get confirmed transactions from blockchain (includes mining rewards)
|
|
772
|
+
confirmed_txs = blockchain.scan_transactions_for_address(self.address)
|
|
773
|
+
self._confirmed_tx_cache[self.address] = confirmed_txs
|
|
774
|
+
|
|
775
|
+
# Get pending transactions from mempool
|
|
776
|
+
pending_txs = mempool.get_pending_transactions(self.address, fetch_remote=True)
|
|
777
|
+
self._pending_tx_cache[self.address] = pending_txs
|
|
288
778
|
|
|
289
|
-
#
|
|
290
|
-
|
|
779
|
+
# Count by type for debugging
|
|
780
|
+
reward_count = sum(1 for tx in confirmed_txs
|
|
781
|
+
if tx.get('type', '').lower() in ['reward', 'mining']
|
|
782
|
+
or tx.get('from') == 'network')
|
|
291
783
|
|
|
292
784
|
return {
|
|
293
785
|
'confirmed': confirmed_txs,
|
|
294
786
|
'pending': pending_txs,
|
|
295
787
|
'total_confirmed': len(confirmed_txs),
|
|
296
|
-
'total_pending': len(pending_txs)
|
|
788
|
+
'total_pending': len(pending_txs),
|
|
789
|
+
'reward_count': reward_count
|
|
297
790
|
}
|
|
298
791
|
except Exception as e:
|
|
299
792
|
print(f"DEBUG: Error getting transaction history: {e}")
|
|
300
|
-
return {'confirmed': [], 'pending': [], 'total_confirmed': 0, 'total_pending': 0}
|
|
301
|
-
|
|
302
|
-
def _generate_private_key(self):
|
|
303
|
-
"""Generate private key"""
|
|
304
|
-
return f"priv_{hashlib.sha256(str(time.time()).encode()).hexdigest()}"
|
|
305
|
-
|
|
306
|
-
def _derive_public_key(self, private_key=None):
|
|
307
|
-
"""Derive public key from private key"""
|
|
308
|
-
priv_key = private_key or self.private_key
|
|
309
|
-
if not priv_key:
|
|
310
|
-
return None
|
|
311
|
-
return f"pub_{priv_key[-16:]}"
|
|
793
|
+
return {'confirmed': [], 'pending': [], 'total_confirmed': 0, 'total_pending': 0, 'reward_count': 0}
|
|
312
794
|
|
|
313
|
-
def
|
|
314
|
-
"""Get
|
|
315
|
-
if not self.address:
|
|
316
|
-
return None
|
|
317
|
-
|
|
318
|
-
# Refresh balances to ensure they're current
|
|
319
|
-
self.refresh_balance()
|
|
320
|
-
|
|
321
|
-
return {
|
|
322
|
-
'address': self.address,
|
|
323
|
-
'balance': self.balance,
|
|
324
|
-
'available_balance': self.available_balance,
|
|
325
|
-
'created': self.created,
|
|
326
|
-
'private_key': self.private_key,
|
|
327
|
-
'public_key': self.public_key,
|
|
328
|
-
'encrypted_private_key': self.encrypted_private_key,
|
|
329
|
-
'label': self.label,
|
|
330
|
-
'is_locked': self.is_locked
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
def create_new_wallet(self, name, password):
|
|
334
|
-
"""Create a new wallet and add to collection without switching"""
|
|
335
|
-
# Generate new wallet data
|
|
336
|
-
address = self._generate_address()
|
|
337
|
-
private_key = self._generate_private_key()
|
|
338
|
-
public_key = f"pub_{private_key[-16:]}"
|
|
339
|
-
|
|
340
|
-
# Encrypt private key
|
|
341
|
-
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
342
|
-
fernet = Fernet(key)
|
|
343
|
-
encrypted_private_key = fernet.encrypt(private_key.encode())
|
|
344
|
-
|
|
345
|
-
# Create new wallet data
|
|
346
|
-
new_wallet_data = {
|
|
347
|
-
'address': address,
|
|
348
|
-
'balance': 0.0,
|
|
349
|
-
'available_balance': 0.0,
|
|
350
|
-
'created': time.time(),
|
|
351
|
-
'private_key': private_key,
|
|
352
|
-
'public_key': public_key,
|
|
353
|
-
'encrypted_private_key': encrypted_private_key,
|
|
354
|
-
'label': name,
|
|
355
|
-
'is_locked': True
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
# CRITICAL: Add to wallets collection
|
|
359
|
-
self.wallets[address] = new_wallet_data
|
|
360
|
-
|
|
361
|
-
print(f"DEBUG: Created new wallet {address}, total wallets: {len(self.wallets)}")
|
|
362
|
-
|
|
363
|
-
return new_wallet_data
|
|
364
|
-
|
|
365
|
-
def create_wallet(self, name, password):
|
|
366
|
-
"""Create a new wallet and set it as current"""
|
|
367
|
-
# Generate new wallet data
|
|
368
|
-
address = self._generate_address()
|
|
369
|
-
private_key = self._generate_private_key()
|
|
370
|
-
public_key = f"pub_{private_key[-16:]}"
|
|
371
|
-
|
|
372
|
-
# Encrypt private key
|
|
373
|
-
key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
|
|
374
|
-
fernet = Fernet(key)
|
|
375
|
-
encrypted_private_key = fernet.encrypt(private_key.encode())
|
|
376
|
-
|
|
377
|
-
# Create wallet data
|
|
378
|
-
wallet_data = {
|
|
379
|
-
'address': address,
|
|
380
|
-
'balance': 0.0,
|
|
381
|
-
'available_balance': 0.0,
|
|
382
|
-
'created': time.time(),
|
|
383
|
-
'private_key': private_key,
|
|
384
|
-
'public_key': public_key,
|
|
385
|
-
'encrypted_private_key': encrypted_private_key,
|
|
386
|
-
'label': name,
|
|
387
|
-
'is_locked': True
|
|
388
|
-
}
|
|
795
|
+
def get_wallet_transactions(self, address: str = None, include_pending: bool = True) -> Dict[str, List[Dict]]:
|
|
796
|
+
"""Get ALL transactions for a wallet including mining rewards, transfers, and pending.
|
|
389
797
|
|
|
390
|
-
|
|
391
|
-
|
|
798
|
+
This is the comprehensive transaction getter that ensures ALL reward transactions
|
|
799
|
+
(mining rewards, explicit reward transactions) are included.
|
|
392
800
|
|
|
393
|
-
|
|
394
|
-
|
|
801
|
+
Args:
|
|
802
|
+
address: wallet address to query (defaults to current wallet address)
|
|
803
|
+
include_pending: whether to include pending mempool transactions (default: True)
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
Dict with:
|
|
807
|
+
- 'confirmed': list of all confirmed transactions (transfers + mining rewards)
|
|
808
|
+
- 'pending': list of pending mempool transactions
|
|
809
|
+
- 'reward_transactions': list of only reward/mining transactions
|
|
810
|
+
- 'transfer_transactions': list of only transfer transactions
|
|
811
|
+
- 'total_rewards': count of reward transactions
|
|
812
|
+
- 'total_transfers': count of transfer transactions
|
|
813
|
+
"""
|
|
814
|
+
if address is None:
|
|
815
|
+
address = self.address
|
|
395
816
|
|
|
396
|
-
|
|
817
|
+
if not address:
|
|
818
|
+
return {
|
|
819
|
+
'confirmed': [],
|
|
820
|
+
'pending': [],
|
|
821
|
+
'reward_transactions': [],
|
|
822
|
+
'transfer_transactions': [],
|
|
823
|
+
'incoming_transfers': [],
|
|
824
|
+
'outgoing_transfers': [],
|
|
825
|
+
'total_rewards': 0,
|
|
826
|
+
'total_transfers': 0,
|
|
827
|
+
'total_incoming': 0,
|
|
828
|
+
'total_outgoing': 0
|
|
829
|
+
}
|
|
397
830
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
"""Set the current wallet from wallet data"""
|
|
402
|
-
self.current_wallet_address = wallet_data['address']
|
|
403
|
-
self.address = wallet_data['address']
|
|
404
|
-
self.balance = wallet_data['balance']
|
|
405
|
-
self.available_balance = wallet_data['available_balance']
|
|
406
|
-
self.created = wallet_data['created']
|
|
407
|
-
self.private_key = wallet_data['private_key']
|
|
408
|
-
self.public_key = wallet_data['public_key']
|
|
409
|
-
self.encrypted_private_key = wallet_data['encrypted_private_key']
|
|
410
|
-
self.label = wallet_data['label']
|
|
411
|
-
self.is_locked = wallet_data.get('is_locked', True)
|
|
412
|
-
|
|
413
|
-
def switch_wallet(self, address, password=None):
|
|
414
|
-
"""Switch to a different wallet in the collection"""
|
|
415
|
-
if address in self.wallets:
|
|
416
|
-
wallet_data = self.wallets[address]
|
|
417
|
-
self._set_current_wallet(wallet_data)
|
|
831
|
+
try:
|
|
832
|
+
# Normalize address for comparison
|
|
833
|
+
norm_addr = self._normalize_address(address)
|
|
418
834
|
|
|
419
|
-
#
|
|
420
|
-
self.
|
|
835
|
+
# Use cache if available
|
|
836
|
+
confirmed_txs = self._confirmed_tx_cache.get(norm_addr, [])
|
|
837
|
+
pending_txs = self._pending_tx_cache.get(norm_addr, []) if include_pending else []
|
|
421
838
|
|
|
422
|
-
# If
|
|
423
|
-
if
|
|
424
|
-
|
|
839
|
+
# If cache is empty, perform fresh scan
|
|
840
|
+
if not confirmed_txs:
|
|
841
|
+
from lunalib.core.blockchain import BlockchainManager
|
|
842
|
+
blockchain = BlockchainManager()
|
|
843
|
+
confirmed_txs = blockchain.scan_transactions_for_address(address)
|
|
844
|
+
self._confirmed_tx_cache[norm_addr] = confirmed_txs
|
|
425
845
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if address not in self.wallets:
|
|
432
|
-
return False
|
|
846
|
+
if include_pending and not pending_txs:
|
|
847
|
+
from lunalib.core.mempool import MempoolManager
|
|
848
|
+
mempool = MempoolManager()
|
|
849
|
+
pending_txs = mempool.get_pending_transactions(address, fetch_remote=True)
|
|
850
|
+
self._pending_tx_cache[norm_addr] = pending_txs
|
|
433
851
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
852
|
+
# Separate rewards and transfers based on type and source
|
|
853
|
+
# Rewards: explicitly marked as reward/mining type, or from network
|
|
854
|
+
reward_txs = [tx for tx in confirmed_txs
|
|
855
|
+
if tx.get('type', '').lower() in ['reward', 'mining', 'gtx_genesis']
|
|
856
|
+
or tx.get('from') == 'network']
|
|
857
|
+
|
|
858
|
+
# Transfers: anything that's NOT a reward (includes both incoming and outgoing)
|
|
859
|
+
transfer_txs = [tx for tx in confirmed_txs
|
|
860
|
+
if tx.get('type', '').lower() not in ['reward', 'mining', 'gtx_genesis']
|
|
861
|
+
and tx.get('from') != 'network']
|
|
862
|
+
|
|
863
|
+
# Separate incoming vs outgoing transfers
|
|
864
|
+
incoming_transfers = [tx for tx in transfer_txs if tx.get('direction') == 'incoming']
|
|
865
|
+
outgoing_transfers = [tx for tx in transfer_txs if tx.get('direction') == 'outgoing']
|
|
866
|
+
|
|
867
|
+
result = {
|
|
868
|
+
'confirmed': confirmed_txs,
|
|
869
|
+
'pending': pending_txs,
|
|
870
|
+
'reward_transactions': reward_txs,
|
|
871
|
+
'transfer_transactions': transfer_txs,
|
|
872
|
+
'incoming_transfers': incoming_transfers,
|
|
873
|
+
'outgoing_transfers': outgoing_transfers,
|
|
874
|
+
'total_rewards': len(reward_txs),
|
|
875
|
+
'total_transfers': len(transfer_txs),
|
|
876
|
+
'total_incoming': len(incoming_transfers),
|
|
877
|
+
'total_outgoing': len(outgoing_transfers)
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
print(f"DEBUG: get_wallet_transactions({address}):")
|
|
881
|
+
print(f" - Mining Rewards: {len(reward_txs)}")
|
|
882
|
+
print(f" - Incoming Transfers: {len(incoming_transfers)}")
|
|
883
|
+
print(f" - Outgoing Transfers: {len(outgoing_transfers)}")
|
|
884
|
+
print(f" - Total Transfers: {len(transfer_txs)}")
|
|
885
|
+
print(f" - Pending: {len(pending_txs)}")
|
|
886
|
+
print(f" - Total Confirmed: {len(confirmed_txs)}")
|
|
887
|
+
|
|
888
|
+
return result
|
|
889
|
+
|
|
890
|
+
except Exception as e:
|
|
891
|
+
print(f"DEBUG: Error getting wallet transactions: {e}")
|
|
892
|
+
import traceback
|
|
893
|
+
traceback.print_exc()
|
|
894
|
+
return {
|
|
895
|
+
'confirmed': [],
|
|
896
|
+
'pending': [],
|
|
897
|
+
'reward_transactions': [],
|
|
898
|
+
'transfer_transactions': [],
|
|
899
|
+
'incoming_transfers': [],
|
|
900
|
+
'outgoing_transfers': [],
|
|
901
|
+
'total_rewards': 0,
|
|
902
|
+
'total_transfers': 0,
|
|
903
|
+
'total_incoming': 0,
|
|
904
|
+
'total_outgoing': 0
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
# ============================================================================
|
|
908
|
+
# WALLET INFO AND UTILITIES
|
|
909
|
+
# ============================================================================
|
|
453
910
|
|
|
454
911
|
@property
|
|
455
912
|
def is_unlocked(self):
|
|
@@ -463,7 +920,7 @@ class LunaWallet:
|
|
|
463
920
|
"""Export private key with password decryption"""
|
|
464
921
|
if address not in self.wallets:
|
|
465
922
|
return None
|
|
466
|
-
|
|
923
|
+
|
|
467
924
|
wallet_data = self.wallets[address]
|
|
468
925
|
|
|
469
926
|
try:
|
|
@@ -482,16 +939,17 @@ class LunaWallet:
|
|
|
482
939
|
address = wallet_data.get('address')
|
|
483
940
|
if not address:
|
|
484
941
|
return False
|
|
485
|
-
|
|
942
|
+
|
|
943
|
+
# Check if wallet uses SM2 cryptography
|
|
944
|
+
if wallet_data.get('crypto_standard') != 'SM2_GB/T_32918':
|
|
945
|
+
print(f"DEBUG: WARNING: Importing wallet without SM2 cryptography standard")
|
|
946
|
+
|
|
486
947
|
# Add to wallets collection
|
|
487
948
|
self.wallets[address] = wallet_data.copy()
|
|
488
949
|
|
|
489
950
|
# Set as current wallet
|
|
490
951
|
self._set_current_wallet(wallet_data)
|
|
491
952
|
|
|
492
|
-
# Refresh balances for imported wallet
|
|
493
|
-
self.refresh_balance()
|
|
494
|
-
|
|
495
953
|
if password and wallet_data.get('encrypted_private_key'):
|
|
496
954
|
return self.unlock_wallet(address, password)
|
|
497
955
|
|
|
@@ -499,11 +957,10 @@ class LunaWallet:
|
|
|
499
957
|
return False
|
|
500
958
|
|
|
501
959
|
def update_balance(self, new_balance):
|
|
502
|
-
"""Update current wallet balance
|
|
960
|
+
"""Update current wallet balance"""
|
|
503
961
|
self.balance = float(new_balance)
|
|
504
962
|
self.available_balance = float(new_balance)
|
|
505
963
|
|
|
506
|
-
# Also update in wallets collection
|
|
507
964
|
if self.current_wallet_address and self.current_wallet_address in self.wallets:
|
|
508
965
|
self.wallets[self.current_wallet_address]['balance'] = self.balance
|
|
509
966
|
self.wallets[self.current_wallet_address]['available_balance'] = self.available_balance
|
|
@@ -531,10 +988,203 @@ class LunaWallet:
|
|
|
531
988
|
if not self.current_wallet_address:
|
|
532
989
|
return None
|
|
533
990
|
|
|
534
|
-
|
|
991
|
+
return self.wallets.get(self.current_wallet_address)
|
|
992
|
+
|
|
993
|
+
def get_wallet_info(self):
|
|
994
|
+
"""Get complete wallet information for current wallet"""
|
|
995
|
+
if not self.address:
|
|
996
|
+
return None
|
|
997
|
+
|
|
535
998
|
self.refresh_balance()
|
|
536
999
|
|
|
537
|
-
return
|
|
1000
|
+
return {
|
|
1001
|
+
'address': self.address,
|
|
1002
|
+
'balance': self.balance,
|
|
1003
|
+
'available_balance': self.available_balance,
|
|
1004
|
+
'created': self.created,
|
|
1005
|
+
'private_key': self.private_key,
|
|
1006
|
+
'public_key': self.public_key,
|
|
1007
|
+
'encrypted_private_key': self.encrypted_private_key,
|
|
1008
|
+
'label': self.label,
|
|
1009
|
+
'is_locked': self.is_locked,
|
|
1010
|
+
'crypto_standard': 'SM2_GB/T_32918'
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
# ============================================================================
|
|
1014
|
+
# UNIFIED WALLET STATE MANAGER INTEGRATION
|
|
1015
|
+
# ============================================================================
|
|
1016
|
+
|
|
1017
|
+
def sync_with_state_manager(self, blockchain=None, mempool=None) -> Dict:
|
|
1018
|
+
"""
|
|
1019
|
+
Sync all registered wallets with the unified WalletStateManager.
|
|
1020
|
+
Scans blockchain once and merges mempool data for all wallets.
|
|
1021
|
+
|
|
1022
|
+
Parameters:
|
|
1023
|
+
blockchain: BlockchainManager instance (required)
|
|
1024
|
+
mempool: MempoolManager instance (required)
|
|
1025
|
+
|
|
1026
|
+
Returns: Dictionary of wallet summaries with balances and transactions
|
|
1027
|
+
"""
|
|
1028
|
+
try:
|
|
1029
|
+
if not blockchain or not mempool:
|
|
1030
|
+
print("❌ blockchain and mempool instances required")
|
|
1031
|
+
return {}
|
|
1032
|
+
|
|
1033
|
+
from .wallet_manager import get_wallet_manager
|
|
1034
|
+
|
|
1035
|
+
state_manager = get_wallet_manager()
|
|
1036
|
+
|
|
1037
|
+
# Register all wallets with state manager if not already done
|
|
1038
|
+
addresses = list(self.wallets.keys())
|
|
1039
|
+
state_manager.register_wallets(addresses)
|
|
1040
|
+
|
|
1041
|
+
print(f"🔄 Syncing {len(addresses)} wallets...")
|
|
1042
|
+
|
|
1043
|
+
# Get data from blockchain and mempool (single scan)
|
|
1044
|
+
blockchain_txs = blockchain.scan_transactions_for_addresses(addresses)
|
|
1045
|
+
mempool_txs = mempool.get_pending_transactions_for_addresses(addresses)
|
|
1046
|
+
|
|
1047
|
+
# Sync state manager with the data
|
|
1048
|
+
state_manager.sync_wallets_from_sources(blockchain_txs, mempool_txs)
|
|
1049
|
+
|
|
1050
|
+
# Update LunaWallet balances from state manager
|
|
1051
|
+
balances = state_manager.get_all_balances()
|
|
1052
|
+
for address, balance_data in balances.items():
|
|
1053
|
+
if address in self.wallets:
|
|
1054
|
+
self.wallets[address]['balance'] = balance_data['confirmed_balance']
|
|
1055
|
+
self.wallets[address]['available_balance'] = balance_data['available_balance']
|
|
1056
|
+
|
|
1057
|
+
# Update current wallet
|
|
1058
|
+
if self.current_wallet_address and self.current_wallet_address in balances:
|
|
1059
|
+
balance_data = balances[self.current_wallet_address]
|
|
1060
|
+
self.balance = balance_data['confirmed_balance']
|
|
1061
|
+
self.available_balance = balance_data['available_balance']
|
|
1062
|
+
|
|
1063
|
+
# Return summaries
|
|
1064
|
+
summaries = state_manager.get_all_summaries()
|
|
1065
|
+
print(f"✅ Sync complete - {len(summaries)} wallets updated")
|
|
1066
|
+
|
|
1067
|
+
return summaries
|
|
1068
|
+
|
|
1069
|
+
except Exception as e:
|
|
1070
|
+
print(f"❌ Sync error: {e}")
|
|
1071
|
+
import traceback
|
|
1072
|
+
traceback.print_exc()
|
|
1073
|
+
return {}
|
|
1074
|
+
|
|
1075
|
+
def get_wallet_details(self, address: str = None) -> Optional[Dict]:
|
|
1076
|
+
"""
|
|
1077
|
+
Get detailed information for a wallet including balance and transaction summary.
|
|
1078
|
+
If address is None, uses current wallet.
|
|
1079
|
+
"""
|
|
1080
|
+
if address is None:
|
|
1081
|
+
address = self.current_wallet_address
|
|
1082
|
+
|
|
1083
|
+
if not address:
|
|
1084
|
+
return None
|
|
1085
|
+
|
|
1086
|
+
try:
|
|
1087
|
+
from .wallet_manager import get_wallet_manager
|
|
1088
|
+
state_manager = get_wallet_manager()
|
|
1089
|
+
|
|
1090
|
+
summary = state_manager.get_wallet_summary(address)
|
|
1091
|
+
if summary:
|
|
1092
|
+
return summary
|
|
1093
|
+
|
|
1094
|
+
# Fallback to basic wallet info
|
|
1095
|
+
if address in self.wallets:
|
|
1096
|
+
wallet_data = self.wallets[address]
|
|
1097
|
+
return {
|
|
1098
|
+
'address': address,
|
|
1099
|
+
'label': wallet_data.get('label', 'Wallet'),
|
|
1100
|
+
'balance': wallet_data.get('balance', 0.0),
|
|
1101
|
+
'available_balance': wallet_data.get('available_balance', 0.0),
|
|
1102
|
+
'is_locked': wallet_data.get('is_locked', True),
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return None
|
|
1106
|
+
|
|
1107
|
+
except Exception as e:
|
|
1108
|
+
print(f"⚠️ Error getting wallet details: {e}")
|
|
1109
|
+
return None
|
|
1110
|
+
|
|
1111
|
+
def get_wallet_transactions(self, address: str = None, tx_type: str = 'all') -> List[Dict]:
|
|
1112
|
+
"""
|
|
1113
|
+
Get transactions for a wallet from the state manager.
|
|
1114
|
+
|
|
1115
|
+
tx_type: 'all', 'confirmed', 'pending', 'transfers', 'rewards', 'genesis'
|
|
1116
|
+
"""
|
|
1117
|
+
if address is None:
|
|
1118
|
+
address = self.current_wallet_address
|
|
1119
|
+
|
|
1120
|
+
if not address:
|
|
1121
|
+
return []
|
|
1122
|
+
|
|
1123
|
+
try:
|
|
1124
|
+
from .wallet_manager import get_wallet_manager
|
|
1125
|
+
state_manager = get_wallet_manager()
|
|
1126
|
+
|
|
1127
|
+
return state_manager.get_transactions(address, tx_type)
|
|
1128
|
+
|
|
1129
|
+
except Exception as e:
|
|
1130
|
+
print(f"⚠️ Error getting transactions: {e}")
|
|
1131
|
+
return []
|
|
1132
|
+
|
|
1133
|
+
def register_wallet_ui_callback(self, callback: Callable) -> None:
|
|
1134
|
+
"""
|
|
1135
|
+
Register a callback to receive real-time wallet balance updates.
|
|
1136
|
+
Callback will be called with: callback(balance_data_dict)
|
|
1137
|
+
"""
|
|
1138
|
+
try:
|
|
1139
|
+
from .wallet_manager import get_wallet_manager
|
|
1140
|
+
state_manager = get_wallet_manager()
|
|
1141
|
+
state_manager.on_balance_update(callback)
|
|
1142
|
+
except Exception as e:
|
|
1143
|
+
print(f"⚠️ Error registering callback: {e}")
|
|
1144
|
+
|
|
1145
|
+
def start_continuous_sync(self, blockchain=None, mempool=None, poll_interval: int = 30) -> None:
|
|
1146
|
+
"""
|
|
1147
|
+
Start continuous synchronization in background thread.
|
|
1148
|
+
Syncs every poll_interval seconds.
|
|
1149
|
+
"""
|
|
1150
|
+
if not blockchain or not mempool:
|
|
1151
|
+
print("❌ blockchain and mempool instances required")
|
|
1152
|
+
return
|
|
1153
|
+
|
|
1154
|
+
try:
|
|
1155
|
+
from .wallet_manager import get_wallet_manager
|
|
1156
|
+
from .wallet_sync_helper import WalletSyncHelper
|
|
1157
|
+
|
|
1158
|
+
state_manager = get_wallet_manager()
|
|
1159
|
+
|
|
1160
|
+
# Register wallets
|
|
1161
|
+
addresses = list(self.wallets.keys())
|
|
1162
|
+
state_manager.register_wallets(addresses)
|
|
1163
|
+
|
|
1164
|
+
def sync_callback(balance_data):
|
|
1165
|
+
"""Update LunaWallet when state manager updates"""
|
|
1166
|
+
for address, balance_info in balance_data.items():
|
|
1167
|
+
if address in self.wallets:
|
|
1168
|
+
self.wallets[address]['balance'] = balance_info['confirmed_balance']
|
|
1169
|
+
self.wallets[address]['available_balance'] = balance_info['available_balance']
|
|
1170
|
+
|
|
1171
|
+
if address == self.current_wallet_address:
|
|
1172
|
+
self.balance = balance_info['confirmed_balance']
|
|
1173
|
+
self.available_balance = balance_info['available_balance']
|
|
1174
|
+
|
|
1175
|
+
# Create sync helper
|
|
1176
|
+
sync_helper = WalletSyncHelper(self, blockchain, mempool)
|
|
1177
|
+
|
|
1178
|
+
# Start continuous sync with callback
|
|
1179
|
+
sync_helper.start_continuous_sync(
|
|
1180
|
+
poll_interval,
|
|
1181
|
+
on_balance_update=sync_callback
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
print(f"🔄 Started continuous sync (interval: {poll_interval}s)")
|
|
1185
|
+
|
|
1186
|
+
except Exception as e:
|
|
1187
|
+
print(f"❌ Error starting continuous sync: {e}")
|
|
538
1188
|
|
|
539
1189
|
def save_to_file(self, filename=None):
|
|
540
1190
|
"""Save wallet to file"""
|
|
@@ -547,23 +1197,18 @@ class LunaWallet:
|
|
|
547
1197
|
filepath = os.path.join(self.data_dir, filename)
|
|
548
1198
|
|
|
549
1199
|
try:
|
|
550
|
-
# Ensure directory exists
|
|
551
1200
|
os.makedirs(self.data_dir, exist_ok=True)
|
|
552
1201
|
|
|
553
|
-
# Prepare encrypted private key for serialization
|
|
554
1202
|
encrypted_key_data = None
|
|
555
1203
|
if self.encrypted_private_key:
|
|
556
|
-
# Ensure it's bytes before encoding
|
|
557
1204
|
if isinstance(self.encrypted_private_key, bytes):
|
|
558
1205
|
encrypted_key_data = base64.b64encode(self.encrypted_private_key).decode('utf-8')
|
|
559
1206
|
else:
|
|
560
1207
|
encrypted_key_data = base64.b64encode(self.encrypted_private_key.encode()).decode('utf-8')
|
|
561
1208
|
|
|
562
|
-
# Prepare wallets for serialization (remove any non-serializable data)
|
|
563
1209
|
serializable_wallets = {}
|
|
564
1210
|
for addr, wallet_info in self.wallets.items():
|
|
565
1211
|
serializable_wallet = wallet_info.copy()
|
|
566
|
-
# Ensure encrypted_private_key is serializable
|
|
567
1212
|
if serializable_wallet.get('encrypted_private_key') and isinstance(serializable_wallet['encrypted_private_key'], bytes):
|
|
568
1213
|
serializable_wallet['encrypted_private_key'] = base64.b64encode(
|
|
569
1214
|
serializable_wallet['encrypted_private_key']
|
|
@@ -580,18 +1225,21 @@ class LunaWallet:
|
|
|
580
1225
|
'label': self.label,
|
|
581
1226
|
'is_locked': self.is_locked,
|
|
582
1227
|
'wallets': serializable_wallets,
|
|
583
|
-
'current_wallet_address': self.current_wallet_address
|
|
1228
|
+
'current_wallet_address': self.current_wallet_address,
|
|
1229
|
+
'crypto_standard': 'SM2_GB/T_32918'
|
|
584
1230
|
}
|
|
585
1231
|
|
|
586
1232
|
with open(filepath, 'w') as f:
|
|
587
1233
|
json.dump(wallet_data, f, indent=2)
|
|
1234
|
+
|
|
1235
|
+
print(f"DEBUG: Wallet saved to {filepath}")
|
|
588
1236
|
return True
|
|
589
1237
|
except Exception as e:
|
|
590
1238
|
print(f"Error saving wallet: {e}")
|
|
591
1239
|
import traceback
|
|
592
1240
|
traceback.print_exc()
|
|
593
1241
|
return False
|
|
594
|
-
|
|
1242
|
+
|
|
595
1243
|
def load_from_file(self, filename, password=None):
|
|
596
1244
|
"""Load wallet from file"""
|
|
597
1245
|
if not self.data_dir:
|
|
@@ -603,6 +1251,11 @@ class LunaWallet:
|
|
|
603
1251
|
with open(filepath, 'r') as f:
|
|
604
1252
|
wallet_data = json.load(f)
|
|
605
1253
|
|
|
1254
|
+
# Check crypto standard
|
|
1255
|
+
crypto_standard = wallet_data.get('crypto_standard')
|
|
1256
|
+
if crypto_standard != 'SM2_GB/T_32918':
|
|
1257
|
+
print(f"DEBUG: WARNING: Loading wallet with different crypto standard: {crypto_standard}")
|
|
1258
|
+
|
|
606
1259
|
# Load wallets collection
|
|
607
1260
|
self.wallets = wallet_data.get('wallets', {})
|
|
608
1261
|
|
|
@@ -614,22 +1267,76 @@ class LunaWallet:
|
|
|
614
1267
|
current_wallet_data = self.wallets[self.current_wallet_address]
|
|
615
1268
|
self._set_current_wallet(current_wallet_data)
|
|
616
1269
|
|
|
617
|
-
# Handle encrypted private key
|
|
618
1270
|
encrypted_key = wallet_data.get('encrypted_private_key')
|
|
619
1271
|
if encrypted_key:
|
|
620
1272
|
self.encrypted_private_key = base64.b64decode(encrypted_key.encode())
|
|
621
|
-
# Also update in wallets collection
|
|
622
1273
|
if self.current_wallet_address in self.wallets:
|
|
623
1274
|
self.wallets[self.current_wallet_address]['encrypted_private_key'] = self.encrypted_private_key
|
|
624
1275
|
|
|
625
|
-
# Refresh balances after loading
|
|
626
1276
|
self.refresh_balance()
|
|
627
1277
|
|
|
628
|
-
# If password provided and we have encrypted key, unlock
|
|
629
1278
|
if password and self.encrypted_private_key and self.current_wallet_address:
|
|
630
1279
|
return self.unlock_wallet(self.current_wallet_address, password)
|
|
631
1280
|
|
|
1281
|
+
print(f"DEBUG: Wallet loaded from {filepath}")
|
|
1282
|
+
print(f"DEBUG: Total wallets: {len(self.wallets)}")
|
|
1283
|
+
print(f"DEBUG: Current wallet: {self.current_wallet_address}")
|
|
1284
|
+
|
|
632
1285
|
return True
|
|
633
1286
|
except Exception as e:
|
|
634
1287
|
print(f"Error loading wallet: {e}")
|
|
635
|
-
return False
|
|
1288
|
+
return False
|
|
1289
|
+
|
|
1290
|
+
def debug_crypto_info(self):
|
|
1291
|
+
"""Debug cryptographic information"""
|
|
1292
|
+
print("\n" + "="*60)
|
|
1293
|
+
print("SM2 CRYPTOGRAPHY DEBUG INFO")
|
|
1294
|
+
print("="*60)
|
|
1295
|
+
|
|
1296
|
+
if self.current_wallet_address:
|
|
1297
|
+
print(f"Current Address: {self.address}")
|
|
1298
|
+
|
|
1299
|
+
# Check address format
|
|
1300
|
+
if self.address.startswith('LUN_'):
|
|
1301
|
+
print("✅ Address has correct LUN_ prefix")
|
|
1302
|
+
else:
|
|
1303
|
+
print("❌ Address missing LUN_ prefix")
|
|
1304
|
+
|
|
1305
|
+
# Check private key
|
|
1306
|
+
if self.private_key:
|
|
1307
|
+
print(f"Private Key: {self.private_key[:16]}...")
|
|
1308
|
+
if len(self.private_key) == 64:
|
|
1309
|
+
print(f"✅ Private key is 256-bit (64 hex chars)")
|
|
1310
|
+
else:
|
|
1311
|
+
print(f"❌ Private key has wrong length: {len(self.private_key)} chars")
|
|
1312
|
+
else:
|
|
1313
|
+
print("❌ No private key available")
|
|
1314
|
+
|
|
1315
|
+
# Check public key
|
|
1316
|
+
if self.public_key:
|
|
1317
|
+
print(f"Public Key: {self.public_key[:16]}...")
|
|
1318
|
+
if len(self.public_key) >= 128:
|
|
1319
|
+
print(f"✅ Public key is valid length")
|
|
1320
|
+
else:
|
|
1321
|
+
print(f"❌ Public key too short: {len(self.public_key)} chars")
|
|
1322
|
+
else:
|
|
1323
|
+
print("❌ No public key available")
|
|
1324
|
+
|
|
1325
|
+
# Check cryptographic standard
|
|
1326
|
+
wallet_data = self.wallets.get(self.address, {})
|
|
1327
|
+
if wallet_data.get('crypto_standard') == 'SM2_GB/T_32918':
|
|
1328
|
+
print("✅ Using SM2 GB/T 32918 East Asian cryptography standard")
|
|
1329
|
+
else:
|
|
1330
|
+
print("❌ Not using SM2 cryptography standard")
|
|
1331
|
+
|
|
1332
|
+
# Test signing
|
|
1333
|
+
if self.private_key and not self.is_locked:
|
|
1334
|
+
test_sig = self._sign_transaction_data({"test": "data"})
|
|
1335
|
+
if test_sig:
|
|
1336
|
+
print(f"✅ Can sign transactions, signature: {test_sig[:16]}...")
|
|
1337
|
+
else:
|
|
1338
|
+
print("❌ Cannot sign transactions")
|
|
1339
|
+
else:
|
|
1340
|
+
print("❌ No wallet selected")
|
|
1341
|
+
|
|
1342
|
+
print("="*60)
|