Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
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 |
-
#
|
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 |
-
#
|
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
|
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
|
273 |
-
|
|
|
274 |
try:
|
275 |
-
fig.write_image(
|
276 |
except Exception as e:
|
277 |
print("Error al guardar la imagen del grafo:", e)
|
278 |
-
|
279 |
|
280 |
-
# Extraer texto plano del informe (
|
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 |
-
|
294 |
-
if temp_img_path and os.path.exists(temp_img_path):
|
295 |
pdf.add_page()
|
296 |
-
|
297 |
-
|
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
|
310 |
-
|
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>
|
362 |
-
<li><strong>Montos únicos en outputs:</strong>
|
363 |
-
<li><strong>Detalles de outputs únicos:</strong>
|
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
|
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
|
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
|
380 |
-
download_btn = gr.
|
381 |
-
|
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()
|