leonett commited on
Commit
b4e6c50
·
verified ·
1 Parent(s): 2909f2b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -97
app.py CHANGED
@@ -3,6 +3,11 @@ import gradio as gr
3
  import networkx as nx
4
  import plotly.graph_objects as go
5
  from datetime import datetime
 
 
 
 
 
6
 
7
  def get_transaction(tx_id):
8
  """
@@ -45,10 +50,8 @@ def generar_grafo(tx_data):
45
  Nodos secundarios: direcciones de inputs y outputs.
46
  """
47
  G = nx.DiGraph()
48
-
49
  txid = tx_data.get("txid", "desconocido")
50
  G.add_node(txid, color="#FF6B6B", size=20, tipo="Transacción")
51
-
52
  # Inputs
53
  for inp in tx_data.get("vin", []):
54
  prevout = inp.get("prevout", {})
@@ -56,28 +59,25 @@ def generar_grafo(tx_data):
56
  if addr:
57
  G.add_node(addr, color="#4ECDC4", size=15, tipo="Input")
58
  G.add_edge(addr, txid)
59
-
60
  # Outputs
61
  for out in tx_data.get("vout", []):
62
  addr = out.get("scriptpubkey_address")
63
  if addr:
64
  G.add_node(addr, color="#45B7D1", size=15, tipo="Output")
65
  G.add_edge(txid, addr)
66
-
67
  return G
68
 
69
  def check_blockchain_tags(tx_id):
70
  """
71
  Consulta la API pública de blockchain.com para ver si el TXID (o sus metadatos)
72
  incluye información que indique que la transacción ha sido etiquetada como MIXER.
73
- Esta función es un ejemplo y puede requerir ajustes según la estructura real del JSON.
74
  """
75
  url = f"https://blockchain.info/rawtx/{tx_id}?format=json"
76
  try:
77
  response = requests.get(url, timeout=10)
78
  if response.status_code == 200:
79
  data = response.json()
80
- # Se asume que la API podría incluir un campo 'tags' o 'category'
81
  tags = data.get("tags", [])
82
  if "MIXER" in tags:
83
  return True
@@ -92,106 +92,75 @@ def check_blockchain_tags(tx_id):
92
  def analizar_transaccion(tx_id):
93
  """
94
  Analiza una transacción Bitcoin:
95
- - Obtiene los datos desde Blockstream.info o mempool.space.
96
  - Calcula totales en BTC y fee, mostrando también su equivalente en USD.
97
- - Muestra información adicional: versión, tamaño, peso y fee rate.
98
- - Aplica una heurística mejorada para detectar posibles CoinJoin/mixers:
99
- * Si tiene >5 inputs y >5 outputs y menos de 3 montos únicos, o
100
- * Si tiene >2 outputs y 2 o menos montos únicos.
101
- - Incorpora información adicional de blockchain.com (si está disponible) para detectar etiquetas MIXER.
102
  - Genera un grafo interactivo.
103
- - Retorna un reporte en HTML y la figura del grafo.
104
-
105
- Nota: La detección se basa en heurísticas y en la información adicional de blockchain.com.
106
- Se recomienda complementar con herramientas especializadas para un análisis forense completo.
107
  """
108
  tx_data = get_transaction(tx_id)
109
  if not tx_data:
110
- return ("❌ Transacción no encontrada o error al obtener datos. "
111
- "Asegúrate de ingresar un TXID de Bitcoin válido."), None
112
 
113
  try:
114
- # Datos básicos
115
  num_inputs = len(tx_data.get("vin", []))
116
  num_outputs = len(tx_data.get("vout", []))
117
  montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])]
118
  montos_unicos = len(set(montos))
119
-
120
- # Cálculos forenses en BTC
121
  total_input_value = sum(inp.get("prevout", {}).get("value", 0) for inp in tx_data.get("vin", [])) / 1e8
122
  total_output_value = sum(out.get("value", 0) for out in tx_data.get("vout", [])) / 1e8
123
  fee = total_input_value - total_output_value
