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