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