124
 
125
- # Heurística para detectar CoinJoin/mixer
126
  heuristic_mixer = ((num_inputs > 5 and num_outputs > 5 and montos_unicos < 3) or
127
  (num_outputs > 2 and montos_unicos <= 2))
128
-
129
- # Verificar información adicional vía blockchain.com
130
  es_mixer_blockchain = check_blockchain_tags(tx_id)
131
-
132
- # Combinamos ambas señales: si alguna indica mixer, marcamos la transacción.
133
  es_mixer = heuristic_mixer or es_mixer_blockchain
134
 
135
- # Construir mensaje detallado según la fuente de la alerta
136
  if es_mixer:
137
  if heuristic_mixer and es_mixer_blockchain:
138
  mixer_message = (
139
  "<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> "
140
- "La transacción presenta patrones compatibles con un mixer (CoinJoin) según la heurística, "
141
- "y la API de blockchain.com indica que está etiquetada como MIXER. Se recomienda un análisis forense adicional.</p>"
142
  )
143
  elif heuristic_mixer:
144
  mixer_message = (
145
  "<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> "
146
- "La transacción presenta patrones compatibles con un mixer (CoinJoin) según la heurística. "
147
- "Se recomienda un análisis forense adicional.</p>"
148
  )
149
  elif es_mixer_blockchain:
150
  mixer_message = (
151
  "<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> "
152
- "La API de blockchain.com indica que esta transacción está etiquetada como MIXER. "
153
- "Se recomienda un análisis forense adicional.</p>"
154
  )
155
  else:
156
  mixer_message = (
157
  "<p style='color: #4CAF50;'><strong>Sin indicios de mezcla:</strong> "
158
- "La transacción no muestra patrones comunes asociados a servicios mixer.</p>"
159
  )
160
-
161
- # Obtener precio BTC actual
162
- btc_price = get_btc_price()
163
- if btc_price is None:
164
- btc_price = 0
165
-
166
- # Datos adicionales de la transacción
167
  version = tx_data.get("version", "N/A")
168
  size = tx_data.get("size", None)
169
  weight = tx_data.get("weight", "N/A")
170
- if size:
171
- fee_rate = (fee * 1e8) / size # en sat/byte
172
- fee_rate_str = f"{fee_rate:.2f} sat/byte"
173
- else:
174
- fee_rate_str = "N/A"
175
-
176
- # Formatear fecha y hora (si la transacción está confirmada)
177
  status = tx_data.get("status", {})
178
  block_time = status.get("block_time")
179
- if block_time:
180
- fecha_hora_str = datetime.fromtimestamp(block_time).strftime('%Y-%m-%d %H:%M:%S')
181
- else:
182
- fecha_hora_str = "Desconocida"
183
-
184
- # Generar grafo de la transacción
185
  G = generar_grafo(tx_data)
186
  pos = nx.spring_layout(G, seed=42)
187
-
188
  edge_x, edge_y = [], []
189
  for edge in G.edges():
190
  x0, y0 = pos[edge[0]]
191
  x1, y1 = pos[edge[1]]
192
  edge_x.extend([x0, x1, None])
193
  edge_y.extend([y0, y1, None])
194
-
195
  node_x, node_y, hover_texts, node_colors, node_sizes = [], [], [], [], []
196
  for node in G.nodes():
197
  x, y = pos[node]
@@ -201,7 +170,6 @@ def analizar_transaccion(tx_id):
201
  hover_texts.append(f"Tipo: {tipo}<br>ID: {node[:8]}...")
202
  node_colors.append(G.nodes[node].get("color", "#FFFFFF"))
203
  node_sizes.append(G.nodes[node].get("size", 10))
