Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -6,78 +6,80 @@ from datetime import datetime
|
|
6 |
|
7 |
def get_transaction(tx_id):
|
8 |
"""
|
9 |
-
|
10 |
-
|
11 |
"""
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
|
|
|
|
|
|
23 |
|
24 |
def generar_grafo(tx_data):
|
25 |
"""
|
26 |
Genera un grafo dirigido a partir de los datos de la transacción.
|
27 |
-
|
28 |
-
|
29 |
"""
|
30 |
G = nx.DiGraph()
|
31 |
|
32 |
txid = tx_data.get("txid", "desconocido")
|
33 |
-
G.add_node(txid, color="#FF6B6B", size=20, tipo="
|
34 |
|
35 |
-
#
|
36 |
for inp in tx_data.get("vin", []):
|
37 |
prevout = inp.get("prevout", {})
|
38 |
addr = prevout.get("scriptpubkey_address")
|
39 |
if addr:
|
40 |
-
G.add_node(addr, color="#4ECDC4", size=15, tipo="
|
41 |
G.add_edge(addr, txid)
|
42 |
|
43 |
-
#
|
44 |
for out in tx_data.get("vout", []):
|
45 |
addr = out.get("scriptpubkey_address")
|
46 |
if addr:
|
47 |
-
G.add_node(addr, color="#45B7D1", size=15, tipo="
|
48 |
G.add_edge(txid, addr)
|
49 |
|
50 |
return G
|
51 |
|
52 |
def analizar_transaccion(tx_id):
|
53 |
"""
|
54 |
-
Analiza una transacción
|
55 |
-
- Obtiene los datos
|
56 |
- Aplica una heurística simple para detectar posibles mixers.
|
57 |
-
- Genera un grafo interactivo
|
58 |
-
- Retorna un reporte en HTML
|
|
|
59 |
"""
|
60 |
tx_data = get_transaction(tx_id)
|
61 |
if not tx_data:
|
62 |
-
return "❌ Transacción no encontrada o error al obtener datos."
|
|
|
63 |
|
64 |
try:
|
65 |
-
# Heurística para detectar mixer
|
66 |
num_inputs = len(tx_data.get("vin", []))
|
67 |
num_outputs = len(tx_data.get("vout", []))
|
68 |
montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])]
|
69 |
montos_unicos = len(set(montos))
|
70 |
|
71 |
-
# Umbrales de detección (se pueden ajustar)
|
72 |
es_mixer = num_inputs > 5 and num_outputs > 5 and montos_unicos < 3
|
73 |
|
74 |
-
# Generar grafo
|
75 |
G = generar_grafo(tx_data)
|
76 |
-
|
77 |
-
# Calcular posición de los nodos (seed para consistencia)
|
78 |
pos = nx.spring_layout(G, seed=42)
|
79 |
|
80 |
-
#
|
81 |
edge_x, edge_y = [], []
|
82 |
for edge in G.edges():
|
83 |
x0, y0 = pos[edge[0]]
|
@@ -85,19 +87,17 @@ def analizar_transaccion(tx_id):
|
|
85 |
edge_x.extend([x0, x1, None])
|
86 |
edge_y.extend([y0, y1, None])
|
87 |
|
88 |
-
#
|
89 |
-
node_x, node_y = [], []
|
90 |
-
hover_texts, node_colors, node_sizes = [], [], []
|
91 |
for node in G.nodes():
|
92 |
x, y = pos[node]
|
93 |
node_x.append(x)
|
94 |
node_y.append(y)
|
95 |
tipo = G.nodes[node].get("tipo", "desconocido")
|
96 |
-
hover_texts.append(f"Tipo: {tipo}<br>ID: {node[:
|
97 |
node_colors.append(G.nodes[node].get("color", "#FFFFFF"))
|
98 |
node_sizes.append(G.nodes[node].get("size", 10))
|
99 |
|
100 |
-
# Crear figura interactiva con Plotly
|
101 |
fig = go.Figure(
|
102 |
data=[
|
103 |
go.Scatter(
|
@@ -110,13 +110,13 @@ def analizar_transaccion(tx_id):
|
|
110 |
x=node_x, y=node_y,
|
111 |
mode="markers+text",
|
112 |
marker=dict(color=node_colors, size=node_sizes, line_width=1),
|
113 |
-
text=[node[:
|
114 |
hovertext=hover_texts,
|
115 |
hoverinfo="text"
|
116 |
)
|
117 |
],
|
118 |
layout=go.Layout(
|
119 |
-
title="
|
120 |
showlegend=False,
|
121 |
margin=dict(b=0, l=0, r=0, t=40),
|
122 |
xaxis=dict(showgrid=False, zeroline=False, visible=False),
|
@@ -125,49 +125,55 @@ def analizar_transaccion(tx_id):
|
|
125 |
)
|
126 |
)
|
127 |
|
128 |
-
|
129 |
-
block_time =
|
130 |
fecha_str = datetime.fromtimestamp(block_time).strftime('%Y-%m-%d') if block_time else "Desconocida"
|
|
|
|
|
131 |
|
132 |
-
# Reporte en HTML con estilos
|
133 |
reporte = f"""
|
134 |
<div style="padding: 20px; background: #1a1a1a; border-radius: 10px; color: white;">
|
135 |
-
<h3 style="color: {'#
|
136 |
-
{'🔮 POSIBLE MIXER' if es_mixer else '✅
|
137 |
</h3>
|
|
|
138 |
<p>📥 Inputs: {num_inputs}</p>
|
139 |
<p>📤 Outputs: {num_outputs}</p>
|
140 |
<p>💰 Montos únicos: {montos_unicos}</p>
|
141 |
<p>📅 Fecha: {fecha_str}</p>
|
|
|
142 |
</div>
|
143 |
"""
|
144 |
|
145 |
return reporte, fig
|
146 |
-
|
147 |
except Exception as e:
|
148 |
return f"⚠️ Error durante el análisis: {str(e)}", None
|
149 |
|
150 |
-
|
151 |
-
|
152 |
-
gr.Markdown("
|
153 |
-
|
154 |
|
155 |
with gr.Row():
|
156 |
-
|
157 |
-
|
158 |
-
|
159 |
-
|
160 |
-
|
161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
162 |
|
163 |
btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output])
|
164 |
-
|
165 |
-
gr.Examples(
|
166 |
-
examples=[
|
167 |
-
["c86157c5d8f6a96d18d5a5f015a726f8e7a837d00cfe2a7dc4a68b2df671f9d7"],
|
168 |
-
["f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"]
|
169 |
-
],
|
170 |
-
inputs=tx_input
|
171 |
-
)
|
172 |
|
173 |
demo.launch()
|
|
|
6 |
|
7 |
def get_transaction(tx_id):
|
8 |
"""
|
9 |
+
Intenta obtener los datos de una transacción Bitcoin usando endpoints públicos.
|
10 |
+
Primero se prueba con Blockstream.info y, de fallar, con Mempool.space.
|
11 |
"""
|
12 |
+
urls = [
|
13 |
+
f"https://blockstream.info/api/tx/{tx_id}",
|
14 |
+
f"https://mempool.space/api/tx/{tx_id}"
|
15 |
+
]
|
16 |
+
for url in urls:
|
17 |
+
try:
|
18 |
+
response = requests.get(url, timeout=10)
|
19 |
+
if response.status_code == 200:
|
20 |
+
return response.json()
|
21 |
+
else:
|
22 |
+
print(f"Error en {url}: Código de estado {response.status_code}")
|
23 |
+
except Exception as e:
|
24 |
+
print(f"Excepción al consultar {url}: {e}")
|
25 |
+
return None
|
26 |
|
27 |
def generar_grafo(tx_data):
|
28 |
"""
|
29 |
Genera un grafo dirigido a partir de los datos de la transacción.
|
30 |
+
Nodo central: la transacción.
|
31 |
+
Nodos secundarios: direcciones de inputs y outputs.
|
32 |
"""
|
33 |
G = nx.DiGraph()
|
34 |
|
35 |
txid = tx_data.get("txid", "desconocido")
|
36 |
+
G.add_node(txid, color="#FF6B6B", size=20, tipo="Transacción")
|
37 |
|
38 |
+
# Inputs
|
39 |
for inp in tx_data.get("vin", []):
|
40 |
prevout = inp.get("prevout", {})
|
41 |
addr = prevout.get("scriptpubkey_address")
|
42 |
if addr:
|
43 |
+
G.add_node(addr, color="#4ECDC4", size=15, tipo="Input")
|
44 |
G.add_edge(addr, txid)
|
45 |
|
46 |
+
# Outputs
|
47 |
for out in tx_data.get("vout", []):
|
48 |
addr = out.get("scriptpubkey_address")
|
49 |
if addr:
|
50 |
+
G.add_node(addr, color="#45B7D1", size=15, tipo="Output")
|
51 |
G.add_edge(txid, addr)
|
52 |
|
53 |
return G
|
54 |
|
55 |
def analizar_transaccion(tx_id):
|
56 |
"""
|
57 |
+
Analiza una transacción Bitcoin:
|
58 |
+
- Obtiene los datos.
|
59 |
- Aplica una heurística simple para detectar posibles mixers.
|
60 |
+
- Genera un grafo interactivo.
|
61 |
+
- Retorna un reporte en HTML y la figura del grafo.
|
62 |
+
Nota: Este analizador solo funciona con transacciones de Bitcoin.
|
63 |
"""
|
64 |
tx_data = get_transaction(tx_id)
|
65 |
if not tx_data:
|
66 |
+
return ("❌ Transacción no encontrada o error al obtener datos. "
|
67 |
+
"Asegúrate de ingresar un TXID de Bitcoin válido."), None
|
68 |
|
69 |
try:
|
70 |
+
# Heurística para detectar mixer: muchos inputs/outputs y pocos montos únicos.
|
71 |
num_inputs = len(tx_data.get("vin", []))
|
72 |
num_outputs = len(tx_data.get("vout", []))
|
73 |
montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])]
|
74 |
montos_unicos = len(set(montos))
|
75 |
|
|
|
76 |
es_mixer = num_inputs > 5 and num_outputs > 5 and montos_unicos < 3
|
77 |
|
78 |
+
# Generar grafo
|
79 |
G = generar_grafo(tx_data)
|
|
|
|
|
80 |
pos = nx.spring_layout(G, seed=42)
|
81 |
|
82 |
+
# Datos de aristas
|
83 |
edge_x, edge_y = [], []
|
84 |
for edge in G.edges():
|
85 |
x0, y0 = pos[edge[0]]
|
|
|
87 |
edge_x.extend([x0, x1, None])
|
88 |
edge_y.extend([y0, y1, None])
|
89 |
|
90 |
+
# Datos de nodos
|
91 |
+
node_x, node_y, hover_texts, node_colors, node_sizes = [], [], [], [], []
|
|
|
92 |
for node in G.nodes():
|
93 |
x, y = pos[node]
|
94 |
node_x.append(x)
|
95 |
node_y.append(y)
|
96 |
tipo = G.nodes[node].get("tipo", "desconocido")
|
97 |
+
hover_texts.append(f"Tipo: {tipo}<br>ID: {node[:8]}...")
|
98 |
node_colors.append(G.nodes[node].get("color", "#FFFFFF"))
|
99 |
node_sizes.append(G.nodes[node].get("size", 10))
|
100 |
|
|
|
101 |
fig = go.Figure(
|
102 |
data=[
|
103 |
go.Scatter(
|
|
|
110 |
x=node_x, y=node_y,
|
111 |
mode="markers+text",
|
112 |
marker=dict(color=node_colors, size=node_sizes, line_width=1),
|
113 |
+
text=[node[:8] for node in G.nodes()],
|
114 |
hovertext=hover_texts,
|
115 |
hoverinfo="text"
|
116 |
)
|
117 |
],
|
118 |
layout=go.Layout(
|
119 |
+
title="Visualización de Conexiones de la Transacción",
|
120 |
showlegend=False,
|
121 |
margin=dict(b=0, l=0, r=0, t=40),
|
122 |
xaxis=dict(showgrid=False, zeroline=False, visible=False),
|
|
|
125 |
)
|
126 |
)
|
127 |
|
128 |
+
status = tx_data.get("status", {})
|
129 |
+
block_time = status.get("block_time")
|
130 |
fecha_str = datetime.fromtimestamp(block_time).strftime('%Y-%m-%d') if block_time else "Desconocida"
|
131 |
+
confirmed = status.get("confirmed", False)
|
132 |
+
estado_confirmacion = "Confirmada" if confirmed else "No confirmada"
|
133 |
|
|
|
134 |
reporte = f"""
|
135 |
<div style="padding: 20px; background: #1a1a1a; border-radius: 10px; color: white;">
|
136 |
+
<h3 style="color: {'#FF5252' if es_mixer else '#4CAF50'};">
|
137 |
+
{'🔮 POSIBLE MIXER' if es_mixer else '✅ Transacción Normal'}
|
138 |
</h3>
|
139 |
+
<p><strong>TXID:</strong> {tx_id}</p>
|
140 |
<p>📥 Inputs: {num_inputs}</p>
|
141 |
<p>📤 Outputs: {num_outputs}</p>
|
142 |
<p>💰 Montos únicos: {montos_unicos}</p>
|
143 |
<p>📅 Fecha: {fecha_str}</p>
|
144 |
+
<p>🔔 Estado: {estado_confirmacion}</p>
|
145 |
</div>
|
146 |
"""
|
147 |
|
148 |
return reporte, fig
|
149 |
+
|
150 |
except Exception as e:
|
151 |
return f"⚠️ Error durante el análisis: {str(e)}", None
|
152 |
|
153 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="🔍 Detector de Mixers en Transacciones Bitcoin") as demo:
|
154 |
+
gr.Markdown("# 🔍 Detector de Mixers en Transacciones Bitcoin")
|
155 |
+
gr.Markdown("**Nota:** Este analizador funciona únicamente con transacciones de Bitcoin. "
|
156 |
+
"No se pueden analizar transacciones de Ethereum.")
|
157 |
|
158 |
with gr.Row():
|
159 |
+
# Columna Izquierda: Entrada y Ejemplos
|
160 |
+
with gr.Column(scale=1):
|
161 |
+
tx_input = gr.Textbox(label="TXID de la Transacción",
|
162 |
+
placeholder="Ej: f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16")
|
163 |
+
btn = gr.Button("Analizar Transacción")
|
164 |
+
gr.Markdown("### Ejemplos de TXIDs válidos:")
|
165 |
+
gr.Examples(
|
166 |
+
examples=[
|
167 |
+
["f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"],
|
168 |
+
["7b67ce16ad1fd3b58030ebf5a5c21d41ef245a8f9f89db46d7f7df1ac857c2c7"]
|
169 |
+
],
|
170 |
+
inputs=tx_input
|
171 |
+
)
|
172 |
+
# Columna Derecha: Resultados
|
173 |
+
with gr.Column(scale=2):
|
174 |
+
reporte_html = gr.HTML()
|
175 |
+
plot_output = gr.Plot()
|
176 |
|
177 |
btn.click(fn=analizar_transaccion, inputs=tx_input, outputs=[reporte_html, plot_output])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
178 |
|
179 |
demo.launch()
|