import requests import gradio as gr import networkx as nx import plotly.graph_objects as go from datetime import datetime import re import os import tempfile import base64 # ---------------- Funciones de análisis y grafo ------------------- def get_transaction(tx_id): """ Obtiene los datos de una transacción Bitcoin usando endpoints públicos. Se prueba primero con Blockstream.info y, de fallar, con mempool.space. """ urls = [ f"https://blockstream.info/api/tx/{tx_id}", f"https://mempool.space/api/tx/{tx_id}" ] for url in urls: try: response = requests.get(url, timeout=10) if response.status_code == 200: return response.json() else: print(f"Error en {url}: Código de estado {response.status_code}") except Exception as e: print(f"Excepción al consultar {url}: {e}") return None def get_btc_price(): """ Obtiene el precio actual de Bitcoin en USD utilizando la API de CoinGecko. """ try: response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", timeout=10) if response.status_code == 200: data = response.json() price = data.get("bitcoin", {}).get("usd") return price except Exception as e: print("Error fetching BTC price:", e) return None def generar_grafo(tx_data): """ Genera un grafo dirigido a partir de los datos de la transacción. Nodo central: la transacción. Nodos secundarios: direcciones de inputs y outputs. """ G = nx.DiGraph() txid = tx_data.get("txid", "desconocido") G.add_node(txid, color="#FF6B6B", size=20, tipo="Transacción") for inp in tx_data.get("vin", []): prevout = inp.get("prevout", {}) addr = prevout.get("scriptpubkey_address") if addr: G.add_node(addr, color="#4ECDC4", size=15, tipo="Input") G.add_edge(addr, txid) for out in tx_data.get("vout", []): addr = out.get("scriptpubkey_address") if addr: G.add_node(addr, color="#45B7D1", size=15, tipo="Output") G.add_edge(txid, addr) return G def check_blockchain_tags(tx_id): """ Consulta la API pública de blockchain.com para ver si el TXID (o sus metadatos) indica que la transacción ha sido etiquetada como MIXER. (Esta función es un ejemplo y podría necesitar ajustes según la respuesta real.) """ url = f"https://blockchain.info/rawtx/{tx_id}?format=json" try: response = requests.get(url, timeout=10) if response.status_code == 200: data = response.json() tags = data.get("tags", []) if "MIXER" in tags: return True if data.get("category", "").upper() == "MIXER": return True else: print(f"Error consultando blockchain.com: {response.status_code}") except Exception as e: print(f"Excepción en check_blockchain_tags: {e}") return False def analizar_transaccion(tx_id): """ Analiza una transacción Bitcoin: - Obtiene datos desde Blockstream.info o mempool.space. - Calcula totales en BTC y fee, mostrando también su equivalente en USD. - Muestra información adicional (versión, tamaño, peso, fee rate). - Aplica heurística para detectar posibles CoinJoin/mixers. - Incorpora información adicional de blockchain.com (etiqueta MIXER). - Genera un grafo interactivo. - Retorna un informe en HTML, la figura del grafo y una tupla (reporte, fig) para su uso. """ tx_data = get_transaction(tx_id) if not tx_data: return ("❌ Transacción no encontrada o error al obtener datos. Asegúrate de ingresar un TXID válido."), None, None try: num_inputs = len(tx_data.get("vin", [])) num_outputs = len(tx_data.get("vout", [])) montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])] montos_unicos = len(set(montos)) total_input_value = sum(inp.get("prevout", {}).get("value", 0) for inp in tx_data.get("vin", [])) / 1e8 total_output_value = sum(out.get("value", 0) for out in tx_data.get("vout", [])) / 1e8 fee = total_input_value - total_output_value heuristic_mixer = ((num_inputs > 5 and num_outputs > 5 and montos_unicos < 3) or (num_outputs > 2 and montos_unicos <= 2)) es_mixer_blockchain = check_blockchain_tags(tx_id) es_mixer = heuristic_mixer or es_mixer_blockchain if es_mixer: if heuristic_mixer and es_mixer_blockchain: mixer_message = ( "
Alerta de Mixer / CoinJoin: La transacción cumple criterios heurísticos y la API de blockchain.com la etiqueta como MIXER.
" ) elif heuristic_mixer: mixer_message = ( "Alerta de Mixer / CoinJoin: La transacción cumple criterios heurísticos compatibles con un mixer.
" ) elif es_mixer_blockchain: mixer_message = ( "Alerta de Mixer / CoinJoin: La API de blockchain.com etiqueta esta transacción como MIXER.
" ) else: mixer_message = ( "Sin indicios de mezcla: La transacción no presenta patrones de mixer.
" ) btc_price = get_btc_price() or 0 version = tx_data.get("version", "N/A") size = tx_data.get("size", None) weight = tx_data.get("weight", "N/A") fee_rate_str = f"{(fee * 1e8) / size:.2f} sat/byte" if size else "N/A" status = tx_data.get("status", {}) block_time = status.get("block_time") fecha_hora_str = datetime.fromtimestamp(block_time).strftime('%Y-%m-%d %H:%M:%S') if block_time else "Desconocida" # Generar grafo G = generar_grafo(tx_data) pos = nx.spring_layout(G, seed=42) edge_x, edge_y = [], [] for edge in G.edges(): x0, y0 = pos[edge[0]] x1, y1 = pos[edge[1]] edge_x.extend([x0, x1, None]) edge_y.extend([y0, y1, None]) node_x, node_y, hover_texts, node_colors, node_sizes = [], [], [], [], [] for node in G.nodes(): x, y = pos[node] node_x.append(x) node_y.append(y) tipo = G.nodes[node].get("tipo", "desconocido") hover_texts.append(f"Tipo: {tipo}📤 Outputs: {num_outputs}
" f"💰 Montos únicos en outputs: {montos_unicos}
" f"Detalles de outputs únicos:
{unique_outputs_details}" ) else: mixer_metrics = "" total_input_str = f"{total_input_value:.8f} BTC" total_input_str += f" (${total_input_value * btc_price:,.2f})" if btc_price else "" total_output_str = f"{total_output_value:.8f} BTC" total_output_str += f" (${total_output_value * btc_price:,.2f})" if btc_price else "" fee_str = f"{fee:.8f} BTC" fee_str += f" (${fee * btc_price:,.2f})" if btc_price else "" # Se ha eliminado elTXID: {tx_id}
Fecha y Hora: {fecha_hora_str}
Versión: {version}
Tamaño: {size if size else 'N/A'} bytes
Peso: {weight}
Fee rate: {fee_rate_str}
📥 Inputs: {num_inputs}
{mixer_metrics}Total Entradas: {total_input_str}
Total Salidas: {total_output_str}
Fee: {fee_str}
Estado de Confirmación: {"Confirmada" if status.get("confirmed", False) else "No confirmada"}
{mixer_message}Nota: La detección se basa en heurísticas y en información adicional de blockchain.com. Se recomienda usar herramientas especializadas para un análisis forense completo.
No hay análisis generado.
" report, _ = analysis_tuple html_modal = f'''