lunalib 1.2.3__py3-none-any.whl → 1.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # blockchain.py - Updated version
2
2
 
3
- from lunalib.storage.cache import BlockchainCache
3
+ from ..storage.cache import BlockchainCache
4
4
  import requests
5
5
  import time
6
6
  import json
@@ -14,6 +14,17 @@ class BlockchainManager:
14
14
  self.endpoint_url = endpoint_url.rstrip('/')
15
15
  self.cache = BlockchainCache()
16
16
  self.network_connected = False
17
+ self._stop_events = [] # Track background monitors so they can be stopped
18
+
19
+ # ------------------------------------------------------------------
20
+ # Address helpers
21
+ # ------------------------------------------------------------------
22
+ def _normalize_address(self, addr: str) -> str:
23
+ """Normalize LUN addresses for comparison (lowercase, strip, drop prefix)."""
24
+ if not addr:
25
+ return ''
26
+ addr_str = str(addr).strip("'\" ").lower()
27
+ return addr_str[4:] if addr_str.startswith('lun_') else addr_str
17
28
 
18
29
  def broadcast_transaction(self, transaction: Dict) -> Tuple[bool, str]:
19
30
  """Broadcast transaction to mempool with enhanced error handling"""
@@ -257,6 +268,8 @@ class BlockchainManager:
257
268
  """Scan blockchain for transactions involving an address"""
258
269
  if end_height is None:
259
270
  end_height = self.get_blockchain_height()
271
+
272
+ print(f"[SCAN] Scanning transactions for {address} from block {start_height} to {end_height}")
260
273
 
261
274
  transactions = []
262
275
 
@@ -264,14 +277,108 @@ class BlockchainManager:
264
277
  batch_size = 100
265
278
  for batch_start in range(start_height, end_height + 1, batch_size):
266
279
  batch_end = min(batch_start + batch_size - 1, end_height)
280
+ print(f"[SCAN] Processing batch {batch_start}-{batch_end}...")
267
281
  blocks = self.get_blocks_range(batch_start, batch_end)
268
282
 
269
283
  for block in blocks:
270
284
  block_transactions = self._find_address_transactions(block, address)
271
285
  transactions.extend(block_transactions)
272
-
286
+
287
+ print(f"[SCAN] Found {len(transactions)} total transactions for {address}")
273
288
  return transactions
274
289
 
