lunalib 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lunalib might be problematic. Click here for more details.

Files changed (53) hide show
  1. core/__init__.py +0 -0
  2. core/blockchain.py +172 -0
  3. core/crypto.py +32 -0
  4. core/wallet.py +408 -0
  5. gtx/__init__.py +0 -0
  6. gtx/bill_registry.py +122 -0
  7. gtx/digital_bill.py +273 -0
  8. gtx/genesis.py +338 -0
  9. lunalib/__init__.py +21 -0
  10. lunalib/cli.py +18 -0
  11. lunalib/core/__init__.py +0 -0
  12. lunalib/core/blockchain.py +803 -0
  13. lunalib/core/crypto.py +270 -0
  14. lunalib/core/mempool.py +342 -0
  15. lunalib/core/sm2.py +723 -0
  16. lunalib/core/wallet.py +1342 -0
  17. lunalib/core/wallet_manager.py +638 -0
  18. lunalib/core/wallet_sync_helper.py +163 -0
  19. lunalib/gtx/__init__.py +0 -0
  20. lunalib/gtx/bill_registry.py +122 -0
  21. lunalib/gtx/digital_bill.py +273 -0
  22. lunalib/gtx/genesis.py +349 -0
  23. lunalib/luna_lib.py +87 -0
  24. lunalib/mining/__init__.py +0 -0
  25. lunalib/mining/cuda_manager.py +137 -0
  26. lunalib/mining/difficulty.py +106 -0
  27. lunalib/mining/miner.py +617 -0
  28. lunalib/requirements.txt +44 -0
  29. lunalib/storage/__init__.py +0 -0
  30. lunalib/storage/cache.py +148 -0
  31. lunalib/storage/database.py +222 -0
  32. lunalib/storage/encryption.py +105 -0
  33. lunalib/transactions/__init__.py +0 -0
  34. lunalib/transactions/security.py +234 -0
  35. lunalib/transactions/transactions.py +399 -0
  36. lunalib/transactions/validator.py +71 -0
  37. lunalib-1.5.1.dist-info/METADATA +283 -0
  38. lunalib-1.5.1.dist-info/RECORD +53 -0
  39. lunalib-1.5.1.dist-info/WHEEL +5 -0
  40. lunalib-1.5.1.dist-info/entry_points.txt +2 -0
  41. lunalib-1.5.1.dist-info/top_level.txt +6 -0
  42. mining/__init__.py +0 -0
  43. mining/cuda_manager.py +137 -0
  44. mining/difficulty.py +106 -0
  45. mining/miner.py +107 -0
  46. storage/__init__.py +0 -0
  47. storage/cache.py +148 -0
  48. storage/database.py +222 -0
  49. storage/encryption.py +105 -0
  50. transactions/__init__.py +0 -0
  51. transactions/security.py +172 -0
  52. transactions/transactions.py +424 -0
  53. transactions/validator.py +71 -0