204
-
205
  fig = go.Figure(
206
  data=[
207
  go.Scatter(
@@ -228,40 +196,35 @@ def analizar_transaccion(tx_id):
228
  template="plotly_dark"
229
  )
230
  )
231
-
232
- # Calcular detalles de outputs: agrupar direcciones por monto
233
- unique_outputs_dict = {}
234
- for out in tx_data.get("vout", []):
235
- amount = out.get("value", 0) / 1e8
236
- addr = out.get("scriptpubkey_address")
237
- if addr:
238
- unique_outputs_dict.setdefault(amount, []).append(addr)
239
-
240
- unique_outputs_details = "<ul>"
241
- for amt, addrs in unique_outputs_dict.items():
242
- unique_outputs_details += f"<li>{amt:.8f} BTC: {', '.join(addrs)}</li>"
243
- unique_outputs_details += "</ul>"
244
-
245
- # Si se detecta posible mixer, se muestran métricas adicionales de outputs;
246
- # de lo contrario, se omiten.
247
  if es_mixer:
248
- mixer_metrics = f"""
249
- <p>📤 <strong>Outputs:</strong> {num_outputs}</p>
250
- <p>💰 <strong>Montos únicos en outputs:</strong> {montos_unicos}</p>
251
- <p><strong>Detalles de outputs únicos:</strong></p>
252
- {unique_outputs_details}
253
- """
 
 
 
 
 
 
 
 
 
254
  else:
255
  mixer_metrics = ""
256
-
257
- # Formateo de montos con equivalentes en USD
258
  total_input_str = f"{total_input_value:.8f} BTC"
259
  total_input_str += f" (${total_input_value * btc_price:,.2f})" if btc_price else ""
260
  total_output_str = f"{total_output_value:.8f} BTC"
261
  total_output_str += f" (${total_output_value * btc_price:,.2f})" if btc_price else ""
262
  fee_str = f"{fee:.8f} BTC"
263
  fee_str += f" (${fee * btc_price:,.2f})" if btc_price else ""
264
-
 
265
  reporte = f"""
266
  <div style="padding: 20px; border-radius: 10px;">
267
  <h3 style="color: {'#FF5252' if es_mixer else '#4CAF50'};">
@@ -282,21 +245,82 @@ def analizar_transaccion(tx_id):
282
  <p>🔔 <strong>Estado de Confirmación:</strong> {"Confirmada" if status.get("confirmed", False) else "No confirmada"}</p>
283
  {mixer_message}
284
  <p style="font-size: 0.9em;">
285
- Nota: La detección se basa en heurísticas y en la información adicional de blockchain.com. Se recomienda usar herramientas especializadas (como CoinJoin explorers) para un análisis forense completo.
286
  </p>
287
  </div>
288
  """
289
-
290
- return reporte, fig
291
 
292
  except Exception as e:
293
- return f"⚠️ Error durante el análisis: {str(e)}", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
- # INTERFAZ GRÁFICA CON GRADIO
296
  with gr.Blocks(
297
  theme=gr.themes.Soft(),
298
  title="🔍 Detector de Mixers / CoinJoin en Transacciones Bitcoin",
299
- css=".custom-btn { background-color: #ADD8E6 !important; color: black !important; }"
300
  ) as demo:
301
  gr.Markdown("# 🔍 Detector de Mixers / CoinJoin en Transacciones Bitcoin")
302
  gr.Markdown("Desarrollado por José R. Leonett para la comunidad de Peritos Forenses Digitales de Guatemala [www.forensedigital.gt](http://www.forensedigital.gt)")
@@ -305,14 +329,12 @@ with gr.Blocks(
305
  with gr.Row():
306
  # Columna Izquierda: Grafo y controles de entrada
307
  with gr.Column(scale=1):
308
- # Espacio para el grafo (se actualizará con la transacción analizada)
309
  plot_output = gr.Plot()
310
- # Controles de entrada debajo del grafo
311
  tx_input = gr.Textbox(
312
  label="TXID de la Transacción",
313
  placeholder="Ej: 9dd51e2d45f4f7bddcc3f0f7a05c3fd60543a11cfc9fbd0e1ca4434668cfa3e1"
314
  )
315
- btn = gr.Button("Analizar Transacción", elem_classes=["custom-btn"])
316
  gr.Markdown("### Ejemplos de TXIDs válidos:")
317
  gr.Examples(
318
  examples=[
@@ -325,7 +347,7 @@ with gr.Blocks(
325
  # Columna Derecha: Resultados y recuadro explicativo
326
  with gr.Column(scale=1):
327
  reporte_html = gr.HTML()
328
- explanation_text = """
329
  <div style="overflow-y: scroll; height: 220px; border: 1px solid #cccccc; padding: 10px;">
330
  <h4>Explicación de los campos:</h4>
331
  <ul>
@@ -343,13 +365,20 @@ with gr.Blocks(
343
  <li><strong>Total Salidas:</strong> Suma total de los valores de salida (en BTC y USD).</li>
344
  <li><strong>Fee:</strong> Diferencia entre entradas y salidas, que representa la tarifa de la transacción (en BTC y USD).</li>
345
  <li><strong>Estado de Confirmación:</strong> Indica si la transacción está confirmada o no.</li>
346
- <li><strong>Alerta de Mixer / CoinJoin:</strong> Muestra si la transacción presenta patrones de mezcla, según la heurística y/o información de blockchain.com.</li>
347
  </ul>
348
  </div>
349
- """
350
- explanation_box = gr.HTML(value=explanation_text)
 
 
 
 
 
351
 
352
- # Configuración de la acción del botón
353
- btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output])
 
 
354
 
355
  demo.launch()
 
3
  import networkx as nx
4
  import plotly.graph_objects as go
5
  from datetime import datetime
6
+ import re
7
+ import os
8
+ from fpdf import FPDF, HTMLMixin
9
+
10
+ # ---------------- Funciones de análisis y grafo -------------------
11
 
12
  def get_transaction(tx_id):
13
  """
 
50
  Nodos secundarios: direcciones de inputs y outputs.
51
  """
52
  G = nx.DiGraph()
 
53
  txid = tx_data.get("txid", "desconocido")
54
  G.add_node(txid, color="#FF6B6B", size=20, tipo="Transacción")
 
55
  # Inputs
56
  for inp in tx_data.get("vin", []):
57
  prevout = inp.get("prevout", {})
 
59
  if addr:
60
  G.add_node(addr, color="#4ECDC4", size=15, tipo="Input")
61
  G.add_edge(addr, txid)
 
62
  # Outputs
63
  for out in tx_data.get("vout", []):
64
  addr = out.get("scriptpubkey_address")
65
  if addr:
66
  G.add_node(addr, color="#45B7D1", size=15, tipo="Output")
67
  G.add_edge(txid, addr)
 
68
  return G
69
 
70
  def check_blockchain_tags(tx_id):
71
  """
72
  Consulta la API pública de blockchain.com para ver si el TXID (o sus metadatos)
73
  incluye información que indique que la transacción ha sido etiquetada como MIXER.
74
+ (Esta función es un ejemplo y podría necesitar ajustes según la respuesta real.)
75
  """
76
  url = f"https://blockchain.info/rawtx/{tx_id}?format=json"
77
  try:
78
  response = requests.get(url, timeout=10)
79
  if response.status_code == 200:
80
  data = response.json()
 
81
  tags = data.get("tags", [])
82
  if "MIXER" in tags:
83
  return True
 
92
  def analizar_transaccion(tx_id):
93
  """
94
  Analiza una transacción Bitcoin:
95
+ - Obtiene datos desde Blockstream.info o mempool.space.
96
  - Calcula totales en BTC y fee, mostrando también su equivalente en USD.
97
+ - Muestra información adicional (versión, tamaño, peso, fee rate).
98
+ - Aplica heurística para detectar posibles CoinJoin/mixers.
99
+ - Incorpora información adicional de blockchain.com (etiqueta MIXER).
 
 
100
  - Genera un grafo interactivo.
101
+ - Retorna un informe en HTML y la figura del grafo.
 
 
 
102
  """
103
  tx_data = get_transaction(tx_id)
104
  if not tx_data:
105
+ return ("❌ Transacción no encontrada o error al obtener datos. Asegúrate de ingresar un TXID válido."), None, None
 
106
 
107
  try:
 
108
  num_inputs = len(tx_data.get("vin", []))
109
  num_outputs = len(tx_data.get("vout", []))
110
  montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])]