290
+ def scan_transactions_for_addresses(self, addresses: List[str], start_height: int = 0, end_height: int = None) -> Dict[str, List[Dict]]:
291
+ """Scan the blockchain once for multiple addresses (rewards and transfers)."""
292
+ if not addresses:
293
+ return {}
294
+
295
+ if end_height is None:
296
+ end_height = self.get_blockchain_height()
297
+
298
+ if end_height < start_height:
299
+ return {addr: [] for addr in addresses}
300
+
301
+ print(f"[MULTI-SCAN] Scanning {len(addresses)} addresses from block {start_height} to {end_height}")
302
+
303
+ # Map normalized address -> original address for quick lookup
304
+ normalized_map = {}
305
+ for addr in addresses:
306
+ norm = self._normalize_address(addr)
307
+ if norm:
308
+ normalized_map[norm] = addr
309
+
310
+ results: Dict[str, List[Dict]] = {addr: [] for addr in addresses}
311
+
312
+ batch_size = 100
313
+ for batch_start in range(start_height, end_height + 1, batch_size):
314
+ batch_end = min(batch_start + batch_size - 1, end_height)
315
+ print(f"[MULTI-SCAN] Processing batch {batch_start}-{batch_end}...")
316
+ blocks = self.get_blocks_range(batch_start, batch_end)
317
+
318
+ for block in blocks:
319
+ collected = self._collect_transactions_for_addresses(block, normalized_map)
320
+ for original_addr, txs in collected.items():
321
+ if txs:
322
+ results[original_addr].extend(txs)
323
+
324
+ # Summary
325
+ total_txs = sum(len(txs) for txs in results.values())
326
+ print(f"[MULTI-SCAN] Found {total_txs} total transactions")
327
+ for addr in addresses:
328
+ print(f" - {addr}: {len(results[addr])} transactions")
329
+
330
+ return results
331
+
332
+ def monitor_addresses(self, addresses: List[str], on_update, poll_interval: int = 15):
333
+ """Start background monitor for addresses; returns a stop event."""
334
+ import threading
335
+ from lunalib.core.mempool import MempoolManager
336
+
337
+ stop_event = threading.Event()
338
+ self._stop_events.append(stop_event)
339
+
340
+ mempool = MempoolManager()
341
+ last_height = self.get_blockchain_height()
342
+
343
+ def _emit_update(confirmed_map: Dict[str, List[Dict]], pending_map: Dict[str, List[Dict]], source: str):
344
+ try:
345
+ if on_update:
346
+ on_update({
347
+ 'confirmed': confirmed_map or {},
348
+ 'pending': pending_map or {},
349
+ 'source': source
350
+ })
351
+ except Exception as e:
352
+ print(f"Monitor callback error: {e}")
353
+
354
+ def _monitor_loop():
355
+ nonlocal last_height
356
+
357
+ # Initial emission: existing chain + current mempool
358
+ initial_confirmed = self.scan_transactions_for_addresses(addresses, 0, last_height)
359
+ initial_pending = mempool.get_pending_transactions_for_addresses(addresses)
360
+ _emit_update(initial_confirmed, initial_pending, source="initial")
361
+
362
+ while not stop_event.wait(poll_interval):
363
+ try:
364
+ current_height = self.get_blockchain_height()
365
+ if current_height > last_height:
366
+ new_confirmed = self.scan_transactions_for_addresses(addresses, last_height + 1, current_height)
367
+ if any(new_confirmed.values()):
368
+ _emit_update(new_confirmed, {}, source="blockchain")
369
+ last_height = current_height
370
+
371
+ pending_now = mempool.get_pending_transactions_for_addresses(addresses)
372
+ if any(pending_now.values()):
373
+ _emit_update({}, pending_now, source="mempool")
374
+
375
+ except Exception as e:
376
+ print(f"Monitor loop error: {e}")
377
+
378
+ thread = threading.Thread(target=_monitor_loop, daemon=True)
379
+ thread.start()
380
+ return stop_event
381
+
275
382
  def submit_mined_block(self, block_data: Dict) -> bool:
276
383
  """Submit a mined block to the network with built-in validation"""
277
384
  try:
@@ -404,174 +511,232 @@ class BlockchainManager:
404
511
  }
405
512
  }
406
513
 
