Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -7,14 +7,14 @@ import re
|
|
7 |
import os
|
8 |
import tempfile
|
9 |
import base64
|
10 |
-
from fpdf import FPDF, HTMLMixin
|
11 |
|
12 |
# ---------------- Funciones de an谩lisis y grafo -------------------
|
13 |
|
14 |
def get_transaction(tx_id):
|
15 |
"""
|
16 |
-
|
17 |
-
|
18 |
"""
|
19 |
urls = [
|
20 |
f"https://blockstream.info/api/tx/{tx_id}",
|
@@ -70,8 +70,8 @@ def generar_grafo(tx_data):
|
|
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 |
-
|
74 |
-
(Esta funci贸n es un ejemplo y
|
75 |
"""
|
76 |
url = f"https://blockchain.info/rawtx/{tx_id}?format=json"
|
77 |
try:
|
@@ -96,9 +96,9 @@ def analizar_transaccion(tx_id):
|
|
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
|
100 |
- Genera un grafo interactivo.
|
101 |
-
- Retorna un informe en HTML, la figura del grafo y una tupla (reporte, fig)
|
102 |
"""
|
103 |
tx_data = get_transaction(tx_id)
|
104 |
if not tx_data:
|
@@ -113,6 +113,7 @@ 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 |
heuristic_mixer = ((num_inputs > 5 and num_outputs > 5 and montos_unicos < 3) or
|
117 |
(num_outputs > 2 and montos_unicos <= 2))
|
118 |
es_mixer_blockchain = check_blockchain_tags(tx_id)
|
@@ -246,73 +247,23 @@ def analizar_transaccion(tx_id):
|
|
246 |
</p>
|
247 |
</div>
|
248 |
"""
|
|
|
249 |
return reporte, fig, (reporte, fig)
|
250 |
|
251 |
except Exception as e:
|
252 |
return f"鈿狅笍 Error durante el an谩lisis: {str(e)}", None, None
|
253 |
|
254 |
-
# ---------------- Funci贸n para
|
255 |
|
256 |
-
|
257 |
-
pass
|
258 |
-
|
259 |
-
def generar_pdf(report, fig):
|
260 |
-
"""
|
261 |
-
Genera un PDF que incluye:
|
262 |
-
- T铆tulo: ANALISIS FORENSE DE BLOCKCHAIN
|
263 |
-
- El contenido del informe (como texto plano)
|
264 |
-
- El grafo (convertido a imagen PNG)
|
265 |
-
- Pie de p谩gina: "Generado por Jos茅 R. Leonett"
|
266 |
-
Retorna el PDF como bytes.
|
267 |
-
"""
|
268 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
|
269 |
-
tmp_path = tmp.name
|
270 |
-
try:
|
271 |
-
fig.write_image(tmp_path)
|
272 |
-
except Exception as e:
|
273 |
-
print("Error al guardar la imagen del grafo:", e)
|
274 |
-
tmp_path = None
|
275 |
-
|
276 |
-
plain_text = re.sub('<[^<]+?>', '', report)
|
277 |
-
|
278 |
-
pdf = PDF()
|
279 |
-
pdf.add_page()
|
280 |
-
pdf.set_font("Arial", "B", 16)
|
281 |
-
pdf.cell(0, 10, "ANALISIS FORENSE DE BLOCKCHAIN", ln=True, align="C")
|
282 |
-
pdf.ln(5)
|
283 |
-
pdf.set_font("Arial", "", 12)
|
284 |
-
pdf.multi_cell(0, 10, plain_text)
|
285 |
-
pdf.ln(5)
|
286 |
-
if tmp_path and os.path.exists(tmp_path):
|
287 |
-
pdf.add_page()
|
288 |
-
pdf.image(tmp_path, x=10, y=20, w=190)
|
289 |
-
os.remove(tmp_path)
|
290 |
-
pdf.set_font("Arial", "I", 10)
|
291 |
-
pdf.ln(10)
|
292 |
-
pdf.cell(0, 10, "Generado por Jos茅 R. Leonett", ln=True, align="C")
|
293 |
-
|
294 |
-
pdf_bytes = pdf.output(dest="S").encode("latin1")
|
295 |
-
return pdf_bytes
|
296 |
-
|
297 |
-
def descargar_pdf(analysis_tuple):
|
298 |
"""
|
299 |
-
|
|
|
300 |
"""
|
301 |
if not analysis_tuple:
|
302 |
-
return "<p>No hay an谩lisis generado.</p>"
|
303 |
-
report,
|
304 |
-
|
305 |
-
pdf_b64 = base64.b64encode(pdf_bytes).decode("utf-8")
|
306 |
-
html_download = f'''
|
307 |
-
<html>
|
308 |
-
<body>
|
309 |
-
<a id="downloadLink" href="data:application/pdf;base64,{pdf_b64}" download="analisis.pdf" style="display:none;"></a>
|
310 |
-
<script>document.getElementById("downloadLink").click();</script>
|
311 |
-
<p>Si no se descarga autom谩ticamente, <a href="data:application/pdf;base64,{pdf_b64}" download="analisis.pdf">haz clic aqu铆</a>.</p>
|
312 |
-
</body>
|
313 |
-
</html>
|
314 |
-
'''
|
315 |
-
return html_download
|
316 |
|
317 |
# ---------------- INTERFAZ GR脕FICA CON GRADIO -------------------
|
318 |
|
@@ -321,18 +272,18 @@ with gr.Blocks(
|
|
321 |
title="馃攳 Detector de Mixers / CoinJoin en Transacciones Bitcoin",
|
322 |
css=".custom-btn { background-color: #ADD8E6 !important; color: black !important; padding: 5px 10px; font-size: 12px; }"
|
323 |
) as demo:
|
|
|
324 |
gr.Markdown("# 馃攳 Detector de Mixers / CoinJoin en Transacciones Bitcoin")
|
325 |
gr.Markdown("Desarrollado por Jos茅 R. Leonett para la comunidad de Peritos Forenses Digitales de Guatemala [www.forensedigital.gt](http://www.forensedigital.gt)")
|
326 |
gr.Markdown("**Nota:** Este analizador funciona 煤nicamente con transacciones de Bitcoin. No se pueden analizar transacciones de Ethereum.")
|
327 |
|
328 |
with gr.Row():
|
|
|
329 |
with gr.Column(scale=1):
|
330 |
-
plot_output = gr.Plot()
|
331 |
tx_input = gr.Textbox(
|
332 |
label="TXID de la Transacci贸n",
|
333 |
placeholder="Ej: 9dd51e2d45f4f7bddcc3f0f7a05c3fd60543a11cfc9fbd0e1ca4434668cfa3e1"
|
334 |
)
|
335 |
-
analyze_btn = gr.Button("Analizar Transacci贸n", elem_classes=["custom-btn"])
|
336 |
gr.Markdown("### Ejemplos de TXIDs v谩lidos:")
|
337 |
gr.Examples(
|
338 |
examples=[
|
@@ -341,11 +292,7 @@ with gr.Blocks(
|
|
341 |
],
|
342 |
inputs=tx_input
|
343 |
)
|
344 |
-
|
345 |
-
reporte_html = gr.HTML()
|
346 |
-
pdf_download_btn = gr.Button("GENERAR ANALISIS EN PDF", elem_classes=["custom-btn"])
|
347 |
-
download_html = gr.HTML()
|
348 |
-
# Se ampli贸 el scroll de explicaci贸n a 350px de alto para ver mejor el contenido
|
349 |
explanation_box = gr.HTML(value="""
|
350 |
<div style="overflow-y: auto; height: 350px; border: 1px solid #cccccc; padding: 10px;">
|
351 |
<h4>Explicaci贸n de los campos:</h4>
|
@@ -357,8 +304,8 @@ with gr.Blocks(
|
|
357 |
<li><strong>Peso:</strong> Peso de la transacci贸n en unidades de peso.</li>
|
358 |
<li><strong>Fee rate:</strong> Tarifa pagada por byte (sat/byte).</li>
|
359 |
<li><strong>Inputs:</strong> N煤mero de entradas de la transacci贸n.</li>
|
360 |
-
<li><strong>Outputs:</strong> Se
|
361 |
-
<li><strong>Montos 煤nicos en outputs:</strong> Se
|
362 |
<li><strong>Detalles de outputs 煤nicos:</strong> Lista de cada monto (en BTC) y las direcciones asociadas (solo si es mixer).</li>
|
363 |
<li><strong>Total Entradas:</strong> Suma total de los valores de entrada (en BTC y USD).</li>
|
364 |
<li><strong>Total Salidas:</strong> Suma total de los valores de salida (en BTC y USD).</li>
|
@@ -368,11 +315,24 @@ with gr.Blocks(
|
|
368 |
</ul>
|
369 |
</div>
|
370 |
""")
|
|
|
|
|
|
|
|
|
|
|
371 |
|
372 |
# Estado para almacenar el an谩lisis (tupla: (reporte, fig))
|
373 |
analysis_state = gr.State()
|
374 |
|
|
|
375 |
analyze_btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output, analysis_state])
|
376 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
377 |
|
378 |
demo.launch()
|
|
|
7 |
import os
|
8 |
import tempfile
|
9 |
import base64
|
10 |
+
from fpdf import FPDF, HTMLMixin # Estos m贸dulos ya no se usan en este ejemplo
|
11 |
|
12 |
# ---------------- Funciones de an谩lisis y grafo -------------------
|
13 |
|
14 |
def get_transaction(tx_id):
|
15 |
"""
|
16 |
+
Obtiene datos de una transacci贸n Bitcoin usando endpoints p煤blicos.
|
17 |
+
Se prueba primero con Blockstream.info y, de fallar, con mempool.space.
|
18 |
"""
|
19 |
urls = [
|
20 |
f"https://blockstream.info/api/tx/{tx_id}",
|
|
|
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 |
+
indica que la transacci贸n ha sido etiquetada como MIXER.
|
74 |
+
(Esta funci贸n es un ejemplo y puede requerir ajustes seg煤n la respuesta real.)
|
75 |
"""
|
76 |
url = f"https://blockchain.info/rawtx/{tx_id}?format=json"
|
77 |
try:
|
|
|
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 de blockchain.com (etiqueta MIXER).
|
100 |
- Genera un grafo interactivo.
|
101 |
+
- Retorna un informe en HTML, la figura del grafo y una tupla (reporte, fig) que se guarda en el estado.
|
102 |
"""
|
103 |
tx_data = get_transaction(tx_id)
|
104 |
if not tx_data:
|
|
|
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)
|
|
|
247 |
</p>
|
248 |
</div>
|
249 |
"""
|
250 |
+
# Se devuelve adem谩s la tupla (reporte, fig) para almacenarla en el estado.
|
251 |
return reporte, fig, (reporte, fig)
|
252 |
|
253 |
except Exception as e:
|
254 |
return f"鈿狅笍 Error durante el an谩lisis: {str(e)}", None, None
|
255 |
|
256 |
+
# ---------------- Funci贸n para mostrar el modal -------------------
|
257 |
|
258 |
+
def mostrar_modal(analysis_tuple):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
259 |
"""
|
260 |
+
Funci贸n que, a partir de la tupla de an谩lisis, devuelve la actualizaci贸n para
|
261 |
+
abrir el modal y el contenido HTML del informe.
|
262 |
"""
|
263 |
if not analysis_tuple:
|
264 |
+
return gr.update(visible=False), "<p>No hay an谩lisis generado.</p>"
|
265 |
+
report, _ = analysis_tuple
|
266 |
+
return gr.update(visible=True), report
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
267 |
|
268 |
# ---------------- INTERFAZ GR脕FICA CON GRADIO -------------------
|
269 |
|
|
|
272 |
title="馃攳 Detector de Mixers / CoinJoin en Transacciones Bitcoin",
|
273 |
css=".custom-btn { background-color: #ADD8E6 !important; color: black !important; padding: 5px 10px; font-size: 12px; }"
|
274 |
) as demo:
|
275 |
+
# Encabezado
|
276 |
gr.Markdown("# 馃攳 Detector de Mixers / CoinJoin en Transacciones Bitcoin")
|
277 |
gr.Markdown("Desarrollado por Jos茅 R. Leonett para la comunidad de Peritos Forenses Digitales de Guatemala [www.forensedigital.gt](http://www.forensedigital.gt)")
|
278 |
gr.Markdown("**Nota:** Este analizador funciona 煤nicamente con transacciones de Bitcoin. No se pueden analizar transacciones de Ethereum.")
|
279 |
|
280 |
with gr.Row():
|
281 |
+
# Columna izquierda: TXID, Ejemplos y Explicaci贸n
|
282 |
with gr.Column(scale=1):
|
|
|
283 |
tx_input = gr.Textbox(
|
284 |
label="TXID de la Transacci贸n",
|
285 |
placeholder="Ej: 9dd51e2d45f4f7bddcc3f0f7a05c3fd60543a11cfc9fbd0e1ca4434668cfa3e1"
|
286 |
)
|
|
|
287 |
gr.Markdown("### Ejemplos de TXIDs v谩lidos:")
|
288 |
gr.Examples(
|
289 |
examples=[
|
|
|
292 |
],
|
293 |
inputs=tx_input
|
294 |
)
|
295 |
+
# Recuadro de explicaci贸n (scroll ampliado a 350px)
|
|
|
|
|
|
|
|
|
296 |
explanation_box = gr.HTML(value="""
|
297 |
<div style="overflow-y: auto; height: 350px; border: 1px solid #cccccc; padding: 10px;">
|
298 |
<h4>Explicaci贸n de los campos:</h4>
|
|
|
304 |
<li><strong>Peso:</strong> Peso de la transacci贸n en unidades de peso.</li>
|
305 |
<li><strong>Fee rate:</strong> Tarifa pagada por byte (sat/byte).</li>
|
306 |
<li><strong>Inputs:</strong> N煤mero de entradas de la transacci贸n.</li>
|
307 |
+
<li><strong>Outputs:</strong> Se muestran solo si se detecta posible mixer.</li>
|
308 |
+
<li><strong>Montos 煤nicos en outputs:</strong> Se muestran solo si es mixer.</li>
|
309 |
<li><strong>Detalles de outputs 煤nicos:</strong> Lista de cada monto (en BTC) y las direcciones asociadas (solo si es mixer).</li>
|
310 |
<li><strong>Total Entradas:</strong> Suma total de los valores de entrada (en BTC y USD).</li>
|
311 |
<li><strong>Total Salidas:</strong> Suma total de los valores de salida (en BTC y USD).</li>
|
|
|
315 |
</ul>
|
316 |
</div>
|
317 |
""")
|
318 |
+
# Columna derecha: Grafo y Reporte
|
319 |
+
with gr.Column(scale=1):
|
320 |
+
plot_output = gr.Plot()
|
321 |
+
reporte_html = gr.HTML()
|
322 |
+
analyze_btn = gr.Button("Analizar Transacci贸n", elem_classes=["custom-btn"])
|
323 |
|
324 |
# Estado para almacenar el an谩lisis (tupla: (reporte, fig))
|
325 |
analysis_state = gr.State()
|
326 |
|
327 |
+
# Al hacer clic en "Analizar Transacci贸n", se actualizan el reporte, el grafo y se guarda el an谩lisis en el estado.
|
328 |
analyze_btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output, analysis_state])
|
329 |
+
|
330 |
+
# Bot贸n para generar el an谩lisis en un modal emergente.
|
331 |
+
modal_btn = gr.Button("GENERAR ANALISIS", elem_classes=["custom-btn"])
|
332 |
+
with gr.Modal("Resultados del An谩lisis", visible=False) as result_modal:
|
333 |
+
modal_report = gr.HTML()
|
334 |
+
|
335 |
+
# La funci贸n mostrar_modal abrir谩 el modal con el contenido del an谩lisis.
|
336 |
+
modal_btn.click(fn=mostrar_modal, inputs=analysis_state, outputs=[result_modal, modal_report])
|
337 |
|
338 |
demo.launch()
|