111
  montos_unicos = len(set(montos))
 
 
112
  total_input_value = sum(inp.get("prevout", {}).get("value", 0) for inp in tx_data.get("vin", [])) / 1e8
113
  total_output_value = sum(out.get("value", 0) for out in tx_data.get("vout", [])) / 1e8
114
  fee = total_input_value - total_output_value
115
 
116
+ # Heurística para detectar mixer
117
  heuristic_mixer = ((num_inputs > 5 and num_outputs > 5 and montos_unicos < 3) or
118
  (num_outputs > 2 and montos_unicos <= 2))
 
 
119
  es_mixer_blockchain = check_blockchain_tags(tx_id)
 
 
120
  es_mixer = heuristic_mixer or es_mixer_blockchain
121
 
 
122
  if es_mixer:
123
  if heuristic_mixer and es_mixer_blockchain:
124
  mixer_message = (
125
  "<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> "
126
+ "La transacción cumple criterios heurísticos y la API de blockchain.com la etiqueta como MIXER.</p>"
 
127
  )
128
  elif heuristic_mixer:
129
  mixer_message = (
130
  "<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> "
131
+ "La transacción cumple criterios heurísticos compatibles con un mixer.</p>"
 
132
  )
133
  elif es_mixer_blockchain:
134
  mixer_message = (
135
  "<p style='color: #FF5252;'><strong>Alerta de Mixer / CoinJoin:</strong> "
136
+ "La API de blockchain.com etiqueta esta transacción como MIXER.</p>"
 
137
  )