514
+ def _collect_transactions_for_addresses(self, block: Dict, normalized_map: Dict[str, str]) -> Dict[str, List[Dict]]:
515
+ """Collect transactions in a block for multiple addresses in one pass."""
516
+ results: Dict[str, List[Dict]] = {original: [] for original in normalized_map.values()}
517
+
518
+ # Mining reward via block metadata
519
+ miner_norm = self._normalize_address(block.get('miner', ''))
520
+ if miner_norm in normalized_map:
521
+ reward_amount = float(block.get('reward', 0) or 0)
522
+ if reward_amount > 0:
523
+ target_addr = normalized_map[miner_norm]
524
+ reward_tx = {
525
+ 'type': 'reward',
526
+ 'from': 'network',
527
+ 'to': target_addr,
528
+ 'amount': reward_amount,
529
+ 'block_height': block.get('index'),
530
+ 'timestamp': block.get('timestamp'),
531
+ 'hash': f"reward_{block.get('index')}_{block.get('hash', '')[:8]}",
532
+ 'status': 'confirmed',
533
+ 'description': f"Mining reward for block #{block.get('index')}",
534
+ 'direction': 'incoming',
535
+ 'effective_amount': reward_amount,
536
+ 'fee': 0
537
+ }
538
+ results[target_addr].append(reward_tx)
539
+
540
+ # Regular transactions
541
+ for tx_index, tx in enumerate(block.get('transactions', [])):
542
+ tx_type = (tx.get('type') or 'transfer').lower()
543
+ from_norm = self._normalize_address(tx.get('from') or tx.get('sender') or '')
544
+ to_norm = self._normalize_address(tx.get('to') or tx.get('receiver') or '')
545
+
546
+ # Explicit reward transaction
547
+ if tx_type == 'reward' and to_norm in normalized_map:
548
+ target_addr = normalized_map[to_norm]
549
+ amount = float(tx.get('amount', 0) or 0)
550
+ enhanced = tx.copy()
551
+ enhanced.update({
552
+ 'block_height': block.get('index'),
553
+ 'status': 'confirmed',
554
+ 'tx_index': tx_index,
555
+ 'direction': 'incoming',
556
+ 'effective_amount': amount,
557
+ 'fee': 0,
558
+ })
559
+ enhanced.setdefault('from', 'network')
560
+ results[target_addr].append(enhanced)
561
+ continue
562
+
563
+ # Incoming transfer
564
+ if to_norm in normalized_map:
565
+ target_addr = normalized_map[to_norm]
566
+ amount = float(tx.get('amount', 0) or 0)
567
+ fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
568
+ enhanced = tx.copy()
569
+ enhanced.update({
570
+ 'block_height': block.get('index'),
571
+ 'status': 'confirmed',
572
+ 'tx_index': tx_index,
573
+ 'direction': 'incoming',
574
+ 'effective_amount': amount,
575
+ 'amount': amount,
576
+ 'fee': fee
577
+ })
578
+ results[target_addr].append(enhanced)
579
+
580
+ # Outgoing transfer
581
+ if from_norm in normalized_map:
582
+ target_addr = normalized_map[from_norm]
583
+ amount = float(tx.get('amount', 0) or 0)
584
+ fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
585
+ enhanced = tx.copy()
586
+ enhanced.update({
587
+ 'block_height': block.get('index'),
588
+ 'status': 'confirmed',
589
+ 'tx_index': tx_index,
590
+ 'direction': 'outgoing',
591
+ 'effective_amount': -(amount + fee),
592
+ 'amount': amount,
593
+ 'fee': fee
594
+ })
595
+ results[target_addr].append(enhanced)
596
+
597
+ # Trim empty entries
598
+ return {addr: txs for addr, txs in results.items() if txs}
599
+
407
600
  def _find_address_transactions(self, block: Dict, address: str) -> List[Dict]:
408
- """Find transactions in block that involve the address - FIXED VERSION"""
601
+ """Find transactions in block that involve the address - FIXED REWARD DETECTION"""
409
602
  transactions = []
410
- address_lower = address.lower()
603
+ address_lower = address.lower().strip('"\'') # Remove quotes if present
411
604
 
412
605
  print(f"🔍 Scanning block #{block.get('index')} for address: {address}")
606
+ print(f" Block data: {block}")
607
+
608
+ # ==================================================================
609
+ # 1. CHECK BLOCK MINING REWARD (from block metadata)
610
+ # ==================================================================
611
+ miner = block.get('miner', '')
612
+ # Clean the miner address (remove quotes, trim)
613
+ miner_clean = str(miner).strip('"\' ')
614
+
615
+ print(f" Miner in block: '{miner_clean}'")
616
+ print(f" Our address: '{address_lower}'")
617
+ print(f" Block reward: {block.get('reward', 0)}")
618
+
619
+ # Function to normalize addresses for comparison
620
+ def normalize_address(addr):
621
+ if not addr:
622
+ return ''
623
+ # Remove LUN_ prefix and quotes, convert to lowercase
624
+ addr_str = str(addr).strip('"\' ').lower()
625
+ # Remove 'lun_' prefix if present
626
+ if addr_str.startswith('lun_'):
627
+ addr_str = addr_str[4:]
628
+ return addr_str
629
+
630
+ # Normalize both addresses
631
+ miner_normalized = normalize_address(miner_clean)
632
+ address_normalized = normalize_address(address_lower)
633
+
634
+ print(f" Miner normalized: '{miner_normalized}'")
635
+ print(f" Address normalized: '{address_normalized}'")
636
+
637
+ # Check if this block was mined by our address
638
+ if miner_normalized == address_normalized and miner_normalized:
639
+ reward_amount = float(block.get('reward', 0))
640
+ if reward_amount > 0:
641
+ reward_tx = {
642
+ 'type': 'reward',
643
+ 'from': 'network',
644
+ 'to': address,
645
+ 'amount': reward_amount,
646
+ 'block_height': block.get('index'),
647
+ 'timestamp': block.get('timestamp'),
648
+ 'hash': f"reward_{block.get('index')}_{block.get('hash', '')[:8]}",
649
+ 'status': 'confirmed',
650
+ 'description': f'Mining reward for block #{block.get("index")}',
651
+ 'direction': 'incoming',
652
+ 'effective_amount': reward_amount,
653
+ 'fee': 0
654
+ }
655
+ transactions.append(reward_tx)
656
+ print(f"🎁 FOUND MINING REWARD: {reward_amount} LUN for block #{block.get('index')}")
657
+ print(f" Miner match: '{miner_clean}' == '{address}'")
658
+ else:
659
+ print(f" Not our block - Miner: '{miner_clean}', Our address: '{address}'")
413
660
 
