leonett commited on
Commit
c62d2fd
·
verified ·
1 Parent(s): df11762

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +25 -38
app.py CHANGED
@@ -5,6 +5,7 @@ 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 -------------------
@@ -52,14 +53,12 @@ def generar_grafo(tx_data):
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", {})
58
  addr = prevout.get("scriptpubkey_address")
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:
@@ -98,7 +97,7 @@ def analizar_transaccion(tx_id):
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:
@@ -113,7 +112,6 @@ def analizar_transaccion(tx_id):
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)
@@ -197,7 +195,7 @@ def analizar_transaccion(tx_id):
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", []):
@@ -224,7 +222,6 @@ def analizar_transaccion(tx_id):
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'};">
@@ -249,7 +246,7 @@ def analizar_transaccion(tx_id):
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:
@@ -264,39 +261,35 @@ 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")
@@ -306,9 +299,8 @@ def generar_pdf(report, fig):
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
@@ -327,7 +319,6 @@ with gr.Blocks(
327
  gr.Markdown("**Nota:** Este analizador funciona únicamente con transacciones de Bitcoin. No se pueden analizar transacciones de Ethereum.")
328
 
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(
@@ -343,8 +334,6 @@ with gr.Blocks(
343
  ],
344
  inputs=tx_input
345
  )
346
-
347
- # Columna Derecha: Resultados y recuadro explicativo
348
  with gr.Column(scale=1):
349
  reporte_html = gr.HTML()
350
  explanation_box = gr.HTML(value="""
@@ -358,27 +347,25 @@ with gr.Blocks(
358
  <li><strong>Peso:</strong> Peso de la transacción en unidades de peso.</li>
359
  <li><strong>Fee rate:</strong> Tarifa pagada por byte (sat/byte).</li>
360
  <li><strong>Inputs:</strong> Número de entradas de la transacción.</li>
361
- <li><strong>Outputs:</strong> Número de salidas (se muestra solo si se detecta posible mixer).</li>
362
- <li><strong>Montos únicos en outputs:</strong> Número de valores únicos en las salidas (se muestra solo si se detecta mixer).</li>
363
- <li><strong>Detalles de outputs únicos:</strong> Lista de cada monto (en BTC) junto a las direcciones que lo reciben (se muestra solo si se detecta mixer).</li>
364
  <li><strong>Total Entradas:</strong> Suma total de los valores de entrada (en BTC y USD).</li>
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()
 
5
  from datetime import datetime
6
  import re
7
  import os
8
+ import tempfile
9
  from fpdf import FPDF, HTMLMixin
10
 
11
  # ---------------- Funciones de análisis y grafo -------------------
 
53
  G = nx.DiGraph()
54
  txid = tx_data.get("txid", "desconocido")
55
  G.add_node(txid, color="#FF6B6B", size=20, tipo="Transacción")
 
56
  for inp in tx_data.get("vin", []):
57
  prevout = inp.get("prevout", {})
58
  addr = prevout.get("scriptpubkey_address")
59
  if addr:
60
  G.add_node(addr, color="#4ECDC4", size=15, tipo="Input")
61
  G.add_edge(addr, txid)
 
62
  for out in tx_data.get("vout", []):
63
  addr = out.get("scriptpubkey_address")
64
  if addr:
 
97
  - Aplica heurística para detectar posibles CoinJoin/mixers.
98
  - Incorpora información adicional de blockchain.com (etiqueta MIXER).
99
  - Genera un grafo interactivo.
100
+ - Retorna un informe en HTML, la figura del grafo y una tupla (interna) para el PDF.
101
  """
102
  tx_data = get_transaction(tx_id)
103
  if not tx_data:
 
112
  total_output_value = sum(out.get("value", 0) for out in tx_data.get("vout", [])) / 1e8
113
  fee = total_input_value - total_output_value
114
 
 
115
  heuristic_mixer = ((num_inputs > 5 and num_outputs > 5 and montos_unicos < 3) or
116
  (num_outputs > 2 and montos_unicos <= 2))
117
  es_mixer_blockchain = check_blockchain_tags(tx_id)
 
195
  )
196
  )
197
 
198
+ # Si se detecta mixer, se muestran métricas adicionales de outputs.
199
  if es_mixer:
200
  unique_outputs_dict = {}
201
  for out in tx_data.get("vout", []):
 
222
  fee_str = f"{fee:.8f} BTC"
223
  fee_str += f" (${fee * btc_price:,.2f})" if btc_price else ""