138
  else:
139
  mixer_message = (
140
  "<p style='color: #4CAF50;'><strong>Sin indicios de mezcla:</strong> "
141
+ "La transacción no presenta patrones de mixer.</p>"
142
  )
143
+
144
+ btc_price = get_btc_price() or 0
145
+
 
 
 
 
146
  version = tx_data.get("version", "N/A")
147
  size = tx_data.get("size", None)
148
  weight = tx_data.get("weight", "N/A")
149
+ fee_rate_str = f"{(fee * 1e8) / size:.2f} sat/byte" if size else "N/A"
150
+
 
 
 
 
 
151
  status = tx_data.get("status", {})
152
  block_time = status.get("block_time")
153
+ fecha_hora_str = datetime.fromtimestamp(block_time).strftime('%Y-%m-%d %H:%M:%S') if block_time else "Desconocida"
154
+
155
+ # Generar grafo
 
 
 
156
  G = generar_grafo(tx_data)
157
  pos = nx.spring_layout(G, seed=42)
 
158
  edge_x, edge_y = [], []
159
  for edge in G.edges():
160
  x0, y0 = pos[edge[0]]
161
  x1, y1 = pos[edge[1]]
162
  edge_x.extend([x0, x1, None])
163
  edge_y.extend([y0, y1, None])
 
164
  node_x, node_y, hover_texts, node_colors, node_sizes = [], [], [], [], []
165
  for node in G.nodes():
166
  x, y = pos[node]
 
170
  hover_texts.append(f"Tipo: {tipo}<br>ID: {node[:8]}...")
171
  node_colors.append(G.nodes[node].get("color", "#FFFFFF"))
172
  node_sizes.append(G.nodes[node].get("size", 10))
 