414
- # Check block reward (miner rewards)
415
- miner = block.get('miner', '').lower()
416
- if miner == address_lower:
417
- reward_tx = {
418
- 'type': 'reward',
419
- 'from': 'network',
420
- 'to': address,
421
- 'amount': float(block.get('reward', 0)),
422
- 'block_height': block.get('index'),
423
- 'timestamp': block.get('timestamp'),
424
- 'hash': f"reward_{block.get('index')}_{address}",
425
- 'status': 'confirmed',
426
- 'description': f'Mining reward for block #{block.get("index")}',
427
- 'direction': 'incoming',
428
- 'effective_amount': float(block.get('reward', 0))
429
- }
430
- transactions.append(reward_tx)
431
- print(f"🎁 Found mining reward: {block.get('reward', 0)} LUN for block #{block.get('index')}")
432
-
433
- # Check all transactions in the block
661
+ # ==================================================================
662
+ # 2. CHECK ALL TRANSACTIONS IN THE BLOCK
663
+ # ==================================================================
434
664
  block_transactions = block.get('transactions', [])
435
- print(f" Block has {len(block_transactions)} transactions")
665
+ print(f" Block has {len(block_transactions)} transactions")
436
666
 
437
667
  for tx_index, tx in enumerate(block_transactions):
438
- print(f"\n TX #{tx_index}:")
668
+ enhanced_tx = tx.copy()
669
+ enhanced_tx['block_height'] = block.get('index')
670
+ enhanced_tx['status'] = 'confirmed'
671
+ enhanced_tx['tx_index'] = tx_index
439
672
 
440
- # Convert all values to strings for easier matching
441
- tx_str = str(tx).lower()
673
+ # Get transaction type
674
+ tx_type = tx.get('type', 'transfer').lower()
442
675
 
443
- # Method 0: Check if address appears in transaction
444
- if address_lower in tx_str:
445
- print(f" 🔄 Address found in transaction #{tx_index}")
446
- print(f" TX data: {tx}")
676
+ # Helper function for address matching with normalization
677
+ def addresses_match(addr1, addr2):
678
+ if not addr1 or not addr2:
679
+ return False
447
680
 
448
- # Use the helper method to extract information
449
- enhanced_tx = self._handle_regular_transfers(tx, address_lower)
681
+ # Normalize both addresses
682
+ addr1_norm = normalize_address(addr1)
683
+ addr2_norm = normalize_address(addr2)
450
684
 
