
Evolución del Listener de Pagos Blockchain
En el desarrollo de mi marketplace con pagos en criptomonedas, uno de los mayores retos ha sido garantizar que ningún pago se pierda, incluso si el usuario abandona la página antes de que la transacción se confirme en la blockchain. La solución: un listener de eventos mejorado que actúa como guardián en segundo plano, asegurando que todas las transacciones se registren correctamente.
El Problema con el Enfoque Anterior
Mi primer listener (versión anterior) presentaba varias limitaciones:
-
Dependencia crítica del hash de transacción:
El sistema esperaba a recibir eltransactionHash
desde el frontend para registrar la transacción en la base de datos. Esto creaba una ventana de vulnerabilidad:- Si el usuario cerraba la pestaña o perdía conexión, la transacción no se guardaba en el servidor (aunque sí en blockchain).
# Versión antigua - Búsqueda solo por hash tx = await sync_to_async(Transaction.objects.get)(transaction_hash=tx_hash)
-
Estados ambiguos:
Las transacciones no encontradas quedaban en un limbo sin clasificar (ni confirmadas ni fallidas).
La Solución: Un Listener Robustecido
Cambios Clave Implementados
-
Registro anticipado con estado
pending
Ahora, la transacción se crea en la base de datos inmediatamente cuando el usuario firma en su wallet, antes de que se confirme en blockchain. Esto elimina la ventana de vulnerabilidad. -
Identificación por transactionId + wallet
Modifiqué el smart contract para que el eventoPaymentReceived
incluya:transactionId
: ID único de la transacción en nuestra DB.sender
: Dirección de la wallet del usuario.
# Versión nueva - Datos del evento event = contract.events.PaymentReceived().process_log(log) id = event['args']['transactionId'] sender_address = event['args']['sender'].lower()
-
Marcado automático de transacciones fallidas
Si después de 5 reintentos no se encuentra la transacción en blockchain, se marca comofailed
:if attempt == max_retries - 1: tx_failed.status = 'failed' # Cambio de estado crítico logger.error(f"Transacción {id} marcada como FAILED")
-
Búsqueda insensible a mayúsculas
Para evitar errores con formatos de dirección (ej:0xABC...
vs0xabc...
):wallet_address__iexact=sender_address # Versión nueva
Flujo Mejorado (vs Antiguo)
Etapa | Versión Antigua | Versión Nueva |
---|---|---|
Registro inicial | Esperaba al hash de blockchain | Se crea con estado pending inmediatamente |
Identificación | Solo por hash de transacción | Por transactionId + wallet_address |
Manejo de errores | Transacción “perdida” | Marcado automático como failed |
Resiliencia | Sin reintentos inteligentes | Backoff exponencial + 5 reintentos |
Beneficios Clave para Usuarios y Negocio
-
Protección contra pérdidas
- Ahora es irrelevante si el usuario cierra el navegador: el listener sigue trabajando.
-
Estados claros
- Los usuarios ven si su pago está
pending
,confirmed
ofailed
.
- Los usuarios ven si su pago está
-
Soporte técnico optimizado
- Si una transacción aparece como
failed
pero el usuario ve el débito:# Ejemplo de búsqueda manual para soporte Transaction.objects.filter(id=id, wallet_address__iexact=sender_address)
- El equipo puede verificar manualmente en la blockchain.
- Si una transacción aparece como
-
Arquitectura portable
Este patrón funciona para cualquier aplicación con pagos on-chain (no solo Django).
Implementación Técnica
El listener se ejecuta como un comando de Django:
python manage.py listener
Estructura de archivos:
mi_app/
├─ management/
│ ├─ commands/
│ │ └─ listener.py # Código completo
Conclusión y Mejoras Futuras
Este nuevo diseño resuelve los puntos débiles de la versión original priorizando:
- Seguridad: No se pierden transacciones válidas.
- Experiencia de usuario: Estados claros y automáticos.
- Mantenibilidad: Código más robusto y portable.
¿Es perfecto? Probablemente no. Por eso:
- Invitación a colaborar: Si ves oportunidades de mejora, ¡házmelo saber!
- Compromiso de transparencia: Publicaré actualizaciones en este blog y LinkedIn.