173
  fig = go.Figure(
174
  data=[
175
  go.Scatter(
 
196
  template="plotly_dark"
197
  )
198
  )
199
+
200
+ # Agrupar detalles de outputs (solo si se detecta mixer)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  if es_mixer:
202
+ unique_outputs_dict = {}
203
+ for out in tx_data.get("vout", []):
204
+ amount = out.get("value", 0) / 1e8
205
+ addr = out.get("scriptpubkey_address")
206
+ if addr:
207
+ unique_outputs_dict.setdefault(amount, []).append(addr)
208
+ unique_outputs_details = "<ul>"
209
+ for amt, addrs in unique_outputs_dict.items():
210
+ unique_outputs_details += f"<li>{amt:.8f} BTC: {', '.join(addrs)}</li>"
211
+ unique_outputs_details += "</ul>"
212
+ mixer_metrics = (
213
+ f"<p>📤 <strong>Outputs:</strong> {num_outputs}</p>"
214
+ f"<p>💰 <strong>Montos únicos en outputs:</strong> {montos_unicos}</p>"
215
+ f"<p><strong>Detalles de outputs únicos:</strong></p>{unique_outputs_details}"
216
+ )
217
  else:
218
  mixer_metrics = ""
219
+
 
220
  total_input_str = f"{total_input_value:.8f} BTC"
221
  total_input_str += f" (${total_input_value * btc_price:,.2f})" if btc_price else ""
222
  total_output_str = f"{total_output_value:.8f} BTC"
223
  total_output_str += f" (${total_output_value * btc_price:,.2f})" if btc_price else ""
224
  fee_str = f"{fee:.8f} BTC"
225
  fee_str += f" (${fee * btc_price:,.2f})" if btc_price else ""
226
+
227
+ # Construir el informe HTML
228
  reporte = f"""
229
  <div style="padding: 20px; border-radius: 10px;">
230
  <h3 style="color: {'#FF5252' if es_mixer else '#4CAF50'};">
 
245
  <p>🔔 <strong>Estado de Confirmación:</strong> {"Confirmada" if status.get("confirmed", False) else "No confirmada"}</p>
246
  {mixer_message}
247
  <p style="font-size: 0.9em;">
248
+ 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.
249
  </p>
250
  </div>
251
  """
252
+ # Además devolvemos (report, fig) para almacenarlos en un estado
253
+ return reporte, fig, (reporte, fig)
254
 
255
  except Exception as e:
256
+ return f"⚠️ Error durante el análisis: {str(e)}", None, None
257
+
258
+ # ---------------- Función para generar el PDF -------------------
259
+
260
+ class PDF(FPDF, HTMLMixin):
261
+ pass
262
+
263
+ def generar_pdf(report, fig):
264
+ """
265
+ Genera un PDF que incluye:
266
+ - Título: ANALISIS FORENSE DE BLOCKCHAIN
267
+ - El contenido del informe (texto plano, obtenido removiendo etiquetas HTML)
268
+ - El grafo (convertido a imagen PNG)
269
+ - Pie de página: "Generado por José R. Leonett"
270
+ Retorna el PDF como bytes.
271
+ """
272
+ # Guardar el grafo como imagen PNG temporalmente
273
+ temp_img_path = "temp_graph.png"
274
+ try:
275
+ fig.write_image(temp_img_path)
276
+ except Exception as e:
277
+ print("Error al guardar la imagen del grafo:", e)
278
+ temp_img_path = None
279
+
280
+ # Extraer texto plano del informe (quitando etiquetas HTML simples)
281
+ plain_text = re.sub('<[^<]+?>', '', report)
282
+
283
+ pdf = PDF()
284
+ pdf.add_page()
285
+ # Título
286
+ pdf.set_font("Arial", "B", 16)
287
+ pdf.cell(0, 10, "ANALISIS FORENSE DE BLOCKCHAIN", ln=True, align="C")
288
+ pdf.ln(5)
289
+ # Contenido del informe
290
+ pdf.set_font("Arial", "", 12)
291
+ pdf.multi_cell(0, 10, plain_text)
292
+ pdf.ln(5)
293
+ # Agregar la imagen del grafo si existe
294
+ if temp_img_path and os.path.exists(temp_img_path):
295
+ pdf.add_page()
296
+ # Ajustar el ancho a 190 (márgenes de 10) para que se centre
297
+ pdf.image(temp_img_path, x=10, y=20, w=190)
298
+ os.remove(temp_img_path)
299
+ # Pie de página
300
+ pdf.set_font("Arial", "I", 10)
301
+ pdf.ln(10)
302
+ pdf.cell(0, 10, "Generado por José R. Leonett", ln=True, align="C")
303
+
304
+ pdf_bytes = pdf.output(dest="S").encode("latin1")
305
+ return pdf_bytes
306
+
307
+ def descargar_pdf(analysis_tuple):
308
+ """
309
+ Función que se activa al presionar el botón DESCARGAR ANALISIS.
310
+ Recibe como entrada la tupla (report, fig) generada por analizar_transaccion.
311
+ Si no hay datos, no devuelve nada.
312
+ """
313
+ if not analysis_tuple:
314
+ return None
315
+ report, fig = analysis_tuple
316
+ return generar_pdf(report, fig)
317
+
318
+ # ---------------- INTERFAZ GRÁFICA CON GRADIO -------------------
319
 
 
320
  with gr.Blocks(
321
  theme=gr.themes.Soft(),
322
  title="🔍 Detector de Mixers / CoinJoin en Transacciones Bitcoin",
323
+ css=".custom-btn { background-color: #ADD8E6 !important; color: black !important; padding: 5px 10px; font-size: 12px; }"
324
  ) as demo:
325
  gr.Markdown("# 🔍 Detector de Mixers / CoinJoin en Transacciones Bitcoin")
326
  gr.Markdown("Desarrollado por José R. Leonett para la comunidad de Peritos Forenses Digitales de Guatemala [www.forensedigital.gt](http://www.forensedigital.gt)")
 
329
  with gr.Row():
330
  # Columna Izquierda: Grafo y controles de entrada
331
  with gr.Column(scale=1):
 
332
  plot_output = gr.Plot()
 
333
  tx_input = gr.Textbox(
334
  label="TXID de la Transacción",
335
  placeholder="Ej: 9dd51e2d45f4f7bddcc3f0f7a05c3fd60543a11cfc9fbd0e1ca4434668cfa3e1"
336
  )
337
+ analyze_btn = gr.Button("Analizar Transacción", elem_classes=["custom-btn"])
338
  gr.Markdown("### Ejemplos de TXIDs válidos:")
339
  gr.Examples(
340
  examples=[
 
347
  # Columna Derecha: Resultados y recuadro explicativo
348
  with gr.Column(scale=1):
349
  reporte_html = gr.HTML()
350
+ explanation_box = gr.HTML(value="""
351
  <div style="overflow-y: scroll; height: 220px; border: 1px solid #cccccc; padding: 10px;">
352
  <h4>Explicación de los campos:</h4>
353
  <ul>
 
365
  <li><strong>Total Salidas:</strong> Suma total de los valores de salida (en BTC y USD).</li>
366
  <li><strong>Fee:</strong> Diferencia entre entradas y salidas, que representa la tarifa de la transacción (en BTC y USD).</li>
367
  <li><strong>Estado de Confirmación:</strong> Indica si la transacción está confirmada o no.</li>
368
+ <li><strong>Alerta de Mixer / CoinJoin:</strong> Muestra si la transacción presenta patrones de mezcla.</li>
369
  </ul>
370
  </div>
371
+ """)
372
+
373
+ # Estado para almacenar el análisis generado (tupla: (report, fig))
374
+ analysis_state = gr.State()
375
+
376
+ # Al hacer clic en "Analizar Transacción", se actualizan el informe, el grafo y se guarda el estado.
377
+ analyze_btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output, analysis_state])
378
 
379
+ # Botón para descargar el análisis en PDF; se activa sólo si hay datos en analysis_state.
380
+ download_btn = gr.Button("DESCARGAR ANALISIS", elem_classes=["custom-btn"])
381
+ pdf_file = gr.File(label="Archivo PDF generado")
382
+ download_btn.click(fn=descargar_pdf, inputs=analysis_state, outputs=pdf_file)
383
 
384
  demo.launch()