451
- if enhanced_tx.get('direction') in ['incoming', 'outgoing']:
452
- enhanced_tx['block_height'] = block.get('index')
453
- enhanced_tx['status'] = 'confirmed'
454
- enhanced_tx['tx_index'] = tx_index
455
- enhanced_tx['hash'] = tx.get('hash') or f"tx_{block.get('index')}_{tx_index}"
456
-
457
- # Ensure amount and fee are floats
458
- enhanced_tx['amount'] = float(enhanced_tx.get('amount', 0))
459
- enhanced_tx['fee'] = float(enhanced_tx.get('fee', 0))
460
-
461
- # Calculate effective amount based on direction
462
- if enhanced_tx['direction'] == 'outgoing':
463
- # For outgoing: negative (amount + fee)
464
- enhanced_tx['effective_amount'] = -(enhanced_tx['amount'] + enhanced_tx['fee'])
465
- print(f" ⬇️ OUTGOING: {enhanced_tx['amount']} + {enhanced_tx['fee']} fee = {enhanced_tx['effective_amount']}")
466
- print(f" From: {enhanced_tx.get('from', 'unknown')}")
467
- print(f" To: {enhanced_tx.get('to', 'unknown')}")
468
- else:
469
- # For incoming: positive amount only
470
- enhanced_tx['effective_amount'] = enhanced_tx['amount']
471
- print(f" ⬆️ INCOMING: +{enhanced_tx['amount']}")
472
- print(f" From: {enhanced_tx.get('from', 'unknown')}")
473
- print(f" To: {enhanced_tx.get('to', 'unknown')}")
474
-
685
+ # Check if they match
686
+ return addr1_norm == addr2_norm
687
+
688
+ # ==================================================================
689
+ # A) REWARD TRANSACTIONS (explicit reward transactions)
690
+ # ==================================================================
691
+ tx_type = tx.get('type', 'transfer').lower()
692
+ if tx_type == 'reward':
693
+ reward_to_address = tx.get('to', '')
694
+ # Compare the reward's destination with our wallet address
695
+ if addresses_match(reward_to_address, address):
696
+ amount = float(tx.get('amount', 0))
697
+ enhanced_tx['direction'] = 'incoming'
698
+ enhanced_tx['effective_amount'] = amount
699
+ enhanced_tx['fee'] = 0
700
+ enhanced_tx.setdefault('from', 'network') # Ensure sender is set
475
701
  transactions.append(enhanced_tx)
476
- continue # Skip other parsing methods
702
+ print(f"✅ Found mining reward: {amount} LUN (to: {reward_to_address})")
703
+ continue # Move to next transaction
477
704
 
478
- # Method 1: Check for standard transfer format
479
- if 'type' in tx or 'from' in tx or 'to' in tx:
480
- from_addr = (tx.get('from') or '').lower()
481
- to_addr = (tx.get('to') or '').lower()
705
+ # ==================================================================
706
+ # B) REGULAR TRANSFERS
707
+ # ==================================================================
708
+ from_addr = tx.get('from') or tx.get('sender') or ''
709
+ to_addr = tx.get('to') or tx.get('receiver') or ''
710
+
711
+ # Check if transaction involves our address
712
+ is_incoming = addresses_match(to_addr, address)
713
+ is_outgoing = addresses_match(from_addr, address)
714
+
715
+ if is_incoming:
482
716
  amount = float(tx.get('amount', 0))
483
- fee = float(tx.get('fee', 0))
717
+ fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
484
718
 
485
- # Check if this transaction involves our address
486
- if from_addr == address_lower or to_addr == address_lower:
487
- enhanced_tx = tx.copy()
488
- enhanced_tx['block_height'] = block.get('index')
489
- enhanced_tx['status'] = 'confirmed'
490
- enhanced_tx['tx_index'] = tx_index
491
-
492
- # Ensure type is set
493
- if not enhanced_tx.get('type'):
494
- enhanced_tx['type'] = tx.get('bill_type', 'transfer')
495
-
496
- # Ensure amounts are floats
497
- enhanced_tx['amount'] = amount
498
- enhanced_tx['fee'] = fee
499
-
500
- # Add direction information
501
- if from_addr == address_lower:
502
- enhanced_tx['direction'] = 'outgoing'
503
- # For outgoing: negative (amount + fee)
504
- enhanced_tx['effective_amount'] = -(amount + fee)
505
- print(f" ⬇️ STANDARD OUTGOING from our address")
506
- print(f" From: {from_addr} (OURS)")
507
- print(f" To: {to_addr}")
508
- print(f" Amount: {amount}, Fee: {fee}, Total: {enhanced_tx['effective_amount']}")
509
- else:
510
- enhanced_tx['direction'] = 'incoming'
511
- enhanced_tx['effective_amount'] = amount
512
- print(f" ⬆️ STANDARD INCOMING to our address")
513
- print(f" From: {from_addr}")
514
- print(f" To: {to_addr} (OURS)")
515
- print(f" Amount: {amount}")
516
-
517
- transactions.append(enhanced_tx)
518
- continue
519
-
520
- # Method 2: Check for GTX/bill_type format
521
- if 'bill_type' in tx:
522
- bill_type = tx.get('bill_type', '')
719
+ enhanced_tx['direction'] = 'incoming'
720
+ enhanced_tx['effective_amount'] = amount
721
+ enhanced_tx['amount'] = amount
722
+ enhanced_tx['fee'] = fee
723
+
724
+ transactions.append(enhanced_tx)
725
+ print(f"⬆️ Found incoming transaction: {amount} LUN")
726
+
727
+ elif is_outgoing:
523
728
  amount = float(tx.get('amount', 0))
