Blockchain Monitors - Detectores de Transacciones
Los Blockchain Monitors son servicios especializados que escuchan constantemente las blockchains para detectar cuando llega dinero a las wallets de los usuarios.
¿Por qué existen?
Problema: ¿Cómo sabe el sistema cuando un usuario recibe criptomonedas?
Sin monitors: El usuario tendría que refrescar manualmente para ver su balance.
Arquitectura Común
Todos los monitors siguen el mismo patrón:
Componentes compartidos:
- Polling Loop: Consulta blockchain periódicamente
- Address Watching: Lista de addresses a monitorear
- Transaction Detection: Detecta txs relevantes
- Persistence: Guarda estado y transacciones
- Notification: Avisa a usuario vía WebSocket
🟣 Monitor Solana
Monitor de Solana
Blockchain: Solana Mainnet
RPC: Ankr, QuickNode
Intervalo: 30 segundos
Modelo: Slots (no bloques tradicionales)
Peculiaridades de Solana
- Slots: Solana usa "slots" (~400ms cada uno) en lugar de bloques
- Finality: 32 slots para confirmación final
- Velocidad: ~150 slots por minuto
- Tokens: SOL (nativo) + SPL tokens
Funcionamiento
Detección de Transferencias
SOL (nativo):
function detectSOLTransfer(transaction: Transaction) {
const instructions = transaction.instructions;
for (const ix of instructions) {
if (ix.programId.equals(SystemProgram.programId)) {
// Transfer SOL
const { source, destination, lamports } = decodeTransfer(ix);
if (isMonitoredAddress(destination)) {
return {
coin: 'SOL',
from: source,
to: destination,
amount: lamports / 1e9 // Lamports to SOL
};
}
}
}
}
SPL Tokens:
function detectSPLTransfer(transaction: Transaction) {
for (const ix of transaction.instructions) {
if (ix.programId.equals(TOKEN_PROGRAM_ID)) {
const { source, destination, amount, mint } = decodeTokenTransfer(ix);
if (isMonitoredAddress(destination)) {
const tokenInfo = await getTokenInfo(mint);
return {
coin: tokenInfo.symbol, // USDC, USDT, etc.
from: source,
to: destination,
amount: amount / (10 ** tokenInfo.decimals)
};
}
}
}
}
Optimizaciones
- Procesamiento en lotes: 5 slots por ciclo
- Cache de addresses: Redis para addresses monitoreadas
- Parseo eficiente: Solo parsea txs relevantes
⚫ Monitor Ethereum
Monitor de Ethereum
Blockchain: Ethereum Mainnet
RPC: Infura, Alchemy
Intervalo: 15 segundos (tiempo de bloque)
Tokens: ETH + ERC-20
Funcionamiento
async monitorEthereum() {
const lastBlock = await this.getLastProcessedBlock();
const currentBlock = await this.web3.eth.getBlockNumber();
for (let blockNum = lastBlock + 1; blockNum <= currentBlock; blockNum++) {
const block = await this.web3.eth.getBlock(blockNum, true);
for (const tx of block.transactions) {
// ETH nativo
if (this.isMonitoredAddress(tx.to)) {
await this.saveTransaction({
coin: 'ETH',
from: tx.from,
to: tx.to,
amount: this.web3.utils.fromWei(tx.value, 'ether'),
txHash: tx.hash,
block: blockNum
});
}
// ERC-20 tokens
if (tx.to === this.isERC20Contract(tx.to)) {
const transfer = this.parseERC20Transfer(tx.input);
if (this.isMonitoredAddress(transfer.to)) {
await this.saveTransaction({
coin: await this.getTokenSymbol(tx.to),
from: transfer.from,
to: transfer.to,
amount: transfer.amount,
txHash: tx.hash,
block: blockNum
});
}
}
}
await this.updateLastBlock(blockNum);
}
}
Detección ERC-20
Los transfers de tokens ERC-20 emiten un evento Transfer:
event Transfer(address indexed from, address indexed to, uint256 value);
// Parsear logs del evento Transfer
function parseERC20Transfer(logs: Log[]) {
for (const log of logs) {
if (log.topics[0] === TRANSFER_EVENT_SIGNATURE) {
const from = '0x' + log.topics[1].slice(26);
const to = '0x' + log.topics[2].slice(26);
const value = web3.utils.hexToNumber(log.data);
if (isMonitoredAddress(to)) {
return { from, to, value };
}
}
}
}
🟡 Monitor BSC (Binance Smart Chain)
Monitor de BSC
Blockchain: BSC Mainnet
RPC: BSC Official, Ankr
Intervalo: 3 segundos
Tokens: BNB + BEP-20
Características
BSC es un fork de Ethereum, por lo que:
- ✅ Misma arquitectura que Ethereum Monitor
- ✅ Compatible con Web3.js
- ✅ Mismo formato de addresses
- ✅ Eventos ERC-20 = BEP-20
Diferencias:
- ⚡ Bloques más rápidos (3s vs 15s)
- 💰 Fees más bajos
- 🔄 Polling más frecuente
🔴 Monitor Tron
Monitor de Tron
Blockchain: Tron Mainnet
RPC: TronGrid
Intervalo: 3 segundos
Tokens: TRX + TRC-20
Peculiaridades de Tron
- Addresses: Base58 en lugar de hex (Txxxx...)
- Energy: Sistema de energy/bandwidth en lugar de gas
- Formato diferente: TronWeb en lugar de Web3
Funcionamiento
async monitorTron() {
const lastBlock = await this.getLastProcessedBlock();
const currentBlock = await this.tronWeb.trx.getCurrentBlock();
for (let blockNum = lastBlock + 1; blockNum <= currentBlock.block_header.raw_data.number; blockNum++) {
const block = await this.tronWeb.trx.getBlockByNumber(blockNum);
for (const tx of block.transactions) {
// TRX nativo
if (tx.raw_data.contract[0].type === 'TransferContract') {
const to = this.tronWeb.address.fromHex(tx.raw_data.contract[0].parameter.value.to_address);
if (this.isMonitoredAddress(to)) {
await this.saveTransaction({
coin: 'TRX',
from: this.tronWeb.address.fromHex(tx.raw_data.contract[0].parameter.value.owner_address),
to: to,
amount: tx.raw_data.contract[0].parameter.value.amount / 1e6,
txHash: tx.txID
});
}
}
// TRC-20 tokens (similar a ERC-20)
if (tx.raw_data.contract[0].type === 'TriggerSmartContract') {
const transfer = this.parseTRC20Transfer(tx);
if (transfer && this.isMonitoredAddress(transfer.to)) {
await this.saveTransaction(transfer);
}
}
}
}
}
🟠 Monitor Bitcoin
Monitor de Bitcoin
Blockchain: Bitcoin Mainnet
RPC: BlockCypher, Blockchain.info
Intervalo: 10 minutos (tiempo de bloque)
Modelo: UTXO
Peculiaridades de Bitcoin
Bitcoin usa el modelo UTXO (Unspent Transaction Output), completamente diferente al modelo de cuentas de Ethereum.
Modelo de cuentas (Ethereum):
Account A: 10 ETH
Account B: 5 ETH
Modelo UTXO (Bitcoin):
UTXO1: 0.5 BTC (pertenece a Address A)
UTXO2: 1.2 BTC (pertenece a Address A)
UTXO3: 0.3 BTC (pertenece a Address B)
Funcionamiento
async monitorBitcoin() {
const lastBlock = await this.getLastProcessedBlock();
const currentBlock = await this.rpc.getBlockCount();
for (let height = lastBlock + 1; height <= currentBlock; height++) {
const blockHash = await this.rpc.getBlockHash(height);
const block = await this.rpc.getBlock(blockHash, 2); // verbosity 2 para txs completas
for (const tx of block.tx) {
// Revisar outputs (vouts)
for (const vout of tx.vout) {
const addresses = vout.scriptPubKey.addresses || [];
for (const address of addresses) {
if (this.isMonitoredAddress(address)) {
await this.saveTransaction({
coin: 'BTC',
to: address,
amount: vout.value, // Ya en BTC
txHash: tx.txid,
vout: vout.n // Índice del output
});
}
}
}
}
await this.updateLastBlock(height);
}
}
Confirmaciones
Bitcoin requiere múltiples confirmaciones para considerar una transacción segura:
const CONFIRMATIONS_REQUIRED = 6;
async checkConfirmations(txHash: string) {
const tx = await this.rpc.getTransaction(txHash);
if (tx.confirmations >= CONFIRMATIONS_REQUIRED) {
// Transacción confirmada
await this.updateTxStatus(txHash, 'confirmed');
await this.notifyUser(tx.address, {
type: 'transaction_confirmed',
txHash,
confirmations: tx.confirmations
});
}
}
Estrategias de Optimización
1. Address Caching
Implementación:
async isMonitoredAddress(address: string): Promise<boolean> {
// Primero buscar en cache (ultra-rápido)
const cached = await this.redis.sismember('monitored_addresses', address);
if (cached) return true;
// Si no está en cache, buscar en DB
const exists = await this.db.wallets.findOne({ address });
if (exists) {
// Agregar a cache para próximas búsquedas
await this.redis.sadd('monitored_addresses', address);
return true;
}
return false;
}
2. Batch Processing
En lugar de procesar bloque por bloque:
// ❌ Lento
for (let i = lastBlock; i <= currentBlock; i++) {
await processBlock(i);
}
// ✅ Rápido
const BATCH_SIZE = 10;
for (let i = lastBlock; i <= currentBlock; i += BATCH_SIZE) {
const blocks = await Promise.all(
Array.from({length: BATCH_SIZE}, (_, j) => getBlock(i + j))
);
await processBlocks(blocks);
}
3. RPC Fallbacks
Si un RPC falla, usar alternativo:
const RPC_ENDPOINTS = [
'https://rpc.ankr.com/eth',
'https://eth.llamarpc.com',
'https://rpc.flashbots.net'
];
async function callRPC(method: string, params: any[]) {
for (const endpoint of RPC_ENDPOINTS) {
try {
return await fetch(endpoint, {
method: 'POST',
body: JSON.stringify({ method, params, id: 1, jsonrpc: '2.0' })
});
} catch (error) {
console.error(`RPC ${endpoint} failed, trying next...`);
continue;
}
}
throw new Error('All RPCs failed');
}
Gestión de Errores
Reorg Detection (Reorganización de Blockchain)
A veces la blockchain se "reorganiza" y bloques previamente confirmados se invalidan:
async detectReorg() {
const lastBlock = await this.getLastProcessedBlock();
const blockHash = await this.rpc.getBlockHash(lastBlock);
const savedHash = await this.db.getBlockHash(lastBlock);
if (blockHash !== savedHash) {
// ¡Reorg detectado!
this.logger.warn(`Reorg detected at block ${lastBlock}`);
// Retroceder N bloques y reprocesar
await this.rollbackBlocks(10);
await this.reprocessBlocks();
}
}
Rate Limiting de RPCs
const limiter = new Bottleneck({
maxConcurrent: 5, // Máx 5 requests simultáneos
minTime: 200 // Mín 200ms entre requests
});
const getBlock = limiter.wrap(async (blockNum) => {
return await this.rpc.getBlock(blockNum);
});
Persistencia de Estado
Cada monitor guarda su estado en MongoDB:
interface MonitorState {
blockchain: 'ethereum' | 'solana' | 'bsc' | 'tron' | 'bitcoin';
lastProcessedBlock: number; // o lastProcessedSlot para Solana
lastProcessedHash: string;
lastUpdate: Date;
isRunning: boolean;
}
Recovery automático:
async startMonitor() {
const state = await this.getState();
if (state.isRunning) {
// El monitor se cayó, continuar desde último bloque
this.logger.warn('Monitor was interrupted, resuming...');
this.startFrom = state.lastProcessedBlock;
}
await this.updateState({ isRunning: true });
await this.monitorLoop();
}
Notificaciones
Cuando se detecta una transacción:
Payload de notificación:
{
type: 'transaction_received',
data: {
coin: 'ETH',
amount: 0.5,
from: '0xabc...',
to: '0xdef...',
txHash: '0x123...',
confirmations: 1,
usdValue: 750.00,
timestamp: '2025-10-20T15:30:00Z'
}
}
Resumen de Monitors
| Monitor | Tiempo Bloque | Intervalo Polling | Confirmations |
|---|---|---|---|
| Solana | ~0.4s | 30s | 32 slots |
| Ethereum | ~15s | 15s | 12 bloques |
| BSC | ~3s | 5s | 15 bloques |
| Tron | ~3s | 5s | 19 bloques |
| Bitcoin | ~10min | 1min | 6 bloques |
Arquitectura de Deployment
Cada monitor corre en su propio contenedor Docker, independiente de los demás.
Para Desarrolladores
Los monitors son críticos pero autónomos. Si un monitor falla, los demás siguen funcionando.
Debugging tips:
- Revisa
lastProcessedBlocken MongoDB para ver si está avanzando - Verifica logs para detectar errores de RPC
- Usa Blockchain explorers para validar transacciones manualmente
- Si una transacción no se detecta, verifica que la address esté en la lista de monitoreadas
Agregar un nuevo blockchain:
- Crear nuevo monitor siguiendo el patrón
- Implementar
getBlock()yparseTransaction() - Agregar a docker-compose
- Configurar RPC endpoint