Riorganizza i file e rinomina main.py in scan_blockchain.py

Aggiunge un piccolo db per test
This commit is contained in:
2026-01-23 07:57:53 +01:00
parent 275b7d4c5f
commit 48f8b31e13
4 changed files with 21 additions and 0 deletions

Binary file not shown.

21
databases/p2pk_data.csv Normal file
View File

@@ -0,0 +1,21 @@
Block,TXID,Output Index,ScriptPubKey,Value (satoshi),Timestamp
1,0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098,0,410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac,5000000000,1231469665
2,9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5,0,41047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac,5000000000,1231469744
3,999e1c837c76a1b7fbb7e57baf87b309960f5ffefbf2a9b95dd890602272f644,0,410494b9d3e76c5b1629ecf97fff95d7a4bbdac87cc26099ada28066c6ff1eb9191223cd897194a08d0c2726c5747f1db49e8cf90e75dc3e3550ae9b30086f3cd5aaac,5000000000,1231470173
4,df2b060fa2e5e9c8ed5eaf6a45c13753ec8c63282b2688322eba40cd98ea067a,0,4104184f32b212815c6e522e66686324030ff7e5bf08efb21f8b00614fb7690e19131dd31304c54f37baa40db231c918106bb9fd43373e37ae31a0befc6ecaefb867ac,5000000000,1231470988
5,63522845d294ee9b0188ae5cac91bf389a0c3723f084ca1025e7d9cdfe481ce1,0,410456579536d150fbce94ee62b47db2ca43af0a730a0467ba55c79e2a7ec9ce4ad297e35cdbb8e42a4643a60eef7c9abee2f5822f86b1da242d9c2301c431facfd8ac,5000000000,1231471428
6,20251a76e64e920e58291a30d4b212939aae976baca40e70818ceaa596fb9d37,0,410408ce279174b34c077c7b2043e3f3d45a588b85ef4ca466740f848ead7fb498f0a795c982552fdfa41616a7c0333a269d62108588e260fd5a48ac8e4dbf49e2bcac,5000000000,1231471789
7,8aa673bc752f2851fd645d6a0a92917e967083007d9c1684f9423b100540673f,0,4104a59e64c774923d003fae7491b2a7f75d6b7aa3f35606a8ff1cf06cd3317d16a41aa16928b1df1f631f31f28c7da35d4edad3603adb2338c4d4dd268f31530555ac,5000000000,1231472369
8,a6f7f1c0dad0f2eb6b13c4f33de664b1b0e9f22efad5994a6d5b6086d85e85e3,0,4104cc8d85f5e7933cb18f13b97d165e1189c1fb3e9c98b0dd5446b2a1989883ff9e740a8a75da99cc59a21016caf7a7afd3e4e9e7952983e18d1ff70529d62e0ba1ac,5000000000,1231472743
9,0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9,0,410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac,5000000000,1231473279
10,d3ad39fa52a89997ac7381c95eeffeaf40b66af7a57e9eba144be0a175a12b11,0,4104fcc2888ca91cf0103d8c5797c256bf976e81f280205d002d85b9b622ed1a6f820866c7b5fe12285cfa78c035355d752fc94a398b67597dc4fbb5b386816425ddac,5000000000,1231473952
11,f8325d8f7fa5d658ea143629288d0530d2710dc9193ddc067439de803c37066e,0,41046cc86ddcd0860b7cef16cbaad7fe31fda1bf073c25cb833fa9e409e7f51e296f39b653a9c8040a2f967319ff37cf14b0991b86173462a2d5907cb6c5648b5b76ac,5000000000,1231474360
12,3b96bb7e197ef276b85131afd4a09c059cc368133a26ca04ebffb0ab4f75c8b8,0,410478ebe2c28660cd2fa1ba17cc04e58d6312679005a7cad1fd56a7b7f4630bd700bcdb84a888a43fe1a2738ea1f3d2301d02faef357e8a5c35a706e4ae0352a6adac,5000000000,1231474888
13,9962d5c704ec27243364cbe9d384808feeac1c15c35ac790dffd1e929829b271,0,4104c5a68f5fa2192b215016c5dfb384399a39474165eea22603cd39780e653baad9106e36947a1ba3ad5d3789c5cead18a38a538a7d834a8a2b9f0ea946fb4e6f68ac,5000000000,1231475020
14,e1afd89295b68bc5247fe0ca2885dd4b8818d7ce430faa615067d7bab8640156,0,41043e8ac6b8ea64e85928b6469f17db0096de0bcae7d09a4497413d9bba49c00ffdf9cb0ce07c404784928b3976f0beea42fe2691a8f0430bcb2b0daaf5aa02b30eac,5000000000,1231475589
15,50748b7a193a0b23f1e9494b51131d2f954cc6cf4792bacc69d207d16002080d,0,4104e0041b4b4d9b6feb7221803a35d997efada6e2b5d24f5fc7205f2ea6b62a1adc9983a7a7dab7e93ea791bed5928e7a32286fa4facadd16313b75b467aea77499ac,5000000000,1231562746
16,e79fc1dad370e628614702f048edc8e98829cf8ea8f6615db19f992b1be92e44,0,4104977367164ca24f1f2de2e2cfb9e5c3f22d510d3f33683de200283100af0c8667dba7e4e389fa9953c6cb83d6ea72990e139f529b58cfbbac27607a28207b2a37ac,5000000000,1231562758
17,a3e0b7558e67f5cadd4a3166912cbf6f930044124358ef3a9afd885ac391625d,0,41045ca3b93e90fe9785734e07c8e564fd72a0d68a200bf907ee01dabab784ad5817f59a41f4f7e04edc3e9b80cc370c281b0f406eb58187664bdf93decc5bb63264ac,5000000000,1231563791
18,f925f26deb2dc4696be8782ab7ad9493d04721b28ee69a09d7dfca51b863ca23,0,4104cf37a46b304e4dad17e081361502d0eff20af2b1360c7b18392a29f9f08ae5a95aa24f859533dabbc8585598bf8c5c71c0e8d89d3655889aee8c49fd948f59feac,5000000000,1231564334
19,9b9e461221e5284f3bfe5656efdc8c7cc633b2f1beef54a86316bf2ae3a3e230,0,4104f5efde0c2d30ab28e3dbe804c1a4aaf13066f9b198a4159c76f8f79b3b20caf99f7c979ed6c71481061277a6fc8666977c249da99960c97c8d8714fda9f0e883ac,5000000000,1231564974
20,ee1afca2d1130676503a6db5d6a77075b2bf71382cfdf99231f89717b5257b5b,0,410408ab2f56361f83064e4ce51acc291fb57c2cbcdb1d6562f6278c43a1406b548fd6cefc11bcc29eb620d5861cb9ed69dc39f2422f54b06a8af4f78c8276cfdc6bac,5000000000,1231565995
1 Block TXID Output Index ScriptPubKey Value (satoshi) Timestamp
2 1 0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098 0 410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac 5000000000 1231469665
3 2 9b0fc92260312ce44e74ef369f5c66bbb85848f2eddd5a7a1cde251e54ccfdd5 0 41047211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073dee6c89064984f03385237d92167c13e236446b417ab79a0fcae412ae3316b77ac 5000000000 1231469744
4 3 999e1c837c76a1b7fbb7e57baf87b309960f5ffefbf2a9b95dd890602272f644 0 410494b9d3e76c5b1629ecf97fff95d7a4bbdac87cc26099ada28066c6ff1eb9191223cd897194a08d0c2726c5747f1db49e8cf90e75dc3e3550ae9b30086f3cd5aaac 5000000000 1231470173
5 4 df2b060fa2e5e9c8ed5eaf6a45c13753ec8c63282b2688322eba40cd98ea067a 0 4104184f32b212815c6e522e66686324030ff7e5bf08efb21f8b00614fb7690e19131dd31304c54f37baa40db231c918106bb9fd43373e37ae31a0befc6ecaefb867ac 5000000000 1231470988
6 5 63522845d294ee9b0188ae5cac91bf389a0c3723f084ca1025e7d9cdfe481ce1 0 410456579536d150fbce94ee62b47db2ca43af0a730a0467ba55c79e2a7ec9ce4ad297e35cdbb8e42a4643a60eef7c9abee2f5822f86b1da242d9c2301c431facfd8ac 5000000000 1231471428
7 6 20251a76e64e920e58291a30d4b212939aae976baca40e70818ceaa596fb9d37 0 410408ce279174b34c077c7b2043e3f3d45a588b85ef4ca466740f848ead7fb498f0a795c982552fdfa41616a7c0333a269d62108588e260fd5a48ac8e4dbf49e2bcac 5000000000 1231471789
8 7 8aa673bc752f2851fd645d6a0a92917e967083007d9c1684f9423b100540673f 0 4104a59e64c774923d003fae7491b2a7f75d6b7aa3f35606a8ff1cf06cd3317d16a41aa16928b1df1f631f31f28c7da35d4edad3603adb2338c4d4dd268f31530555ac 5000000000 1231472369
9 8 a6f7f1c0dad0f2eb6b13c4f33de664b1b0e9f22efad5994a6d5b6086d85e85e3 0 4104cc8d85f5e7933cb18f13b97d165e1189c1fb3e9c98b0dd5446b2a1989883ff9e740a8a75da99cc59a21016caf7a7afd3e4e9e7952983e18d1ff70529d62e0ba1ac 5000000000 1231472743
10 9 0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9 0 410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac 5000000000 1231473279
11 10 d3ad39fa52a89997ac7381c95eeffeaf40b66af7a57e9eba144be0a175a12b11 0 4104fcc2888ca91cf0103d8c5797c256bf976e81f280205d002d85b9b622ed1a6f820866c7b5fe12285cfa78c035355d752fc94a398b67597dc4fbb5b386816425ddac 5000000000 1231473952
12 11 f8325d8f7fa5d658ea143629288d0530d2710dc9193ddc067439de803c37066e 0 41046cc86ddcd0860b7cef16cbaad7fe31fda1bf073c25cb833fa9e409e7f51e296f39b653a9c8040a2f967319ff37cf14b0991b86173462a2d5907cb6c5648b5b76ac 5000000000 1231474360
13 12 3b96bb7e197ef276b85131afd4a09c059cc368133a26ca04ebffb0ab4f75c8b8 0 410478ebe2c28660cd2fa1ba17cc04e58d6312679005a7cad1fd56a7b7f4630bd700bcdb84a888a43fe1a2738ea1f3d2301d02faef357e8a5c35a706e4ae0352a6adac 5000000000 1231474888
14 13 9962d5c704ec27243364cbe9d384808feeac1c15c35ac790dffd1e929829b271 0 4104c5a68f5fa2192b215016c5dfb384399a39474165eea22603cd39780e653baad9106e36947a1ba3ad5d3789c5cead18a38a538a7d834a8a2b9f0ea946fb4e6f68ac 5000000000 1231475020
15 14 e1afd89295b68bc5247fe0ca2885dd4b8818d7ce430faa615067d7bab8640156 0 41043e8ac6b8ea64e85928b6469f17db0096de0bcae7d09a4497413d9bba49c00ffdf9cb0ce07c404784928b3976f0beea42fe2691a8f0430bcb2b0daaf5aa02b30eac 5000000000 1231475589
16 15 50748b7a193a0b23f1e9494b51131d2f954cc6cf4792bacc69d207d16002080d 0 4104e0041b4b4d9b6feb7221803a35d997efada6e2b5d24f5fc7205f2ea6b62a1adc9983a7a7dab7e93ea791bed5928e7a32286fa4facadd16313b75b467aea77499ac 5000000000 1231562746
17 16 e79fc1dad370e628614702f048edc8e98829cf8ea8f6615db19f992b1be92e44 0 4104977367164ca24f1f2de2e2cfb9e5c3f22d510d3f33683de200283100af0c8667dba7e4e389fa9953c6cb83d6ea72990e139f529b58cfbbac27607a28207b2a37ac 5000000000 1231562758
18 17 a3e0b7558e67f5cadd4a3166912cbf6f930044124358ef3a9afd885ac391625d 0 41045ca3b93e90fe9785734e07c8e564fd72a0d68a200bf907ee01dabab784ad5817f59a41f4f7e04edc3e9b80cc370c281b0f406eb58187664bdf93decc5bb63264ac 5000000000 1231563791
19 18 f925f26deb2dc4696be8782ab7ad9493d04721b28ee69a09d7dfca51b863ca23 0 4104cf37a46b304e4dad17e081361502d0eff20af2b1360c7b18392a29f9f08ae5a95aa24f859533dabbc8585598bf8c5c71c0e8d89d3655889aee8c49fd948f59feac 5000000000 1231564334
20 19 9b9e461221e5284f3bfe5656efdc8c7cc633b2f1beef54a86316bf2ae3a3e230 0 4104f5efde0c2d30ab28e3dbe804c1a4aaf13066f9b198a4159c76f8f79b3b20caf99f7c979ed6c71481061277a6fc8666977c249da99960c97c8d8714fda9f0e883ac 5000000000 1231564974
21 20 ee1afca2d1130676503a6db5d6a77075b2bf71382cfdf99231f89717b5257b5b 0 410408ab2f56361f83064e4ce51acc291fb57c2cbcdb1d6562f6278c43a1406b548fd6cefc11bcc29eb620d5861cb9ed69dc39f2422f54b06a8af4f78c8276cfdc6bac 5000000000 1231565995

