Saltar al contenido principal

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:

  1. Polling Loop: Consulta blockchain periódicamente
  2. Address Watching: Lista de addresses a monitorear
  3. Transaction Detection: Detecta txs relevantes
  4. Persistence: Guarda estado y transacciones
  5. 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

MonitorTiempo BloqueIntervalo PollingConfirmations
Solana~0.4s30s32 slots
Ethereum~15s15s12 bloques
BSC~3s5s15 bloques
Tron~3s5s19 bloques
Bitcoin~10min1min6 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 lastProcessedBlock en 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:

  1. Crear nuevo monitor siguiendo el patrón
  2. Implementar getBlock() y parseTransaction()
  3. Agregar a docker-compose
  4. Configurar RPC endpoint