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.
- core/__init__.py +0 -0
- core/blockchain.py +172 -0
- core/crypto.py +32 -0
- core/wallet.py +408 -0
- gtx/__init__.py +0 -0
- gtx/bill_registry.py +122 -0
- gtx/digital_bill.py +273 -0
- gtx/genesis.py +338 -0
- lunalib/__init__.py +21 -0
- lunalib/cli.py +18 -0
- lunalib/core/__init__.py +0 -0
- lunalib/core/blockchain.py +803 -0
- lunalib/core/crypto.py +270 -0
- lunalib/core/mempool.py +342 -0
- lunalib/core/sm2.py +723 -0
- lunalib/core/wallet.py +1342 -0
- lunalib/core/wallet_manager.py +638 -0
- lunalib/core/wallet_sync_helper.py +163 -0
- lunalib/gtx/__init__.py +0 -0
- lunalib/gtx/bill_registry.py +122 -0
- lunalib/gtx/digital_bill.py +273 -0
- lunalib/gtx/genesis.py +349 -0
- lunalib/luna_lib.py +87 -0
- lunalib/mining/__init__.py +0 -0
- lunalib/mining/cuda_manager.py +137 -0
- lunalib/mining/difficulty.py +106 -0
- lunalib/mining/miner.py +617 -0
- lunalib/requirements.txt +44 -0
- lunalib/storage/__init__.py +0 -0
- lunalib/storage/cache.py +148 -0
- lunalib/storage/database.py +222 -0
- lunalib/storage/encryption.py +105 -0
- lunalib/transactions/__init__.py +0 -0
- lunalib/transactions/security.py +234 -0
- lunalib/transactions/transactions.py +399 -0
- lunalib/transactions/validator.py +71 -0
- lunalib-1.5.1.dist-info/METADATA +283 -0
- lunalib-1.5.1.dist-info/RECORD +53 -0
- lunalib-1.5.1.dist-info/WHEEL +5 -0
- lunalib-1.5.1.dist-info/entry_points.txt +2 -0
- lunalib-1.5.1.dist-info/top_level.txt +6 -0
- mining/__init__.py +0 -0
- mining/cuda_manager.py +137 -0
- mining/difficulty.py +106 -0
- mining/miner.py +107 -0
- storage/__init__.py +0 -0
- storage/cache.py +148 -0
- storage/database.py +222 -0
- storage/encryption.py +105 -0
- transactions/__init__.py +0 -0
- transactions/security.py +172 -0
- transactions/transactions.py +424 -0
- 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
|