224
 
 
225
  reporte = f"""
226
  <div style="padding: 20px; border-radius: 10px;">
227
  <h3 style="color: {'#FF5252' if es_mixer else '#4CAF50'};">
 
246
  </p>
247
  </div>
248
  """
249
+ # La tupla interna que usaremos para el PDF es (reporte, fig)
250
  return reporte, fig, (reporte, fig)
251
 
252
  except Exception as e:
 
261
  """
262
  Genera un PDF que incluye:
263
  - Título: ANALISIS FORENSE DE BLOCKCHAIN
264
+ - El contenido del informe (como texto plano)
265
  - El grafo (convertido a imagen PNG)
266
  - Pie de página: "Generado por José R. Leonett"
267
  Retorna el PDF como bytes.
268
  """
269
+ # Guardar el grafo a imagen PNG en un archivo temporal
270
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
271
+ tmp_path = tmp.name
272
  try:
273
+ fig.write_image(tmp_path)
274
  except Exception as e:
275
  print("Error al guardar la imagen del grafo:", e)
276
+ tmp_path = None
277
 
278
+ # Extraer texto plano del informe (remover etiquetas HTML)
279
  plain_text = re.sub('<[^<]+?>', '', report)
280
 
281
  pdf = PDF()
282
  pdf.add_page()
 
283
  pdf.set_font("Arial", "B", 16)
284
  pdf.cell(0, 10, "ANALISIS FORENSE DE BLOCKCHAIN", ln=True, align="C")
285
  pdf.ln(5)
 
286
  pdf.set_font("Arial", "", 12)
287
  pdf.multi_cell(0, 10, plain_text)
288
  pdf.ln(5)
289
+ if tmp_path and os.path.exists(tmp_path):
 
290
  pdf.add_page()
291
+ pdf.image(tmp_path, x=10, y=20, w=190)
292
+ os.remove(tmp_path)
 
 
293
  pdf.set_font("Arial", "I", 10)
294
  pdf.ln(10)
295
  pdf.cell(0, 10, "Generado por José R. Leonett", ln=True, align="C")
 
299
 
300
  def descargar_pdf(analysis_tuple):
301
  """
302
+ Función para generar y retornar el PDF.
303
+ Si no hay datos en la tupla, retorna None.
 
304
  """
305
  if not analysis_tuple:
306
  return None
 
319
  gr.Markdown("**Nota:** Este analizador funciona únicamente con transacciones de Bitcoin. No se pueden analizar transacciones de Ethereum.")
320
 
321
  with gr.Row():
 
322
  with gr.Column(scale=1):
323
  plot_output = gr.Plot()
324
  tx_input = gr.Textbox(
 
334
  ],
335
  inputs=tx_input
336
  )
 
 
337
  with gr.Column(scale=1):
338
  reporte_html = gr.HTML()
339
  explanation_box = gr.HTML(value="""
 
347
  <li><strong>Peso:</strong> Peso de la transacción en unidades de peso.</li>
348
  <li><strong>Fee rate:</strong> Tarifa pagada por byte (sat/byte).</li>
349
  <li><strong>Inputs:</strong> Número de entradas de la transacción.</li>
350
+ <li><strong>Outputs:</strong> (Se muestra solo si se detecta posible mixer)</li>
351
+ <li><strong>Montos únicos en outputs:</strong> (Solo si es mixer)</li>
352
+ <li><strong>Detalles de outputs únicos:</strong> (Solo si es mixer)</li>
353
  <li><strong>Total Entradas:</strong> Suma total de los valores de entrada (en BTC y USD).</li>
354
  <li><strong>Total Salidas:</strong> Suma total de los valores de salida (en BTC y USD).</li>
355
+ <li><strong>Fee:</strong> Diferencia entre entradas y salidas (en BTC y USD).</li>
356
  <li><strong>Estado de Confirmación:</strong> Indica si la transacción está confirmada o no.</li>
357
  <li><strong>Alerta de Mixer / CoinJoin:</strong> Muestra si la transacción presenta patrones de mezcla.</li>
358
  </ul>
359
  </div>
360
  """)
361
 
362
+ # Estado para almacenar el análisis (tupla: (reporte, fig))
363
  analysis_state = gr.State()
364
+
 
365
  analyze_btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output, analysis_state])
366
 
367
+ # Botón de descarga (utilizamos gr.Download para que al hacer clic se inicie la descarga automáticamente)
368
+ download_btn = gr.Download(label="GENERAR ANALISIS EN PDF", elem_classes=["custom-btn"])
369
+ download_btn.click(fn=descargar_pdf, inputs=analysis_state, outputs="file")
 
370
 
371
  demo.launch()