lunalib 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lunalib/core/wallet.py ADDED
@@ -0,0 +1,635 @@
1
+ # wallet.py
2
+ import time
3
+ import hashlib
4
+ import json
5
+ from cryptography.fernet import Fernet
6
+ import base64
7
+ import os
8
+
9
+ class LunaWallet:
10
+ """Luna wallet implementation with proper key management and balance tracking"""
11
+
12
+ def __init__(self, data_dir=None):
13
+ self.data_dir = data_dir
14
+ self.wallets = {} # Main wallet storage: {address: wallet_data}
15
+ self.current_wallet_address = None # Track which wallet is active
16
+
17
+ # Initialize with an empty current wallet state
18
+ self._reset_current_wallet()
19
+
20
+ def _reset_current_wallet(self):
21
+ """Reset current wallet to empty state"""
22
+ self.address = None
23
+ self.balance = 0.0 # Total balance (confirmed transactions)
24
+ self.available_balance = 0.0 # Available balance (total - pending outgoing)
25
+ self.created = time.time()
26
+ self.private_key = None
27
+ self.public_key = None
28
+ self.encrypted_private_key = None
29
+ self.label = "New Wallet"
30
+ self.is_locked = True
31
+
32
+ def _generate_address(self):
33
+ """Generate unique wallet address"""
34
+ import secrets
35
+ import time
36
+ # Use cryptographically secure random data for uniqueness
37
+ random_data = secrets.token_hex(32)
38
+ timestamp_ns = time.time_ns() # More precise timestamp
39
+ base_data = f"LUN_{timestamp_ns}_{random_data}"
40
+ return hashlib.sha256(base_data.encode()).hexdigest()[:32]
41
+
42
+ def calculate_available_balance(self) -> float:
43
+ """Calculate available balance (total balance minus pending outgoing transactions)"""
44
+ try:
45
+ from lunalib.core.mempool import MempoolManager
46
+ from lunalib.core.blockchain import BlockchainManager
47
+
48
+ # Get total balance from blockchain
49
+ total_balance = self._get_total_balance_from_blockchain()
50
+
51
+ # Get pending outgoing transactions from mempool
52
+ mempool = MempoolManager()
53
+ pending_txs = mempool.get_pending_transactions(self.address)
54
+
55
+ # Sum pending outgoing amounts
56
+ pending_outgoing = 0.0
57
+ for tx in pending_txs:
58
+ if tx.get('from') == self.address:
59
+ pending_outgoing += float(tx.get('amount', 0)) + float(tx.get('fee', 0))
60
+
61
+ available_balance = max(0.0, total_balance - pending_outgoing)
62
+
63
+ # Update both current wallet and wallets collection
64
+ self.available_balance = available_balance
65
+ if self.current_wallet_address in self.wallets:
66
+ self.wallets[self.current_wallet_address]['available_balance'] = available_balance
67
+
68
+ print(f"DEBUG: Available balance calculated - Total: {total_balance}, Pending Out: {pending_outgoing}, Available: {available_balance}")
69
+ return available_balance
70
+
71
+ except Exception as e:
72
+ print(f"DEBUG: Error calculating available balance: {e}")
73
+ return self.balance # Fallback to total balance
74
+
75
+ def _get_total_balance_from_blockchain(self) -> float:
76
+ """Get total balance by scanning blockchain for confirmed transactions"""
77
+ try:
78
+ from lunalib.core.blockchain import BlockchainManager
79
+
80
+ blockchain = BlockchainManager()
81
+ transactions = blockchain.scan_transactions_for_address(self.address)
82
+
83
+ total_balance = 0.0
84
+ for tx in transactions:
85
+ tx_type = tx.get('type', '')
86
+
87
+ # Handle incoming transactions
88
+ if tx.get('to') == self.address:
89
+ if tx_type in ['transfer', 'reward', 'fee_distribution', 'gtx_genesis']:
90
+ total_balance += float(tx.get('amount', 0))
91
+
92
+ # Handle outgoing transactions
93
+ elif tx.get('from') == self.address:
94
+ if tx_type in ['transfer', 'stake', 'delegate']:
95
+ total_balance -= float(tx.get('amount', 0))
96
+ total_balance -= float(tx.get('fee', 0))
97
+
98
+ return max(0.0, total_balance)
99
+
100
+ except Exception as e:
101
+ print(f"DEBUG: Error getting blockchain balance: {e}")
102
+ return self.balance
103
+
104
+ def refresh_balance(self) -> bool:
105
+ """Refresh both total and available balance from blockchain and mempool"""
106
+ try:
107
+ total_balance = self._get_total_balance_from_blockchain()
108
+ available_balance = self.calculate_available_balance()
109
+
110
+ # Update wallet state
111
+ self.balance = total_balance
112
+ self.available_balance = available_balance
113
+
114
+ # Update in wallets collection
115
+ if self.current_wallet_address in self.wallets:
116
+ self.wallets[self.current_wallet_address]['balance'] = total_balance
117
+ self.wallets[self.current_wallet_address]['available_balance'] = available_balance
118
+
119
+ print(f"DEBUG: Balance refreshed - Total: {total_balance}, Available: {available_balance}")
120
+ return True
121
+
122
+ except Exception as e:
123
+ print(f"DEBUG: Error refreshing balance: {e}")
124
+ return False
125
+
126
+ def get_available_balance(self) -> float:
127
+ """Get current wallet available balance"""
128
+ return self.available_balance
129
+
130
+ def _get_total_balance_from_blockchain(self) -> float:
131
+ """Get total balance by scanning blockchain for confirmed transactions"""
132
+ try:
133
+ from lunalib.core.blockchain import BlockchainManager
134
+
135
+ blockchain = BlockchainManager()
136
+ transactions = blockchain.scan_transactions_for_address(self.address)
137
+
138
+ total_balance = 0.0
139
+ for tx in transactions:
140
+ tx_type = tx.get('type', '')
141
+
142
+ # Handle incoming transactions
143
+ if tx.get('to') == self.address:
144
+ if tx_type in ['transfer', 'reward', 'fee_distribution', 'gtx_genesis']:
145
+ total_balance += float(tx.get('amount', 0))
146
+
147
+ # Handle outgoing transactions
148
+ elif tx.get('from') == self.address:
149
+ if tx_type in ['transfer', 'stake', 'delegate']:
150
+ total_balance -= float(tx.get('amount', 0))
151
+ total_balance -= float(tx.get('fee', 0))
152
+
153
+ return max(0.0, total_balance)
154
+
155
+ except Exception as e:
156
+ print(f"DEBUG: Error getting blockchain balance: {e}")
157
+ return self.balance
158
+
159
+ def refresh_balance(self) -> bool:
160
+ """Refresh both total and available balance from blockchain and mempool"""
161
+ try:
162
+ total_balance = self._get_total_balance_from_blockchain()
163
+ available_balance = self.calculate_available_balance()
164
+
165
+ # Update wallet state
166
+ self.balance = total_balance
167
+ self.available_balance = available_balance
168
+
169
+ # Update in wallets collection
170
+ if self.current_wallet_address in self.wallets:
171
+ self.wallets[self.current_wallet_address]['balance'] = total_balance
172
+ self.wallets[self.current_wallet_address]['available_balance'] = available_balance
173
+
174
+ print(f"DEBUG: Balance refreshed - Total: {total_balance}, Available: {available_balance}")
175
+ return True
176
+
177
+ except Exception as e:
178
+ print(f"DEBUG: Error refreshing balance: {e}")
179
+ return False
180
+
181
+ def send_transaction(self, to_address: str, amount: float, memo: str = "", password: str = None) -> bool:
182
+ """Send transaction using lunalib transactions with proper mempool submission"""
183
+ try:
184
+ print(f"DEBUG: send_transaction called - to: {to_address}, amount: {amount}, memo: {memo}")
185
+
186
+ # Refresh balances first to get latest state
187
+ self.refresh_balance()
188
+
189
+ # Check available balance before proceeding
190
+ if amount > self.available_balance:
191
+ print(f"DEBUG: Insufficient available balance: {self.available_balance} < {amount}")
192
+ return False
193
+
194
+ # Check if wallet is unlocked
195
+ if self.is_locked or not self.private_key:
196
+ print("DEBUG: Wallet is locked or no private key available")
197
+ return False
198
+
199
+ # Import transaction manager
200
+ from lunalib.transactions.transactions import TransactionManager
201
+
202
+ # Create transaction manager
203
+ tx_manager = TransactionManager()
204
+
205
+ # Create and sign transaction
206
+ transaction = tx_manager.create_transaction(
207
+ from_address=self.address,
208
+ to_address=to_address,
209
+ amount=amount,
210
+ private_key=self.private_key,
211
+ memo=memo,
212
+ transaction_type="transfer"
213
+ )
214
+
215
+ print(f"DEBUG: Transaction created: {transaction.get('hash')}")
216
+
217
+ # Validate transaction
218
+ is_valid, message = tx_manager.validate_transaction(transaction)
219
+ if not is_valid:
220
+ print(f"DEBUG: Transaction validation failed: {message}")
221
+ return False
222
+
223
+ # Send to mempool for broadcasting
224
+ success, message = tx_manager.send_transaction(transaction)
225
+ if success:
226
+ print(f"DEBUG: Transaction sent to mempool: {message}")
227
+
228
+ # Update available balance immediately (deduct pending transaction)
229
+ fee = transaction.get('fee', 0)
230
+ self.available_balance -= (amount + fee)
231
+ if self.current_wallet_address in self.wallets:
232
+ self.wallets[self.current_wallet_address]['available_balance'] = self.available_balance
233
+
234
+ print(f"DEBUG: Available balance updated - new available: {self.available_balance}")
235
+ return True
236
+ else:
237
+ print(f"DEBUG: Failed to send transaction to mempool: {message}")
238
+ return False
239
+
240
+ except Exception as e:
241
+ print(f"DEBUG: Error in send_transaction: {e}")
242
+ import traceback
243
+ traceback.print_exc()
244
+ return False
245
+
246
+ def send_transaction_from(self, from_address: str, to_address: str, amount: float, memo: str = "", password: str = None) -> bool:
247
+ """Send transaction from specific address"""
248
+ try:
249
+ print(f"DEBUG: send_transaction_from called - from: {from_address}, to: {to_address}, amount: {amount}")
250
+
251
+ # Switch to the specified wallet if different from current
252
+ if from_address != self.current_wallet_address:
253
+ if from_address in self.wallets:
254
+ # Switch to the wallet first
255
+ wallet_data = self.wallets[from_address]
256
+ self._set_current_wallet(wallet_data)
257
+
258
+ # If password provided, unlock the wallet
259
+ if password:
260
+ unlock_success = self.unlock_wallet(from_address, password)
261
+ if not unlock_success:
262
+ print("DEBUG: Failed to unlock wallet for sending")
263
+ return False
264
+ else:
265
+ print(f"DEBUG: Wallet not found: {from_address}")
266
+ return False
267
+
268
+ # Now use the regular send_transaction method
269
+ return self.send_transaction(to_address, amount, memo, password)
270
+
271
+ except Exception as e:
272
+ print(f"DEBUG: Error in send_transaction_from: {e}")
273
+ import traceback
274
+ traceback.print_exc()
275
+ return False
276
+
277
+ def get_transaction_history(self) -> dict:
278
+ """Get complete transaction history (both pending and confirmed)"""
279
+ try:
280
+ from lunalib.blockchain import BlockchainManager
281
+ from lunalib.core.mempool import MempoolManager
282
+
283
+ blockchain = BlockchainManager()
284
+ mempool = MempoolManager()
285
+
286
+ # Get confirmed transactions from blockchain
287
+ confirmed_txs = blockchain.scan_transactions_for_address(self.address)
288
+
289
+ # Get pending transactions from mempool
290
+ pending_txs = mempool.get_pending_transactions(self.address)
291
+
292
+ return {
293
+ 'confirmed': confirmed_txs,
294
+ 'pending': pending_txs,
295
+ 'total_confirmed': len(confirmed_txs),
296
+ 'total_pending': len(pending_txs)
297
+ }
298
+ except Exception as e:
299
+ print(f"DEBUG: Error getting transaction history: {e}")
300
+ return {'confirmed': [], 'pending': [], 'total_confirmed': 0, 'total_pending': 0}
301
+
302
+ def _generate_private_key(self):
303
+ """Generate private key"""
304
+ return f"priv_{hashlib.sha256(str(time.time()).encode()).hexdigest()}"
305
+
306
+ def _derive_public_key(self, private_key=None):
307
+ """Derive public key from private key"""
308
+ priv_key = private_key or self.private_key
309
+ if not priv_key:
310
+ return None
311
+ return f"pub_{priv_key[-16:]}"
312
+
313
+ def get_wallet_info(self):
314
+ """Get complete wallet information for current wallet"""
315
+ if not self.address:
316
+ return None
317
+
318
+ # Refresh balances to ensure they're current
319
+ self.refresh_balance()
320
+
321
+ return {
322
+ 'address': self.address,
323
+ 'balance': self.balance,
324
+ 'available_balance': self.available_balance,
325
+ 'created': self.created,
326
+ 'private_key': self.private_key,
327
+ 'public_key': self.public_key,
328
+ 'encrypted_private_key': self.encrypted_private_key,
329
+ 'label': self.label,
330
+ 'is_locked': self.is_locked
331
+ }
332
+
333
+ def create_new_wallet(self, name, password):
334
+ """Create a new wallet and add to collection without switching"""
335
+ # Generate new wallet data
336
+ address = self._generate_address()
337
+ private_key = self._generate_private_key()
338
+ public_key = f"pub_{private_key[-16:]}"
339
+
340
+ # Encrypt private key
341
+ key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
342
+ fernet = Fernet(key)
343
+ encrypted_private_key = fernet.encrypt(private_key.encode())
344
+
345
+ # Create new wallet data
346
+ new_wallet_data = {
347
+ 'address': address,
348
+ 'balance': 0.0,
349
+ 'available_balance': 0.0,
350
+ 'created': time.time(),
351
+ 'private_key': private_key,
352
+ 'public_key': public_key,
353
+ 'encrypted_private_key': encrypted_private_key,
354
+ 'label': name,
355
+ 'is_locked': True
356
+ }
357
+
358
+ # CRITICAL: Add to wallets collection
359
+ self.wallets[address] = new_wallet_data
360
+
361
+ print(f"DEBUG: Created new wallet {address}, total wallets: {len(self.wallets)}")
362
+
363
+ return new_wallet_data
364
+
365
+ def create_wallet(self, name, password):
366
+ """Create a new wallet and set it as current"""
367
+ # Generate new wallet data
368
+ address = self._generate_address()
369
+ private_key = self._generate_private_key()
370
+ public_key = f"pub_{private_key[-16:]}"
371
+
372
+ # Encrypt private key
373
+ key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
374
+ fernet = Fernet(key)
375
+ encrypted_private_key = fernet.encrypt(private_key.encode())
376
+
377
+ # Create wallet data
378
+ wallet_data = {
379
+ 'address': address,
380
+ 'balance': 0.0,
381
+ 'available_balance': 0.0,
382
+ 'created': time.time(),
383
+ 'private_key': private_key,
384
+ 'public_key': public_key,
385
+ 'encrypted_private_key': encrypted_private_key,
386
+ 'label': name,
387
+ 'is_locked': True
388
+ }
389
+
390
+ # CRITICAL: Add to wallets collection
391
+ self.wallets[address] = wallet_data
392
+
393
+ # Set as current wallet
394
+ self._set_current_wallet(wallet_data)
395
+
396
+ print(f"DEBUG: Created and switched to wallet {address}, total wallets: {len(self.wallets)}")
397
+
398
+ return wallet_data
399
+
400
+ def _set_current_wallet(self, wallet_data):
401
+ """Set the current wallet from wallet data"""
402
+ self.current_wallet_address = wallet_data['address']
403
+ self.address = wallet_data['address']
404
+ self.balance = wallet_data['balance']
405
+ self.available_balance = wallet_data['available_balance']
406
+ self.created = wallet_data['created']
407
+ self.private_key = wallet_data['private_key']
408
+ self.public_key = wallet_data['public_key']
409
+ self.encrypted_private_key = wallet_data['encrypted_private_key']
410
+ self.label = wallet_data['label']
411
+ self.is_locked = wallet_data.get('is_locked', True)
412
+
413
+ def switch_wallet(self, address, password=None):
414
+ """Switch to a different wallet in the collection"""
415
+ if address in self.wallets:
416
+ wallet_data = self.wallets[address]
417
+ self._set_current_wallet(wallet_data)
418
+
419
+ # Refresh balances for the new wallet
420
+ self.refresh_balance()
421
+
422
+ # If password provided, unlock the wallet
423
+ if password:
424
+ return self.unlock_wallet(address, password)
425
+
426
+ return True
427
+ return False
428
+
429
+ def unlock_wallet(self, address, password):
430
+ """Unlock wallet with password"""
431
+ if address not in self.wallets:
432
+ return False
433
+
434
+ wallet_data = self.wallets[address]
435
+
436
+ try:
437
+ if wallet_data.get('encrypted_private_key'):
438
+ key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
439
+ fernet = Fernet(key)
440
+ decrypted_key = fernet.decrypt(wallet_data['encrypted_private_key'])
441
+ wallet_data['private_key'] = decrypted_key.decode()
442
+ wallet_data['is_locked'] = False
443
+
444
+ # If this is the current wallet, update current state
445
+ if self.current_wallet_address == address:
446
+ self.private_key = wallet_data['private_key']
447
+ self.is_locked = False
448
+
449
+ return True
450
+ except:
451
+ pass
452
+ return False
453
+
454
+ @property
455
+ def is_unlocked(self):
456
+ """Check if current wallet is unlocked"""
457
+ if not self.current_wallet_address:
458
+ return False
459
+ wallet_data = self.wallets.get(self.current_wallet_address, {})
460
+ return not wallet_data.get('is_locked', True)
461
+
462
+ def export_private_key(self, address, password):
463
+ """Export private key with password decryption"""
464
+ if address not in self.wallets:
465
+ return None
466
+
467
+ wallet_data = self.wallets[address]
468
+
469
+ try:
470
+ if wallet_data.get('encrypted_private_key'):
471
+ key = base64.urlsafe_b64encode(hashlib.sha256(password.encode()).digest())
472
+ fernet = Fernet(key)
473
+ decrypted_key = fernet.decrypt(wallet_data['encrypted_private_key'])
474
+ return decrypted_key.decode()
475
+ except:
476
+ pass
477
+ return None
478
+
479
+ def import_wallet(self, wallet_data, password=None):
480
+ """Import wallet from data"""
481
+ if isinstance(wallet_data, dict):
482
+ address = wallet_data.get('address')
483
+ if not address:
484
+ return False
485
+
486
+ # Add to wallets collection
487
+ self.wallets[address] = wallet_data.copy()
488
+
489
+ # Set as current wallet
490
+ self._set_current_wallet(wallet_data)
491
+
492
+ # Refresh balances for imported wallet
493
+ self.refresh_balance()
494
+
495
+ if password and wallet_data.get('encrypted_private_key'):
496
+ return self.unlock_wallet(address, password)
497
+
498
+ return True
499
+ return False
500
+
501
+ def update_balance(self, new_balance):
502
+ """Update current wallet balance (use refresh_balance instead for accurate tracking)"""
503
+ self.balance = float(new_balance)
504
+ self.available_balance = float(new_balance)
505
+
506
+ # Also update in wallets collection
507
+ if self.current_wallet_address and self.current_wallet_address in self.wallets:
508
+ self.wallets[self.current_wallet_address]['balance'] = self.balance
509
+ self.wallets[self.current_wallet_address]['available_balance'] = self.available_balance
510
+
511
+ return True
512
+
513
+ def get_balance(self):
514
+ """Get current wallet total balance"""
515
+ return self.balance
516
+
517
+ def get_available_balance(self):
518
+ """Get current wallet available balance"""
519
+ return self.available_balance
520
+
521
+ def get_wallet_by_address(self, address):
522
+ """Get wallet by address from wallets collection"""
523
+ return self.wallets.get(address)
524
+
525
+ def list_wallets(self):
526
+ """List all wallets in collection"""
527
+ return list(self.wallets.keys())
528
+
529
+ def get_current_wallet_info(self):
530
+ """Get current wallet information"""
531
+ if not self.current_wallet_address:
532
+ return None
533
+
534
+ # Refresh balances to ensure they're current
535
+ self.refresh_balance()
536
+
537
+ return self.wallets.get(self.current_wallet_address)
538
+
539
+ def save_to_file(self, filename=None):
540
+ """Save wallet to file"""
541
+ if not self.data_dir:
542
+ return False
543
+
544
+ if filename is None:
545
+ filename = f"wallet_{self.address}.json"
546
+
547
+ filepath = os.path.join(self.data_dir, filename)
548
+
549
+ try:
550
+ # Ensure directory exists
551
+ os.makedirs(self.data_dir, exist_ok=True)
552
+
553
+ # Prepare encrypted private key for serialization
554
+ encrypted_key_data = None
555
+ if self.encrypted_private_key:
556
+ # Ensure it's bytes before encoding
557
+ if isinstance(self.encrypted_private_key, bytes):
558
+ encrypted_key_data = base64.b64encode(self.encrypted_private_key).decode('utf-8')
559
+ else:
560
+ encrypted_key_data = base64.b64encode(self.encrypted_private_key.encode()).decode('utf-8')
561
+
562
+ # Prepare wallets for serialization (remove any non-serializable data)
563
+ serializable_wallets = {}
564
+ for addr, wallet_info in self.wallets.items():
565
+ serializable_wallet = wallet_info.copy()
566
+ # Ensure encrypted_private_key is serializable
567
+ if serializable_wallet.get('encrypted_private_key') and isinstance(serializable_wallet['encrypted_private_key'], bytes):
568
+ serializable_wallet['encrypted_private_key'] = base64.b64encode(
569
+ serializable_wallet['encrypted_private_key']
570
+ ).decode('utf-8')
571
+ serializable_wallets[addr] = serializable_wallet
572
+
573
+ wallet_data = {
574
+ 'address': self.address,
575
+ 'balance': self.balance,
576
+ 'available_balance': self.available_balance,
577
+ 'created': self.created,
578
+ 'public_key': self.public_key,
579
+ 'encrypted_private_key': encrypted_key_data,
580
+ 'label': self.label,
581
+ 'is_locked': self.is_locked,
582
+ 'wallets': serializable_wallets,
583
+ 'current_wallet_address': self.current_wallet_address
584
+ }
585
+
586
+ with open(filepath, 'w') as f:
587
+ json.dump(wallet_data, f, indent=2)
588
+ return True
589
+ except Exception as e:
590
+ print(f"Error saving wallet: {e}")
591
+ import traceback
592
+ traceback.print_exc()
593
+ return False
594
+
595
+ def load_from_file(self, filename, password=None):
596
+ """Load wallet from file"""
597
+ if not self.data_dir:
598
+ return False
599
+
600
+ filepath = os.path.join(self.data_dir, filename)
601
+
602
+ try:
603
+ with open(filepath, 'r') as f:
604
+ wallet_data = json.load(f)
605
+
606
+ # Load wallets collection
607
+ self.wallets = wallet_data.get('wallets', {})
608
+
609
+ # Load current wallet address
610
+ self.current_wallet_address = wallet_data.get('current_wallet_address')
611
+
612
+ # If we have a current wallet, load its data
613
+ if self.current_wallet_address and self.current_wallet_address in self.wallets:
614
+ current_wallet_data = self.wallets[self.current_wallet_address]
615
+ self._set_current_wallet(current_wallet_data)
616
+
617
+ # Handle encrypted private key
618
+ encrypted_key = wallet_data.get('encrypted_private_key')
619
+ if encrypted_key:
620
+ self.encrypted_private_key = base64.b64decode(encrypted_key.encode())
621
+ # Also update in wallets collection
622
+ if self.current_wallet_address in self.wallets:
623
+ self.wallets[self.current_wallet_address]['encrypted_private_key'] = self.encrypted_private_key
624
+
625
+ # Refresh balances after loading
626
+ self.refresh_balance()
627
+
628
+ # If password provided and we have encrypted key, unlock
629
+ if password and self.encrypted_private_key and self.current_wallet_address:
630
+ return self.unlock_wallet(self.current_wallet_address, password)
631
+
632
+ return True
633
+ except Exception as e:
634
+ print(f"Error loading wallet: {e}")
635
+ return False
File without changes