@@ -0,0 +1,803 @@
1
+ # blockchain.py - Updated version
2
+
3
+ from ..storage.cache import BlockchainCache
4
+ import requests
5
+ import time
6
+ import json
7
+ from typing import Dict, List, Optional, Tuple
8
+
9
+
10
+ class BlockchainManager:
11
+ """Manages blockchain interactions and scanning with transaction broadcasting"""
12
+
13
+ def __init__(self, endpoint_url="https://bank.linglin.art"):
14
+ self.endpoint_url = endpoint_url.rstrip('/')
15
+ self.cache = BlockchainCache()
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
28
+
29
+ def broadcast_transaction(self, transaction: Dict) -> Tuple[bool, str]:
30
+ """Broadcast transaction to mempool with enhanced error handling"""
31
+ try:
32
+ print(f"🔄 Broadcasting transaction to {self.endpoint_url}/mempool/add")
33
+ print(f" Transaction type: {transaction.get('type', 'unknown')}")
34
+ print(f" From: {transaction.get('from', 'unknown')}")
35
+ print(f" To: {transaction.get('to', 'unknown')}")
36
+ print(f" Amount: {transaction.get('amount', 'unknown')}")
37
+
38
+ # Ensure transaction has required fields
39
+ if not self._validate_transaction_before_broadcast(transaction):
40
+ return False, "Transaction validation failed"
41
+
42
+ response = requests.post(
43
+ f'{self.endpoint_url}/mempool/add',
44
+ json=transaction,
45
+ headers={'Content-Type': 'application/json'},
46
+ timeout=30
47
+ )
48
+
49
+ print(f"📡 Broadcast response: HTTP {response.status_code}")
50
+
51
+ if response.status_code in [200, 201]:
52
+ result = response.json()
53
+ if result.get('success'):
54
+ tx_hash = result.get('transaction_hash', transaction.get('hash', 'unknown'))
55
+ print(f"✅ Transaction broadcast successful! Hash: {tx_hash}")
56
+ return True, f"Transaction broadcast successfully: {tx_hash}"
57
+ else:
58
+ error_msg = result.get('error', 'Unknown error from server')
59
+ print(f"❌ Broadcast failed: {error_msg}")
60
+ return False, f"Server rejected transaction: {error_msg}"
61
+ else:
62
+ error_msg = f"HTTP {response.status_code}: {response.text}"
63
+ print(f"❌ Network error: {error_msg}")
64
+ return False, error_msg
65
+
66
+ except requests.exceptions.ConnectionError:
67
+ error_msg = "Cannot connect to blockchain server"
68
+ print(f"❌ {error_msg}")
69
+ return False, error_msg
70
+ except requests.exceptions.Timeout:
71
+ error_msg = "Broadcast request timed out"
72
+ print(f"❌ {error_msg}")
73
+ return False, error_msg
74
+ except Exception as e:
75
+ error_msg = f"Unexpected error: {str(e)}"
76
+ print(f"❌ {error_msg}")
77
+ return False, error_msg
78
+
79
+ def _validate_transaction_before_broadcast(self, transaction: Dict) -> bool:
80
+ """Validate transaction before broadcasting"""
81
+ required_fields = ['type', 'from', 'to', 'amount', 'timestamp', 'hash', 'signature']
82
+
83
+ for field in required_fields:
84
+ if field not in transaction:
85
+ print(f"❌ Missing required field: {field}")
86
+ return False
87
+
88
+ # Validate addresses
89
+ if not transaction['from'].startswith('LUN_'):
90
+ print(f"❌ Invalid from address format: {transaction['from']}")
91
+ return False
92
+
93
+ if not transaction['to'].startswith('LUN_'):
94
+ print(f"❌ Invalid to address format: {transaction['to']}")
95
+ return False
96
+
97
+ # Validate amount
98
+ try:
99
+ amount = float(transaction['amount'])
100
+ if amount <= 0:
101
+ print(f"❌ Invalid amount: {amount}")
102
+ return False
103
+ except (ValueError, TypeError):
104
+ print(f"❌ Invalid amount format: {transaction['amount']}")
105
+ return False
106
+
107
+ # Validate signature
108
+ if not transaction.get('signature') or len(transaction['signature']) < 10:
109
+ print(f"❌ Invalid or missing signature")
110
+ return False
111
+
112
+ # Validate hash
113
+ if not transaction.get('hash') or len(transaction['hash']) < 10:
114
+ print(f"❌ Invalid or missing transaction hash")
115
+ return False
116
+
117
+ print("✅ Transaction validation passed")
118
+ return True
119
+
120
+ def get_transaction_status(self, tx_hash: str) -> Dict:
121
+ """Check transaction status (pending/confirmed)"""
122
+ try:
123
+ # First check mempool for pending transactions
124
+ mempool_txs = self.get_mempool()
125
+ for tx in mempool_txs:
126
+ if tx.get('hash') == tx_hash:
127
+ return {
128
+ 'status': 'pending',
129
+ 'message': 'Transaction is in mempool waiting to be mined',
130
+ 'confirmations': 0
131
+ }
132
+
133
+ # Then check blockchain for confirmed transactions
134
+ current_height = self.get_blockchain_height()
135
+ for height in range(max(0, current_height - 100), current_height + 1):
136
+ block = self.get_block(height)
137
+ if block:
138
+ for tx in block.get('transactions', []):
139
+ if tx.get('hash') == tx_hash:
140
+ confirmations = current_height - height + 1
141
+ return {
142
+ 'status': 'confirmed',
143
+ 'message': f'Transaction confirmed in block {height}',
144
+ 'confirmations': confirmations,
145
+ 'block_height': height
146
+ }
147
+
148
+ return {
149
+ 'status': 'unknown',
150
+ 'message': 'Transaction not found in mempool or recent blocks'
151
+ }
152
+
153
+ except Exception as e:
154
+ return {
155
+ 'status': 'error',
156
+ 'message': f'Error checking transaction status: {str(e)}'
157
+ }
158
+
159
+ def get_blockchain_height(self) -> int:
160
+ """Get current blockchain height - FIXED VERSION"""
161
+ try:
162
+ # Get the actual latest block to determine height
163
+ response = requests.get(f'{self.endpoint_url}/blockchain/blocks', timeout=10)
164
+ if response.status_code == 200:
165
+ data = response.json()
166
+ blocks = data.get('blocks', [])
167
+
168
+ if blocks:
169
+ # The height is the index of the latest block
170
+ latest_block = blocks[-1]
171
+ latest_index = latest_block.get('index', len(blocks) - 1)
172
+ print(f"🔍 Server has {len(blocks)} blocks, latest index: {latest_index}")
173
+ print(f"🔍 Latest block hash: {latest_block.get('hash', '')[:32]}...")
174
+ return latest_index
175
+ return 0
176
+
177
+ except Exception as e:
178
+ print(f"Blockchain height error: {e}")
179
+
180
+ return 0
181
+
182
+ def get_latest_block(self) -> Optional[Dict]:
183
+ """Get the actual latest block from server"""
184
+ try:
185
+ response = requests.get(f'{self.endpoint_url}/blockchain/blocks', timeout=10)
186
+ if response.status_code == 200:
187
+ data = response.json()
188
+ blocks = data.get('blocks', [])
189
+ if blocks:
190
+ return blocks[-1]
191
+ except Exception as e:
192
+ print(f"Get latest block error: {e}")
193
+ return None
194
+
195
+ def get_block(self, height: int) -> Optional[Dict]:
196
+ """Get block by height"""
197
+ # Check cache first
198
+ cached_block = self.cache.get_block(height)
199
+ if cached_block:
200
+ return cached_block
201
+
202
+ try:
203
+ response = requests.get(f'{self.endpoint_url}/blockchain/block/{height}', timeout=10)
204
+ if response.status_code == 200:
205
+ block = response.json()
206
+ self.cache.save_block(height, block.get('hash', ''), block)
207
+ return block
208
+ except Exception as e:
209
+ print(f"Get block error: {e}")
210
+
211
+ return None
212
+
213
+ def get_blocks_range(self, start_height: int, end_height: int) -> List[Dict]:
214
+ """Get range of blocks"""
215
+ blocks = []
216
+
217
+ # Check cache first
218
+ cached_blocks = self.cache.get_block_range(start_height, end_height)
219
+ if len(cached_blocks) == (end_height - start_height + 1):
220
+ return cached_blocks
221
+
222
+ try:
223
+ response = requests.get(
224
+ f'{self.endpoint_url}/blockchain/range?start={start_height}&end={end_height}',
225
+ timeout=30
226
+ )
227
+ if response.status_code == 200:
228
+ blocks = response.json().get('blocks', [])
229
+ # Cache the blocks
230
+ for block in blocks:
231
+ height = block.get('index', 0)
232
+ self.cache.save_block(height, block.get('hash', ''), block)
233
+ else:
234
+ # Fallback: get blocks individually
235
+ for height in range(start_height, end_height + 1):
236
+ block = self.get_block(height)
237
+ if block:
238
+ blocks.append(block)
239
+ time.sleep(0.01) # Be nice to the API
240
+
241
+ except Exception as e:
242
+ print(f"Get blocks range error: {e}")
243
+
244
+ return blocks
245
+
246
+ def get_mempool(self) -> List[Dict]:
247
+ """Get current mempool transactions"""
248
+ try:
249
+ response = requests.get(f'{self.endpoint_url}/mempool', timeout=10)
250
+ if response.status_code == 200:
251
+ return response.json()
252
+ except Exception as e:
253
+ print(f"Mempool error: {e}")
254
+
255
+ return []
256
+
257
+ def check_network_connection(self) -> bool:
258
+ """Check if network is accessible"""
259
+ try:
260
+ response = requests.get(f'{self.endpoint_url}/system/health', timeout=5)
261
+ self.network_connected = response.status_code == 200
262
+ return self.network_connected
263
+ except:
264
+ self.network_connected = False
265
+ return False
266
+
267
+ def scan_transactions_for_address(self, address: str, start_height: int = 0, end_height: int = None) -> List[Dict]:
268
+ """Scan blockchain for transactions involving an address"""
269
+ if end_height is None:
270
+ end_height = self.get_blockchain_height()
271
+
272
+ print(f"[SCAN] Scanning transactions for {address} from block {start_height} to {end_height}")
273
+
274
+ transactions = []
275
+
276
+ # Scan in batches for efficiency
277
+ batch_size = 100
278
+ for batch_start in range(start_height, end_height + 1, batch_size):
279
+ batch_end = min(batch_start + batch_size - 1, end_height)
280
+ print(f"[SCAN] Processing batch {batch_start}-{batch_end}...")
281
+ blocks = self.get_blocks_range(batch_start, batch_end)
282
+
283
+ for block in blocks:
284
+ block_transactions = self._find_address_transactions(block, address)
285
+ transactions.extend(block_transactions)
286
+
287
+ print(f"[SCAN] Found {len(transactions)} total transactions for {address}")
288
+ return transactions
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
+
382
+ def submit_mined_block(self, block_data: Dict) -> bool:
383
+ """Submit a mined block to the network with built-in validation"""
384
+ try:
385
+ print(f"🔄 Preparing to submit block #{block_data.get('index')}...")
386
+
387
+ # Step 1: Validate block structure before submission
388
+ validation_result = self._validate_block_structure(block_data)
389
+ if not validation_result['valid']:
390
+ print(f"❌ Block validation failed:")
391
+ for issue in validation_result['issues']:
392
+ print(f" - {issue}")
393
+ return False
394
+
395
+ print(f"✅ Block structure validation passed")
396
+ print(f" Block #{block_data.get('index')} | Hash: {block_data.get('hash', '')[:16]}...")
397
+ print(f" Transactions: {len(block_data.get('transactions', []))} | Difficulty: {block_data.get('difficulty')}")
398
+
399
+ # Step 2: Submit to the correct endpoint
400
+ response = requests.post(
401
+ f'{self.endpoint_url}/blockchain/submit-block',
402
+ json=block_data,
403
+ headers={'Content-Type': 'application/json'},
404
+ timeout=30
405
+ )
406
+
407
+ # Step 3: Handle response
408
+ if response.status_code in [200, 201]:
409
+ result = response.json()
410
+ if result.get('success'):
411
+ print(f"🎉 Block #{block_data.get('index')} successfully added to blockchain!")
412
+ print(f" Block hash: {result.get('block_hash', '')[:16]}...")
413
+ print(f" Transactions count: {result.get('transactions_count', 0)}")
414
+ print(f" Miner: {result.get('miner', 'unknown')}")
415
+ return True
416
+ else:
417
+ error_msg = result.get('error', 'Unknown error')
418
+ print(f"❌ Block submission rejected: {error_msg}")
419
+ return False
420
+ else:
421
+ print(f"❌ HTTP error {response.status_code}: {response.text}")
422
+ return False
423
+
424
+ except requests.exceptions.RequestException as e:
425
+ print(f"❌ Network error submitting block: {e}")
426
+ return False
427
+ except Exception as e:
428
+ print(f"💥 Unexpected error submitting block: {e}")
429
+ return False
430
+
431
+ def _validate_block_structure(self, block_data: Dict) -> Dict:
432
+ """Internal: Validate block structure before submission"""
433
+ issues = []
434
+
435
+ # Check required fields
436
+ required_fields = ["index", "previous_hash", "timestamp", "transactions", "miner", "difficulty", "nonce", "hash"]
437
+ missing_fields = [field for field in required_fields if field not in block_data]
438
+ if missing_fields:
439
+ issues.append(f"Missing required fields: {missing_fields}")
440
+
441
+ # Check data types
442
+ if not isinstance(block_data.get('index'), int) or block_data.get('index') < 0:
443
+ issues.append("Index must be a non-negative integer")
444
+
445
+ if not isinstance(block_data.get('transactions'), list):
446
+ issues.append("Transactions must be a list")
447
+
448
+ if not isinstance(block_data.get('difficulty'), int) or block_data.get('difficulty') < 0:
449
+ issues.append("Difficulty must be a non-negative integer")
450
+
451
+ if not isinstance(block_data.get('nonce'), int) or block_data.get('nonce') < 0:
452
+ issues.append("Nonce must be a non-negative integer")
453
+
454
+ # Check hash meets difficulty requirement
455
+ block_hash = block_data.get('hash', '')
456
+ difficulty = block_data.get('difficulty', 0)
457
+ if difficulty > 0 and not block_hash.startswith('0' * difficulty):
458
+ issues.append(f"Hash doesn't meet difficulty {difficulty}: {block_hash[:16]}...")
459
+
460
+ # Check hash length (should be 64 chars for SHA-256)
461
+ if len(block_hash) != 64:
462
+ issues.append(f"Hash should be 64 characters, got {len(block_hash)}")
463
+
464
+ # Check previous hash format
465
+ previous_hash = block_data.get('previous_hash', '')
466
+ if len(previous_hash) != 64 and previous_hash != '0' * 64: # Allow genesis block
467
+ issues.append(f"Previous hash should be 64 characters, got {len(previous_hash)}")
468
+
469
+ # Check timestamp is reasonable
470
+ current_time = time.time()
471
+ block_time = block_data.get('timestamp', 0)
472
+ if block_time > current_time + 300: # 5 minutes in future
473
+ issues.append(f"Block timestamp is in the future")
474
+ if block_time < current_time - 86400: # 24 hours in past
475
+ issues.append(f"Block timestamp is too far in the past")
476
+
477
+ # Validate transactions structure
478
+ transactions = block_data.get('transactions', [])
479
+ for i, tx in enumerate(transactions):
480
+ if not isinstance(tx, dict):
481
+ issues.append(f"Transaction {i} is not a dictionary")
482
+ continue
483
+
484
+ tx_type = tx.get('type')
485
+ if not tx_type:
486
+ issues.append(f"Transaction {i} missing 'type' field")
487
+
488
+ # Basic transaction validation
489
+ if tx_type == 'GTX_Genesis':
490
+ required_tx_fields = ['serial_number', 'denomination', 'issued_to', 'timestamp', 'hash']
491
+ missing_tx_fields = [field for field in required_tx_fields if field not in tx]
492
+ if missing_tx_fields:
493
+ issues.append(f"GTX_Genesis transaction {i} missing fields: {missing_tx_fields}")
494
+
495
+ elif tx_type == 'reward':
496
+ required_tx_fields = ['to', 'amount', 'timestamp', 'hash']
497
+ missing_tx_fields = [field for field in required_tx_fields if field not in tx]
498
+ if missing_tx_fields:
499
+ issues.append(f"Reward transaction {i} missing fields: {missing_tx_fields}")
500
+
501
+ return {
502
+ 'valid': len(issues) == 0,
503
+ 'issues': issues,
504
+ 'block_info': {
505
+ 'index': block_data.get('index'),
506
+ 'hash_preview': block_data.get('hash', '')[:16] + '...',
507
+ 'transaction_count': len(transactions),
508
+ 'difficulty': block_data.get('difficulty'),
509
+ 'miner': block_data.get('miner'),
510
+ 'nonce': block_data.get('nonce')
511
+ }
512
+ }
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
+
600
+ def _find_address_transactions(self, block: Dict, address: str) -> List[Dict]:
601
+ """Find transactions in block that involve the address - FIXED REWARD DETECTION"""
602
+ transactions = []
603
+ address_lower = address.lower().strip('"\'') # Remove quotes if present
604
+
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}'")
660
+
661
+ # ==================================================================
662
+ # 2. CHECK ALL TRANSACTIONS IN THE BLOCK
663
+ # ==================================================================
664
+ block_transactions = block.get('transactions', [])
665
+ print(f" Block has {len(block_transactions)} transactions")
666
+
667
+ for tx_index, tx in enumerate(block_transactions):
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
672
+
673
+ # Get transaction type
674
+ tx_type = tx.get('type', 'transfer').lower()
675
+
676
+ # Helper function for address matching with normalization
677
+ def addresses_match(addr1, addr2):
678
+ if not addr1 or not addr2:
679
+ return False
680
+
681
+ # Normalize both addresses
682
+ addr1_norm = normalize_address(addr1)
683
+ addr2_norm = normalize_address(addr2)
684
+
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
701
+ transactions.append(enhanced_tx)
702
+ print(f"✅ Found mining reward: {amount} LUN (to: {reward_to_address})")
703
+ continue # Move to next transaction
704
+
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:
716
+ amount = float(tx.get('amount', 0))
717
+ fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
718
+
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:
728
+ amount = float(tx.get('amount', 0))
729
+ fee = float(tx.get('fee', 0) or tx.get('gas', 0) or 0)
730
+
731
+ enhanced_tx['direction'] = 'outgoing'
732
+ enhanced_tx['effective_amount'] = -(amount + fee)
733
+ enhanced_tx['amount'] = amount
734
+ enhanced_tx['fee'] = fee
735
+
736
+ transactions.append(enhanced_tx)
737
+ print(f"⬇️ Found outgoing transaction: {amount} LUN + {fee} fee")
738
+
739
+ print(f"📊 Scan complete for block #{block.get('index')}: {len(transactions)} transactions found")
740
+ return transactions
741
+ def _handle_regular_transfers(self, tx: Dict, address_lower: str) -> Dict:
742
+ """Handle regular transfer transactions that might be in different formats"""
743
+ enhanced_tx = tx.copy()
744
+
745
+ # Try to extract addresses from various possible field names
746
+ possible_from_fields = ['from', 'sender', 'from_address', 'source', 'payer']
747
+ possible_to_fields = ['to', 'receiver', 'to_address', 'destination', 'payee']
748
+ possible_amount_fields = ['amount', 'value', 'quantity', 'transfer_amount']
749
+ possible_fee_fields = ['fee', 'gas', 'transaction_fee', 'gas_fee']
750
+
751
+ # Find from address
752
+ from_addr = ''
753
+ for field in possible_from_fields:
754
+ if field in tx:
755
+ from_addr = (tx.get(field) or '').lower()
756
+ break
757
+
758
+ # Find to address
759
+ to_addr = ''
760
+ for field in possible_to_fields:
761
+ if field in tx:
762
+ to_addr = (tx.get(field) or '').lower()
763
+ break
764
+
765
+ # Find amount
766
+ amount = 0
767
+ for field in possible_amount_fields:
768
+ if field in tx:
769
+ amount = float(tx.get(field, 0))
770
+ break
771
+
772
+ # Find fee
773
+ fee = 0
774
+ for field in possible_fee_fields:
775
+ if field in tx:
776
+ fee = float(tx.get(field, 0))
777
+ break
778
+
779
+ # Set direction
780
+ if from_addr == address_lower:
781
+ enhanced_tx['direction'] = 'outgoing'
782
+ enhanced_tx['effective_amount'] = -(amount + fee)
783
+ enhanced_tx['from'] = from_addr
784
+ enhanced_tx['to'] = to_addr
785
+ enhanced_tx['amount'] = amount
786
+ enhanced_tx['fee'] = fee
787
+ elif to_addr == address_lower:
788
+ enhanced_tx['direction'] = 'incoming'
789
+ enhanced_tx['effective_amount'] = amount
790
+ enhanced_tx['from'] = from_addr
791
+ enhanced_tx['to'] = to_addr
792
+ enhanced_tx['amount'] = amount
793
+ enhanced_tx['fee'] = fee
794
+ else:
795
+ # If we can't determine direction from addresses, check other fields
796
+ enhanced_tx['direction'] = 'unknown'
797
+ enhanced_tx['effective_amount'] = amount
798
+
799
+ # Set type if not present
800
+ if not enhanced_tx.get('type'):
801
+ enhanced_tx['type'] = 'transfer'
802
+
803
+ return enhanced_tx