524
- fee = float(tx.get('fee', 0))
729
+ fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
525
730
 
526
- # Try to find address fields
527
- address_fields = ['from_address', 'from', 'sender', 'to_address', 'to', 'receiver', 'owner']
731
+ enhanced_tx['direction'] = 'outgoing'
732
+ enhanced_tx['effective_amount'] = -(amount + fee)
733
+ enhanced_tx['amount'] = amount
734
+ enhanced_tx['fee'] = fee
528
735
 
529
- for field in address_fields:
530
- if field in tx:
531
- tx_addr = (tx.get(field) or '').lower()
532
- if tx_addr == address_lower:
533
- enhanced_tx = tx.copy()
534
- enhanced_tx['block_height'] = block.get('index')
535
- enhanced_tx['status'] = 'confirmed'
536
- enhanced_tx['tx_index'] = tx_index
537
- enhanced_tx['type'] = bill_type.lower()
538
-
539
- # Map bill_type to standard type
540
- if 'gtx_genesis' in bill_type.lower():
541
- enhanced_tx['type'] = 'gtx_genesis'
542
- enhanced_tx['description'] = 'GTX Genesis Transaction'
543
-
544
- enhanced_tx['amount'] = amount
545
- enhanced_tx['fee'] = fee
546
-
547
- # Determine direction
548
- if field in ['from_address', 'from', 'sender']:
549
- enhanced_tx['direction'] = 'outgoing'
550
- enhanced_tx['effective_amount'] = -(amount + fee)
551
- enhanced_tx['from'] = tx_addr
552
- enhanced_tx['to'] = tx.get('to_address') or tx.get('receiver') or tx.get('to') or 'unknown'
553
- print(f" ⬇️ GTX OUTGOING ({bill_type})")
554
- print(f" From: {tx_addr} (OURS)")
555
- print(f" Amount: {amount}, Fee: {fee}, Total: {enhanced_tx['effective_amount']}")
556
- else:
557
- enhanced_tx['direction'] = 'incoming'
558
- enhanced_tx['effective_amount'] = amount
559
- enhanced_tx['from'] = tx.get('from_address') or tx.get('sender') or tx.get('from') or 'network'
560
- enhanced_tx['to'] = tx_addr
561
- print(f" ⬆️ GTX INCOMING ({bill_type})")
562
- print(f" To: {tx_addr} (OURS)")
563
- print(f" Amount: {amount}")
564
-
565
- transactions.append(enhanced_tx)
566
- break
567
-
568
- print(f"\n ✅ Total transactions found for address: {len(transactions)}")
569
-
570
- # Debug: Show transaction types found
571
- incoming = [t for t in transactions if t.get('direction') == 'incoming']
572
- outgoing = [t for t in transactions if t.get('direction') == 'outgoing']
573
- print(f" Incoming: {len(incoming)}, Outgoing: {len(outgoing)}")
736
+ transactions.append(enhanced_tx)
737
+ print(f"⬇️ Found outgoing transaction: {amount} LUN + {fee} fee")
574
738
 
739
+ print(f"📊 Scan complete for block #{block.get('index')}: {len(transactions)} transactions found")
575
740
  return transactions
576
741
  def _handle_regular_transfers(self, tx: Dict, address_lower: str) -> Dict:
577
742
  """Handle regular transfer transactions that might be in different formats"""