View File

@@ -0,0 +1,466 @@
import requests
import sqlite3
import time
import json
from typing import Optional, List, Dict
class P2PKBlockchainScanner:
def __init__(self, db_path: str = "bitcoin_p2pk.db"):
"""Inizializza lo scanner con connessione al database"""
self.db_path = db_path
self.api_base = "https://mempool.space/api"
self.init_database()
def init_database(self):
"""Crea il database SQLite per memorizzare i P2PK"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS p2pk_addresses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
block_height INTEGER NOT NULL,
txid TEXT NOT NULL,
output_index INTEGER NOT NULL,
scriptpubkey TEXT NOT NULL,
value_satoshi INTEGER,
timestamp INTEGER,
is_unspent INTEGER DEFAULT 0,
last_checked INTEGER DEFAULT 0,
UNIQUE(txid, output_index)
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS scan_progress (
id INTEGER PRIMARY KEY CHECK (id = 1),
last_scanned_block INTEGER DEFAULT 0,
total_p2pk_found INTEGER DEFAULT 0
)
''')
# Inizializza il progresso se non esiste
cursor.execute('INSERT OR IGNORE INTO scan_progress (id, last_scanned_block) VALUES (1, 0)')
conn.commit()
conn.close()
def get_block_hash(self, height: int) -> Optional[str]:
"""Ottieni l'hash del blocco data l'altezza"""
try:
response = requests.get(f"{self.api_base}/block-height/{height}", timeout=10)
if response.status_code == 200:
return response.text.strip()
except Exception as e:
print(f"Errore ottenendo hash per blocco {height}: {e}")
return None
def get_block_transactions(self, block_hash: str) -> Optional[List[Dict]]:
"""Ottieni le transazioni di un blocco con paginazione"""
try:
all_transactions = []
start_index = 0
# L'API restituisce massimo 25 transazioni per volta
while True:
url = f"{self.api_base}/block/{block_hash}/txs/{start_index}"
response = requests.get(url, timeout=30)
if response.status_code != 200:
break
txs = response.json()
if not txs or not isinstance(txs, list) or len(txs) == 0:
break
all_transactions.extend(txs)
# Se riceviamo meno di 25 transazioni, siamo alla fine
if len(txs) < 25:
break
start_index += 25
time.sleep(0.1) # Piccola pausa tra le richieste paginate
return all_transactions if all_transactions else None
except Exception as e:
print(f" ⚠️ Errore ottenendo transazioni per blocco {block_hash}: {e}")
return None
def extract_p2pk_from_transaction(self, tx: Dict) -> List[Dict]:
"""Estrai output P2PK da una transazione"""
p2pk_outputs = []
for idx, output in enumerate(tx.get('vout', [])):
scriptpubkey_type = output.get('scriptpubkey_type', '')
scriptpubkey = output.get('scriptpubkey', '')
scriptpubkey_asm = output.get('scriptpubkey_asm', '')
# Metodo 1: Controlla il tipo esplicito
is_p2pk_type = scriptpubkey_type in ['pubkey', 'p2pk']
# Metodo 2: Controlla la lunghezza dello script (P2PK = 67 o 35 byte)
# 67 byte = chiave pubblica non compressa (04 + 64 hex chars) + OP_CHECKSIG
# 35 byte = chiave pubblica compressa (02/03 + 32 hex chars) + OP_CHECKSIG
script_len = len(scriptpubkey) // 2 if scriptpubkey else 0
is_p2pk_length = script_len in [67, 35]
# Metodo 3: Controlla il pattern ASM (PUBLIC_KEY OP_CHECKSIG)
is_p2pk_asm = False
if scriptpubkey_asm:
parts = scriptpubkey_asm.split()
# P2PK ha esattamente 2 elementi: chiave pubblica + OP_CHECKSIG
if len(parts) == 2 and parts[1] in ['OP_CHECKSIG', 'CHECKSIG']:
# Verifica che il primo elemento sia una chiave pubblica valida
pubkey = parts[0]
# Chiave non compressa: 130 caratteri (65 byte)
# Chiave compressa: 66 caratteri (33 byte)
if len(pubkey) in [130, 66]:
is_p2pk_asm = True
# Metodo 4: Controlla il pattern hex dello scriptPubKey
is_p2pk_hex = False
if scriptpubkey:
# P2PK non compresso: 41 (65 byte pubkey) + pubkey + ac (OP_CHECKSIG)
# P2PK compresso: 21 (33 byte pubkey) + pubkey + ac (OP_CHECKSIG)
if scriptpubkey.endswith('ac'): # OP_CHECKSIG
if scriptpubkey.startswith('41') and len(scriptpubkey) == 134: # 67 byte * 2
is_p2pk_hex = True
elif scriptpubkey.startswith('21') and len(scriptpubkey) == 70: # 35 byte * 2
is_p2pk_hex = True
# Se uno dei metodi identifica P2PK
if is_p2pk_type or is_p2pk_length or is_p2pk_asm or is_p2pk_hex:
p2pk_data = {
'txid': tx['txid'],
'output_index': idx, # Usa l'indice dell'array
'scriptpubkey': scriptpubkey,
'value': output.get('value', 0),
'asm': scriptpubkey_asm
}
p2pk_outputs.append(p2pk_data)
return p2pk_outputs
def save_p2pk_data(self, block_height: int, p2pk_data: List[Dict], block_timestamp: int):
"""Salva i dati P2PK nel database e verifica lo stato UTXO"""
if not p2pk_data:
return
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
for data in p2pk_data:
try:
# Verifica se l'UTXO è ancora non speso
utxo_status = self.check_utxo_status(data['txid'], data['output_index'])
is_unspent = 1 if utxo_status['is_unspent'] else 0
current_time = int(time.time())
cursor.execute('''
INSERT OR REPLACE INTO p2pk_addresses
(block_height, txid, output_index, scriptpubkey, value_satoshi, timestamp, is_unspent, last_checked)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', (
block_height,
data['txid'],
data['output_index'],
data['scriptpubkey'],
data['value'], # Il valore è già in satoshi dall'API
block_timestamp,
is_unspent,
current_time
))
# Log dello stato
if is_unspent:
print(f" 💰 UTXO NON SPESO! Valore: {data['value']} sat")
except Exception as e:
print(f"Errore salvataggio dato: {e}")
# Aggiorna il progresso
cursor.execute('''
UPDATE scan_progress
SET last_scanned_block = ?,
total_p2pk_found = total_p2pk_found + ?
WHERE id = 1
''', (block_height, len(p2pk_data)))
conn.commit()
conn.close()
def get_block_timestamp(self, block_hash: str) -> Optional[int]:
"""Ottieni timestamp del blocco"""
try:
response = requests.get(f"{self.api_base}/block/{block_hash}", timeout=10)
if response.status_code == 200:
block_data = response.json()
return block_data.get('timestamp', 0)
except Exception:
pass
return None
def check_utxo_status(self, txid: str, vout: int) -> Dict:
"""Verifica se l'UTXO è ancora non speso (ha saldo)"""
try:
response = requests.get(f"{self.api_base}/tx/{txid}/outspend/{vout}", timeout=10)
if response.status_code == 200:
data = response.json()
# Se 'spent' è False, l'UTXO non è stato speso
is_unspent = not data.get('spent', True)
return {
'is_unspent': is_unspent,
'spent_txid': data.get('txid') if data.get('spent') else None,
'spent_vin': data.get('vin') if data.get('spent') else None
}
except Exception as e:
print(f" ⚠️ Errore verifica UTXO {txid}:{vout} - {e}")
return {'is_unspent': False, 'spent_txid': None, 'spent_vin': None}
def get_last_scanned_block(self) -> int:
"""Ottieni l'ultimo blocco scannerizzato"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT last_scanned_block FROM scan_progress WHERE id = 1')
result = cursor.fetchone()
conn.close()
return result[0] if result else 0
def scan_blocks(self, start_block: int, end_block: int, delay: float = 0.5):
"""Scannerizza blocchi in un range"""
print(f"🚀 Inizio scansione blocchi {start_block} a {end_block}")
print(f"Database: {self.db_path}")
for height in range(start_block, end_block + 1):
try:
print(f"\n📦 Analisi blocco {height}...")
# Ottieni hash del blocco
block_hash = self.get_block_hash(height)
if not block_hash:
print(f" ⚠️ Hash non trovato per blocco {height}")
continue
# Ottieni timestamp
timestamp = self.get_block_timestamp(block_hash) or 0
# Ottieni transazioni
transactions = self.get_block_transactions(block_hash)
if transactions is None:
print(f" ⚠️ Transazioni non trovate per blocco {height}")
continue
# Processa transazioni
total_p2pk_in_block = 0
all_p2pk_in_block = []
for tx in transactions:
p2pk_outputs = self.extract_p2pk_from_transaction(tx)
if p2pk_outputs:
total_p2pk_in_block += len(p2pk_outputs)
all_p2pk_in_block.extend(p2pk_outputs)
for p2pk in p2pk_outputs:
print(f" ✅ P2PK in TX {tx['txid'][:16]}... | Valore: {p2pk['value']} sat | Script: {p2pk['scriptpubkey'][:20]}...")
# Salva tutti i P2PK trovati nel blocco
if all_p2pk_in_block:
self.save_p2pk_data(height, all_p2pk_in_block, timestamp)
print(f" 📊 Blocco {height}: {total_p2pk_in_block} P2PK trovati e salvati")
else:
# Aggiorna comunque il progresso anche se non ci sono P2PK
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('UPDATE scan_progress SET last_scanned_block = ? WHERE id = 1', (height,))
conn.commit()
conn.close()
print(f" 🔍 Blocco {height}: Nessun P2PK trovato")
# Aggiorna progresso ogni 10 blocchi
if height % 10 == 0:
self.print_progress()
# Rispetta l'API (rate limiting)
time.sleep(delay)
except KeyboardInterrupt:
print("\n⏸️ Scansione interrotta dall'utente")
break
except Exception as e:
print(f"❌ Errore durante scansione blocco {height}: {e}")
time.sleep(2) # Pausa più lunga in caso di errore
self.print_final_report()
def print_progress(self):
"""Stampa il progresso attuale"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('SELECT last_scanned_block, total_p2pk_found FROM scan_progress WHERE id = 1')
progress = cursor.fetchone()
cursor.execute('SELECT COUNT(DISTINCT block_height) FROM p2pk_addresses')
blocks_with_p2pk = cursor.fetchone()[0]
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses')
total_p2pk = cursor.fetchone()[0]
conn.close()
print(f"\n{'='*50}")
print(f"📈 PROGRESSO SCANSIONE")
print(f"📦 Ultimo blocco: {progress[0]}")
print(f"🔑 P2PK totali: {progress[1]}")
print(f"📊 Blocchi con P2PK: {blocks_with_p2pk}")
print(f"📝 Indirizzi unici: {total_p2pk}")
print(f"{'='*50}\n")
def print_final_report(self):
"""Stampa report finale"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Statistiche finali
cursor.execute('SELECT MAX(block_height), MIN(block_height) FROM p2pk_addresses')
max_block, min_block = cursor.fetchone()
cursor.execute('SELECT SUM(value_satoshi) FROM p2pk_addresses')
total_value = cursor.fetchone()[0] or 0
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses')
total_addresses = cursor.fetchone()[0]
print(f"\n{'='*60}")
print(f"🎉 SCANSIONE COMPLETATA")
print(f"{'='*60}")
print(f"📊 Totale indirizzi P2PK: {total_addresses}")
print(f"📦 Primo blocco con P2PK: {min_block or 'Nessuno'}")
print(f"📦 Ultimo blocco con P2PK: {max_block or 'Nessuno'}")
print(f"💰 Valore totale: {total_value / 100000000:.8f} BTC")
print(f"💾 Database: {self.db_path}")
print(f"{'='*60}")
# Esempio query per estrarre chiavi pubbliche
if total_addresses > 0:
print(f"\n📋 Esempio dati raccolti:")
cursor.execute('SELECT scriptpubkey FROM p2pk_addresses LIMIT 3')
samples = cursor.fetchall()
for i, sample in enumerate(samples):
print(f" {i+1}. {sample[0][:50]}...")
conn.close()
def export_to_csv(self, output_file: str = "p2pk_data.csv"):
"""Esporta dati in CSV per analisi"""
import csv
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT block_height, txid, output_index,
scriptpubkey, value_satoshi, timestamp
FROM p2pk_addresses
ORDER BY block_height
''')
with open(output_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Block', 'TXID', 'Output Index',
'ScriptPubKey', 'Value (satoshi)', 'Timestamp'])
for row in cursor.fetchall():
writer.writerow(row)
conn.close()
print(f"📁 Dati esportati in: {output_file}")
# USO DIDATTICO
if __name__ == "__main__":
# AVVERTENZA IMPORTANTE
print("⚠️ AVVISO: Questo script è SOLO per SCOPI EDUCATIVI")
print("⛔ NON usare per attività illegali")
print("📚 Scopo: Studio della struttura blockchain Bitcoin\n")
# Crea scanner
scanner = P2PKBlockchainScanner("bitcoin_p2pk_study.db")
# Recupera l'ultimo blocco scannerizzato
last_scanned = scanner.get_last_scanned_block()
print(f"📊 Ultimo blocco scannerizzato: {last_scanned}")
print(f"💡 I primi blocchi di Bitcoin (1-10000) contengono molti P2PK")
# Chiedi all'utente il blocco di inizio
start_input = input(f"\n📍 Blocco di inizio scansione (default: {last_scanned + 1 if last_scanned > 0 else 1}): ").strip()
if start_input:
try:
START_BLOCK = int(start_input)
if START_BLOCK < 1:
print("⚠️ Il blocco deve essere >= 1, uso blocco 1")
START_BLOCK = 1
except ValueError:
print(f"⚠️ Valore non valido, uso blocco {last_scanned + 1 if last_scanned > 0 else 1}")
START_BLOCK = last_scanned + 1 if last_scanned > 0 else 1
else:
START_BLOCK = last_scanned + 1 if last_scanned > 0 else 1
# Chiedi all'utente il blocco finale
end_input = input(f"📍 Blocco finale scansione (default: {START_BLOCK + 999}): ").strip()
if end_input:
try:
END_BLOCK = int(end_input)
if END_BLOCK < START_BLOCK:
print(f"⚠️ Il blocco finale deve essere >= {START_BLOCK}, uso {START_BLOCK + 999}")
END_BLOCK = START_BLOCK + 999
except ValueError:
print(f"⚠️ Valore non valido, uso blocco {START_BLOCK + 999}")
END_BLOCK = START_BLOCK + 999
else:
END_BLOCK = START_BLOCK + 999
# Chiedi il delay tra le richieste
delay_input = input("⏱️ Delay tra richieste in secondi (default: 1.0): ").strip()
if delay_input:
try:
REQUEST_DELAY = float(delay_input)
if REQUEST_DELAY < 0.1:
print("⚠️ Il delay minimo è 0.1s per rispettare l'API")
REQUEST_DELAY = 0.1
except ValueError:
print("⚠️ Valore non valido, uso 1.0s")
REQUEST_DELAY = 1.0
else:
REQUEST_DELAY = 1.0
print(f"\n🔧 Configurazione:")
print(f" Primo blocco: {START_BLOCK}")
print(f" Ultimo blocco: {END_BLOCK}")
print(f" Totale blocchi: {END_BLOCK - START_BLOCK + 1}")
print(f" Delay richieste: {REQUEST_DELAY}s")
print(f" Tempo stimato: ~{((END_BLOCK - START_BLOCK + 1) * REQUEST_DELAY) / 60:.1f} minuti")
# Conferma
response = input("\n▶️ Avviare la scansione? (s/n): ").strip().lower()
if response == 's':
scanner.scan_blocks(START_BLOCK, END_BLOCK, REQUEST_DELAY)
# Esporta in CSV
export = input("\n📤 Esportare dati in CSV? (s/n): ").strip().lower()
if export == 's':
scanner.export_to_csv()
else:
print("⏹️ Scansione annullata")
print("\n💡 Per analisi ulteriori:")
print(" 1. Usa SQLite per query: SELECT * FROM p2pk_addresses LIMIT 10")
print(" 2. Analizza gli script P2PK con librerie Bitcoin")
print(" 3. Studia la struttura delle transazioni early Bitcoin")

658
databases/view_db.py Executable file
View File

@@ -0,0 +1,658 @@
#!/usr/bin/env python3
"""
Script per visualizzare i dati P2PK dal database
Genera un report HTML interattivo
"""
import sqlite3
import sys
from datetime import datetime
from pathlib import Path
class P2PKDatabaseViewer:
def __init__(self, db_path: str = "bitcoin_p2pk_study.db"):
self.db_path = db_path
def check_database_exists(self) -> bool:
"""Verifica se il database esiste"""
return Path(self.db_path).exists()
def get_statistics(self) -> dict:
"""Ottieni statistiche dal database"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
stats = {}
try:
# Progresso scansione
cursor.execute('SELECT last_scanned_block, total_p2pk_found FROM scan_progress WHERE id = 1')
progress = cursor.fetchone()
stats['last_block'] = progress[0] if progress else 0
stats['total_found'] = progress[1] if progress else 0
# Totale P2PK nel database
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses')
stats['total_in_db'] = cursor.fetchone()[0]
# Range blocchi
cursor.execute('SELECT MIN(block_height), MAX(block_height) FROM p2pk_addresses')
min_block, max_block = cursor.fetchone()
stats['min_block'] = min_block if min_block is not None else 0
stats['max_block'] = max_block if max_block is not None else 0
# Blocchi unici con P2PK
cursor.execute('SELECT COUNT(DISTINCT block_height) FROM p2pk_addresses')
stats['unique_blocks'] = cursor.fetchone()[0]
# Transazioni unique
cursor.execute('SELECT COUNT(DISTINCT txid) FROM p2pk_addresses')
stats['unique_txs'] = cursor.fetchone()[0]
# P2PK non spesi (con saldo attuale)
cursor.execute('SELECT COUNT(*) FROM p2pk_addresses WHERE is_unspent = 1')
stats['unspent_count'] = cursor.fetchone()[0]
# Valore totale - calcolo manuale per evitare overflow
cursor.execute('SELECT value_satoshi FROM p2pk_addresses')
all_values = cursor.fetchall()
total_sat = 0.0
for (val,) in all_values:
if val is not None:
total_sat += float(val)
stats['total_value_btc'] = total_sat / 100000000.0
stats['total_value_sat'] = int(total_sat)
# Valore non speso
cursor.execute('SELECT value_satoshi FROM p2pk_addresses WHERE is_unspent = 1')
unspent_values = cursor.fetchall()
unspent_sat = 0.0
for (val,) in unspent_values:
if val is not None:
unspent_sat += float(val)
stats['unspent_value_btc'] = unspent_sat / 100000000.0
stats['unspent_value_sat'] = int(unspent_sat)
except Exception as e:
print(f"⚠️ Errore nel calcolo statistiche: {e}")
import traceback
traceback.print_exc()
stats = {
'last_block': 0,
'total_found': 0,
'total_in_db': 0,
'min_block': 0,
'max_block': 0,
'unique_blocks': 0,
'total_value_btc': 0.0,
'total_value_sat': 0,
'unique_txs': 0,
'unspent_count': 0,
'unspent_value_btc': 0.0,
'unspent_value_sat': 0
}
conn.close()
return stats
def get_all_p2pk(self, limit: int = None) -> list:
"""Ottieni tutti i P2PK dal database"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
query = '''
SELECT id, block_height, txid, output_index,
scriptpubkey, value_satoshi, timestamp, is_unspent, last_checked
FROM p2pk_addresses
ORDER BY block_height ASC, id ASC
'''
if limit:
query += f' LIMIT {limit}'
cursor.execute(query)
results = cursor.fetchall()
conn.close()
return results
def extract_pubkey_from_script(self, scriptpubkey: str) -> str:
"""Estrae la chiave pubblica dallo scriptPubKey"""
if not scriptpubkey:
return ""
# P2PK non compresso: 41 + pubkey (130 hex chars) + ac
if scriptpubkey.startswith('41') and len(scriptpubkey) == 134:
return scriptpubkey[2:132] # Rimuovi 41 all'inizio e ac alla fine
# P2PK compresso: 21 + pubkey (66 hex chars) + ac
elif scriptpubkey.startswith('21') and len(scriptpubkey) == 70:
return scriptpubkey[2:68] # Rimuovi 21 all'inizio e ac alla fine
return scriptpubkey
def _generate_table_html(self, p2pk_data: list) -> str:
"""Genera l'HTML della tabella"""
if not p2pk_data:
return '<div class="no-data">📭 Nessun dato P2PK trovato nel database</div>'
rows_html = []
for row in p2pk_data:
pubkey = self.extract_pubkey_from_script(row[4])
txid_short = row[2][:16] if len(row[2]) > 16 else row[2]
timestamp_str = datetime.fromtimestamp(row[6]).strftime('%Y-%m-%d %H:%M') if row[6] else 'N/A'
# row[5] è già in satoshi, lo convertiamo in BTC dividendo per 100000000
value_satoshi = row[5]
value_btc = value_satoshi / 100000000.0
# Stato UTXO (row[7] = is_unspent)
is_unspent = row[7] if len(row) > 7 else 0
utxo_status = '🟢 NON SPESO' if is_unspent else '🔴 SPESO'
utxo_class = 'unspent' if is_unspent else 'spent'
row_html = f'''
<tr class="{utxo_class}">
<td>{row[0]}</td>
<td><span class="block">{row[1]}</span></td>
<td>
<span class="txid">{txid_short}...</span>
<button class="copy-btn" onclick="copyToClipboard('{row[2]}')">📋</button>
</td>
<td>{row[3]}</td>
<td>
<div class="pubkey">
{pubkey}
<button class="copy-btn" onclick="copyToClipboard('{pubkey}')">📋</button>
</div>
</td>
<td class="value">{value_btc:.8f} BTC<br><small>({value_satoshi:,} sat)</small></td>
<td><span class="status-badge {utxo_class}">{utxo_status}</span></td>
<td>{timestamp_str}</td>
</tr>'''
rows_html.append(row_html)
return f'''
<table id="dataTable">
<thead>
<tr>
<th>ID</th>
<th>Blocco</th>
<th>TXID</th>
<th>Output</th>
<th>Chiave Pubblica</th>
<th>Valore (BTC)</th>
<th>Stato UTXO</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{''.join(rows_html)}
</tbody>
</table>'''
def generate_html_report(self, output_file: str = "p2pk_report.html"):
"""Genera un report HTML interattivo"""
if not self.check_database_exists():
print(f"❌ Database non trovato: {self.db_path}")
return
stats = self.get_statistics()
p2pk_data = self.get_all_p2pk()
html = f"""<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P2PK Database Report</title>
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
color: #333;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}}
.header {{
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 30px;
text-align: center;
}}
.header h1 {{
font-size: 2.5em;
margin-bottom: 10px;
}}
.header p {{
opacity: 0.9;
font-size: 1.1em;
}}
.stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
padding: 30px;
background: #f8f9fa;
}}
.stat-card {{
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.3s;
}}
.stat-card:hover {{
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0,0,0,0.2);
}}
.stat-card .label {{
color: #666;
font-size: 0.9em;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}}
.stat-card .value {{
font-size: 2em;
font-weight: bold;
color: #667eea;
}}
.content {{
padding: 30px;
}}
.search-box {{
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}}
.search-box input {{
width: 100%;
padding: 12px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s;
}}
.search-box input:focus {{
outline: none;
border-color: #667eea;
}}
.filter-buttons {{
margin: 20px 0;
text-align: center;
}}
.filter-btn {{
padding: 10px 20px;
margin: 0 5px;
border: 2px solid #667eea;
background: white;
color: #667eea;
border-radius: 20px;
cursor: pointer;
font-weight: bold;
font-size: 0.95em;
transition: all 0.3s;
}}
.filter-btn:hover {{
background: #f0f0f0;
}}
.filter-btn.active {{
background: #667eea;
color: white;
}}
.filter-btn.unspent-filter.active {{
background: #28a745;
border-color: #28a745;
}}
.filter-btn.spent-filter.active {{
background: #dc3545;
border-color: #dc3545;
}}
table {{
width: 100%;
border-collapse: collapse;
background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
border-radius: 10px;
overflow: hidden;
}}
thead {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}}
th {{
padding: 15px;
text-align: left;
font-weight: 600;
text-transform: uppercase;
font-size: 0.9em;
letter-spacing: 1px;
}}
td {{
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
}}
tr:hover {{
background: #f8f9fa;
}}
.pubkey {{
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #e74c3c;
word-break: break-all;
background: #fff3cd;
padding: 5px;
border-radius: 4px;
}}
.txid {{
font-family: 'Courier New', monospace;
font-size: 0.85em;
color: #3498db;
}}
.block {{
background: #667eea;
color: white;
padding: 5px 10px;
border-radius: 20px;
display: inline-block;
font-weight: bold;
}}
.value {{
color: #27ae60;
font-weight: bold;
}}
.footer {{
background: #f8f9fa;
padding: 20px;
text-align: center;
color: #666;
font-size: 0.9em;
}}
.copy-btn {{
background: #667eea;
color: white;
border: none;
padding: 5px 10px;
border-radius: 5px;
cursor: pointer;
font-size: 0.8em;
margin-left: 5px;
}}
.copy-btn:hover {{
background: #764ba2;
}}
.no-data {{
text-align: center;
padding: 50px;
color: #999;
font-size: 1.2em;
}}
.status-badge {{
padding: 5px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: bold;
display: inline-block;
}}
.status-badge.unspent {{
background: #d4edda;
color: #155724;
}}
.status-badge.spent {{
background: #f8d7da;
color: #721c24;
}}
tr.unspent {{
background: #f0fff4;
}}
tr.spent {{
opacity: 0.7;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔐 P2PK Database Report</h1>
<p>Bitcoin Pay-to-Public-Key Transaction Scanner</p>
<p style="font-size: 0.9em; margin-top: 10px;">Database: {self.db_path}</p>
</div>
<div class="stats">
<div class="stat-card">
<div class="label">Ultimo Blocco</div>
<div class="value">{stats['last_block']:,}</div>
</div>
<div class="stat-card">
<div class="label">P2PK Trovati</div>
<div class="value">{stats['total_in_db']:,}</div>
</div>
<div class="stat-card">
<div class="label">Blocchi Unici</div>
<div class="value">{stats['unique_blocks']:,}</div>
</div>
<div class="stat-card">
<div class="label">Valore Totale</div>
<div class="value">{stats['total_value_btc']:.8f} BTC</div>
</div>
<div class="stat-card">
<div class="label">Range Blocchi</div>
<div class="value">{stats['min_block']:,} - {stats['max_block']:,}</div>
</div>
<div class="stat-card">
<div class="label">Transazioni Uniche</div>
<div class="value">{stats['unique_txs']:,}</div>
</div>
<div class="stat-card" style="background: #d4edda;">
<div class="label" style="color: #155724;">💰 P2PK Non Spesi</div>
<div class="value" style="color: #155724;">{stats['unspent_count']:,}</div>
</div>
<div class="stat-card" style="background: #d1ecf1;">
<div class="label" style="color: #0c5460;">💎 Valore Non Speso</div>
<div class="value" style="color: #0c5460;">{stats['unspent_value_btc']:.8f} BTC</div>
</div>
</div>
<div class="content">
<div class="search-box">
<input type="text" id="searchInput" placeholder="🔍 Cerca per blocco, txid, o chiave pubblica..." onkeyup="filterTable()">
</div>
<div class="filter-buttons">
<button class="filter-btn active" onclick="filterByStatus('all')">🔍 Tutti i P2PK</button>
<button class="filter-btn unspent-filter" onclick="filterByStatus('unspent')">🟢 Solo Non Spesi</button>
<button class="filter-btn spent-filter" onclick="filterByStatus('spent')">🔴 Solo Spesi</button>
</div>
{self._generate_table_html(p2pk_data)}
</div>
<div class="footer">
<p>Report generato il {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p style="margin-top: 10px;">⚠️ Questo report è solo per scopi educativi e di ricerca</p>
</div>
</div>
<script>
function filterTable() {{
const input = document.getElementById('searchInput');
const filter = input.value.toUpperCase();
const table = document.getElementById('dataTable');
if (!table) return;
const tr = table.getElementsByTagName('tr');
for (let i = 1; i < tr.length; i++) {{
const row = tr[i];
const text = row.textContent || row.innerText;
if (text.toUpperCase().indexOf(filter) > -1) {{
row.style.display = '';
}} else {{
row.style.display = 'none';
}}
}}
}}
function copyToClipboard(text) {{
navigator.clipboard.writeText(text).then(() => {{
alert('✅ Copiato negli appunti!');
}}).catch(err => {{
console.error('Errore durante la copia:', err);
}});
}}
function filterByStatus(status) {{
const table = document.getElementById('dataTable');
if (!table) return;
const tr = table.getElementsByTagName('tr');
const buttons = document.querySelectorAll('.filter-btn');
// Aggiorna stato pulsanti
buttons.forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// Filtra righe in base allo stato UTXO
for (let i = 1; i < tr.length; i++) {{
const row = tr[i];
if (status === 'all') {{
row.style.display = '';
}} else if (status === 'unspent') {{
row.style.display = row.classList.contains('unspent') ? '' : 'none';
}} else if (status === 'spent') {{
row.style.display = row.classList.contains('spent') ? '' : 'none';
}}
}}
}}
</script>
</body>
</html>
"""
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html)
print(f"✅ Report HTML generato: {output_file}")
print(f"📊 Statistiche:")
print(f" - P2PK totali: {stats['total_in_db']}")
print(f" - Blocchi scansionati: {stats['last_block']}")
print(f" - Valore totale: {stats['total_value_btc']:.8f} BTC")
def print_console_report(self):
"""Stampa un report nel terminale"""
if not self.check_database_exists():
print(f"❌ Database non trovato: {self.db_path}")
return
stats = self.get_statistics()
print("\n" + "="*60)
print("📊 REPORT DATABASE P2PK")
print("="*60)
print(f"📁 Database: {self.db_path}")
print(f"📦 Ultimo blocco scansionato: {stats['last_block']:,}")
print(f"🔑 P2PK totali trovati: {stats['total_in_db']:,}")
print(f"📊 Blocchi unici con P2PK: {stats['unique_blocks']:,}")
print(f"📈 Range blocchi: {stats['min_block']:,} - {stats['max_block']:,}")
print(f"💰 Valore totale: {stats['total_value_btc']:.8f} BTC ({stats['total_value_sat']:,} sat)")
print(f"📝 Transazioni uniche: {stats['unique_txs']:,}")
print("-"*60)
print(f"💎 P2PK NON SPESI: {stats['unspent_count']:,}")
print(f"💵 Valore non speso: {stats['unspent_value_btc']:.8f} BTC ({stats['unspent_value_sat']:,} sat)")
print("="*60)
# Mostra alcuni esempi
p2pk_data = self.get_all_p2pk(limit=5)
if p2pk_data:
print("\n🔍 Primi 5 P2PK trovati:")
for row in p2pk_data:
pubkey = self.extract_pubkey_from_script(row[4])
print(f"\n Blocco {row[1]} | TX: {row[2][:16]}...")
print(f" Pubkey: {pubkey[:40]}...")
print(f" Valore: {row[5] / 100000000:.8f} BTC")
if __name__ == "__main__":
db_file = "bitcoin_p2pk_study.db"
# Se viene passato un argomento, usalo come path del database
if len(sys.argv) > 1:
db_file = sys.argv[1]
viewer = P2PKDatabaseViewer(db_file)
print("🔐 P2PK Database Viewer")
print("="*60)
print("Scegli un'opzione:")
print("1. Genera report HTML")
print("2. Mostra report nel terminale")
print("3. Entrambi")
print("="*60)
choice = input("Scelta (1/2/3): ").strip()
if choice == "1":
viewer.generate_html_report()
elif choice == "2":
viewer.print_console_report()
elif choice == "3":
viewer.generate_html_report()
viewer.print_console_report()
else:
print("❌ Scelta non valida")