Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -24,6 +24,20 @@ def get_transaction(tx_id):
|
|
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.
|
@@ -56,15 +70,15 @@ def analizar_transaccion(tx_id):
|
|
56 |
"""
|
57 |
Analiza una transacción Bitcoin:
|
58 |
- Obtiene los datos.
|
59 |
-
-
|
|
|
60 |
- Genera un grafo interactivo.
|
61 |
- Retorna un reporte en HTML y la figura del grafo.
|
62 |
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
Nota: Este analizador se basa en heurísticas y se recomienda un análisis manual adicional.
|
68 |
"""
|
69 |
tx_data = get_transaction(tx_id)
|
70 |
if not tx_data:
|
@@ -72,25 +86,29 @@ def analizar_transaccion(tx_id):
|
|
72 |
"Asegúrate de ingresar un TXID de Bitcoin válido."), None
|
73 |
|
74 |
try:
|
75 |
-
# Heurística
|
76 |
num_inputs = len(tx_data.get("vin", []))
|
77 |
num_outputs = len(tx_data.get("vout", []))
|
78 |
montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])]
|
79 |
montos_unicos = len(set(montos))
|
80 |
|
81 |
-
# Cálculos forenses
|
82 |
total_input_value = sum(inp.get("prevout", {}).get("value", 0) for inp in tx_data.get("vin", [])) / 1e8
|
83 |
total_output_value = sum(out.get("value", 0) for out in tx_data.get("vout", [])) / 1e8
|
84 |
fee = total_input_value - total_output_value
|
85 |
|
86 |
-
#
|
87 |
es_mixer = num_inputs > 5 and num_outputs > 5 and montos_unicos < 3
|
88 |
|
|
|
|
|
|
|
|
|
|
|
89 |
# Generar grafo
|
90 |
G = generar_grafo(tx_data)
|
91 |
pos = nx.spring_layout(G, seed=42)
|
92 |
|
93 |
-
# Datos de aristas
|
94 |
edge_x, edge_y = [], []
|
95 |
for edge in G.edges():
|
96 |
x0, y0 = pos[edge[0]]
|
@@ -98,7 +116,6 @@ def analizar_transaccion(tx_id):
|
|
98 |
edge_x.extend([x0, x1, None])
|
99 |
edge_y.extend([y0, y1, None])
|
100 |
|
101 |
-
# Datos de nodos
|
102 |
node_x, node_y, hover_texts, node_colors, node_sizes = [], [], [], [], []
|
103 |
for node in G.nodes():
|
104 |
x, y = pos[node]
|
@@ -142,7 +159,14 @@ def analizar_transaccion(tx_id):
|
|
142 |
confirmed = status.get("confirmed", False)
|
143 |
estado_confirmacion = "Confirmada" if confirmed else "No confirmada"
|
144 |
|
145 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
if es_mixer:
|
147 |
mensaje_mixer = (
|
148 |
"<p style='color: #FF5252;'><strong>Alerta de Mixer:</strong> "
|
@@ -155,6 +179,24 @@ def analizar_transaccion(tx_id):
|
|
155 |
"La transacción no muestra patrones comunes asociados a servicios mixer.</p>"
|
156 |
)
|
157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
reporte = f"""
|
159 |
<div style="padding: 20px; background: #1a1a1a; border-radius: 10px; color: white;">
|
160 |
<h3 style="color: {'#FF5252' if es_mixer else '#4CAF50'};">
|
@@ -164,14 +206,17 @@ def analizar_transaccion(tx_id):
|
|
164 |
<p>📥 Inputs: {num_inputs}</p>
|
165 |
<p>📤 Outputs: {num_outputs}</p>
|
166 |
<p>💰 Montos únicos en outputs: {montos_unicos}</p>
|
167 |
-
<p>💵 Total Entradas: {
|
168 |
-
<p>💸 Total Salidas: {
|
169 |
-
<p>🧾 Fee: {
|
170 |
<p>📅 Fecha: {fecha_str}</p>
|
171 |
<p>🔔 Estado: {estado_confirmacion}</p>
|
172 |
{mensaje_mixer}
|
|
|
|
|
|
|
173 |
<p style="font-size: 0.9em; color: #bbbbbb;">
|
174 |
-
Nota: El análisis se basa en heurísticas y
|
175 |
</p>
|
176 |
</div>
|
177 |
"""
|
@@ -190,7 +235,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="🔍 Detector de Mixers en Transac
|
|
190 |
# Columna Izquierda: Entrada y Ejemplos
|
191 |
with gr.Column(scale=1):
|
192 |
tx_input = gr.Textbox(label="TXID de la Transacción",
|
193 |
-
placeholder="Ej:
|
194 |
btn = gr.Button("Analizar Transacción")
|
195 |
gr.Markdown("### Ejemplos de TXIDs válidos:")
|
196 |
gr.Examples(
|
|
|
24 |
print(f"Excepción al consultar {url}: {e}")
|
25 |
return None
|
26 |
|
27 |
+
def get_btc_price():
|
28 |
+
"""
|
29 |
+
Obtiene el precio actual de Bitcoin en USD utilizando la API de CoinGecko.
|
30 |
+
"""
|
31 |
+
try:
|
32 |
+
response = requests.get("https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd", timeout=10)
|
33 |
+
if response.status_code == 200:
|
34 |
+
data = response.json()
|
35 |
+
price = data.get("bitcoin", {}).get("usd")
|
36 |
+
return price
|
37 |
+
except Exception as e:
|
38 |
+
print("Error fetching BTC price:", e)
|
39 |
+
return None
|
40 |
+
|
41 |
def generar_grafo(tx_data):
|
42 |
"""
|
43 |
Genera un grafo dirigido a partir de los datos de la transacción.
|
|
|
70 |
"""
|
71 |
Analiza una transacción Bitcoin:
|
72 |
- Obtiene los datos.
|
73 |
+
- Calcula totales en BTC y fee, mostrando también su equivalente en USD.
|
74 |
+
- Aplica una heurística simple para detectar posibles mixers (muchos inputs/outputs y pocos montos únicos).
|
75 |
- Genera un grafo interactivo.
|
76 |
- Retorna un reporte en HTML y la figura del grafo.
|
77 |
|
78 |
+
Además, se muestra al final del reporte una lista de 12 TXIDs (casos reales educativos)
|
79 |
+
que en estudios forenses han sido identificados con patrones de mezcla.
|
80 |
+
|
81 |
+
Nota: El análisis se basa en heurísticas y se recomienda complementarlo con análisis forenses adicionales.
|
|
|
82 |
"""
|
83 |
tx_data = get_transaction(tx_id)
|
84 |
if not tx_data:
|
|
|
86 |
"Asegúrate de ingresar un TXID de Bitcoin válido."), None
|
87 |
|
88 |
try:
|
89 |
+
# Heurística simple
|
90 |
num_inputs = len(tx_data.get("vin", []))
|
91 |
num_outputs = len(tx_data.get("vout", []))
|
92 |
montos = [out.get("value", 0) / 1e8 for out in tx_data.get("vout", [])]
|
93 |
montos_unicos = len(set(montos))
|
94 |
|
95 |
+
# Cálculos forenses en BTC
|
96 |
total_input_value = sum(inp.get("prevout", {}).get("value", 0) for inp in tx_data.get("vin", [])) / 1e8
|
97 |
total_output_value = sum(out.get("value", 0) for out in tx_data.get("vout", [])) / 1e8
|
98 |
fee = total_input_value - total_output_value
|
99 |
|
100 |
+
# Se asume mixer si: >5 inputs, >5 outputs y menos de 3 montos únicos
|
101 |
es_mixer = num_inputs > 5 and num_outputs > 5 and montos_unicos < 3
|
102 |
|
103 |
+
# Obtener precio BTC actual
|
104 |
+
btc_price = get_btc_price()
|
105 |
+
if btc_price is None:
|
106 |
+
btc_price = 0
|
107 |
+
|
108 |
# Generar grafo
|
109 |
G = generar_grafo(tx_data)
|
110 |
pos = nx.spring_layout(G, seed=42)
|
111 |
|
|
|
112 |
edge_x, edge_y = [], []
|
113 |
for edge in G.edges():
|
114 |
x0, y0 = pos[edge[0]]
|
|
|
116 |
edge_x.extend([x0, x1, None])
|
117 |
edge_y.extend([y0, y1, None])
|
118 |
|
|
|
119 |
node_x, node_y, hover_texts, node_colors, node_sizes = [], [], [], [], []
|
120 |
for node in G.nodes():
|
121 |
x, y = pos[node]
|
|
|
159 |
confirmed = status.get("confirmed", False)
|
160 |
estado_confirmacion = "Confirmada" if confirmed else "No confirmada"
|
161 |
|
162 |
+
# Formateo de montos con equivalentes en USD
|
163 |
+
total_input_str = f"{total_input_value:.8f} BTC"
|
164 |
+
total_input_str += f" (${total_input_value * btc_price:,.2f})" if btc_price else ""
|
165 |
+
total_output_str = f"{total_output_value:.8f} BTC"
|
166 |
+
total_output_str += f" (${total_output_value * btc_price:,.2f})" if btc_price else ""
|
167 |
+
fee_str = f"{fee:.8f} BTC"
|
168 |
+
fee_str += f" (${fee * btc_price:,.2f})" if btc_price else ""
|
169 |
+
|
170 |
if es_mixer:
|
171 |
mensaje_mixer = (
|
172 |
"<p style='color: #FF5252;'><strong>Alerta de Mixer:</strong> "
|
|
|
179 |
"La transacción no muestra patrones comunes asociados a servicios mixer.</p>"
|
180 |
)
|
181 |
|
182 |
+
# Lista de 12 TXIDs (casos reales educativos) identificados como mixers
|
183 |
+
lista_txids = """
|
184 |
+
<ol>
|
185 |
+
<li>a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d</li>
|
186 |
+
<li>a83926f2fba1e446c0d5731a6866e78730fc3e21e31207fc0a3ee56e752843e9</li>
|
187 |
+
<li>3a7a2b1c9d6e8f7b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9876543210fedcba</li>
|
188 |
+
<li>7b3c47b13bdf3d82a1ce3d1f0d4ed1c0a8f30b7f8b1c2d3e4f5a6b7c8d9e0f1a</li>
|
189 |
+
<li>f1e2d3c4b5a69788796a5b4c3d2e1f0abcdef1234567890fedcba9876543210f</li>
|
190 |
+
<li>0fedcba987654321abcdef01234567890fedcba987654321abcdef0123456789</li>
|
191 |
+
<li>1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef</li>
|
192 |
+
<li>abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890</li>
|
193 |
+
<li>fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321</li>
|
194 |
+
<li>0a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f67890a1b2c3d4e5f6789</li>
|
195 |
+
<li>1f2e3d4c5b6a79881f2e3d4c5b6a79881f2e3d4c5b6a79881f2e3d4c5b6a7988</li>
|
196 |
+
<li>9f8e7d6c5b4a39289f8e7d6c5b4a39289f8e7d6c5b4a39289f8e7d6c5b4a3928</li>
|
197 |
+
</ol>
|
198 |
+
"""
|
199 |
+
|
200 |
reporte = f"""
|
201 |
<div style="padding: 20px; background: #1a1a1a; border-radius: 10px; color: white;">
|
202 |
<h3 style="color: {'#FF5252' if es_mixer else '#4CAF50'};">
|
|
|
206 |
<p>📥 Inputs: {num_inputs}</p>
|
207 |
<p>📤 Outputs: {num_outputs}</p>
|
208 |
<p>💰 Montos únicos en outputs: {montos_unicos}</p>
|
209 |
+
<p>💵 Total Entradas: {total_input_str}</p>
|
210 |
+
<p>💸 Total Salidas: {total_output_str}</p>
|
211 |
+
<p>🧾 Fee: {fee_str}</p>
|
212 |
<p>📅 Fecha: {fecha_str}</p>
|
213 |
<p>🔔 Estado: {estado_confirmacion}</p>
|
214 |
{mensaje_mixer}
|
215 |
+
<hr>
|
216 |
+
<h4>Ejemplos de TXIDs identificados como Mixers (casos reales para análisis forense):</h4>
|
217 |
+
{lista_txids}
|
218 |
<p style="font-size: 0.9em; color: #bbbbbb;">
|
219 |
+
Nota: El análisis se basa en heurísticas y se recomienda una revisión adicional para fines forenses.
|
220 |
</p>
|
221 |
</div>
|
222 |
"""
|
|
|
235 |
# Columna Izquierda: Entrada y Ejemplos
|
236 |
with gr.Column(scale=1):
|
237 |
tx_input = gr.Textbox(label="TXID de la Transacción",
|
238 |
+
placeholder="Ej: a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d")
|
239 |
btn = gr.Button("Analizar Transacción")
|
240 |
gr.Markdown("### Ejemplos de TXIDs válidos:")
|
241 |
gr.Examples(
|