tutoriales.com

Desarrollando dApps con WebSockets y TheGraph: Notificaciones en Tiempo Real y Consultas Optimizadas en Web3 ⚡

Este tutorial te guiará en la construcción de dApps Web3 con capacidades de notificación en tiempo real, integrando WebSockets para la comunicación instantánea y TheGraph para optimizar las consultas de datos on-chain. Exploraremos cómo estas tecnologías se combinan para crear experiencias de usuario fluidas y reactivas en tus aplicaciones descentralizadas.

Intermedio20 min de lectura11 views
Reportar error

Introducción a las dApps Reactivas y la Importancia de Datos en Tiempo Real 🚀

En el mundo de Web3, la interacción con contratos inteligentes y la blockchain a menudo se percibe como una serie de transacciones asíncronas y esperas. Sin embargo, para que las Aplicaciones Descentralizadas (dApps) compitan con sus contrapartes centralizadas en términos de experiencia de usuario, es crucial ofrecer información y actualizaciones en tiempo real. Imagina un mercado NFT donde no recibes una notificación inmediata cuando tu oferta es superada, o un juego descentralizado donde no ves las acciones de otros jugadores al instante. ¡Sería frustrante!

Aquí es donde entran en juego tecnologías como WebSockets para la comunicación bidireccional instantánea y TheGraph para la indexación y consulta eficiente de datos on-chain. Este tutorial explora cómo combinar estas herramientas para construir dApps más reactivas, interactivas y con una mejor experiencia de usuario.

📌 Nota: Este tutorial asume que tienes conocimientos básicos de desarrollo Web3, JavaScript, Node.js, Solidity y el funcionamiento de blockchains como Ethereum.

¿Por qué la reactividad es clave en Web3? ✨

La blockchain es un entorno inherentemente asincrónico. Las transacciones tardan un tiempo en confirmarse, y los cambios de estado en los contratos inteligentes no son inmediatamente visibles para todos los clientes a menos que se "listen" activamente. Una dApp reactiva es aquella que puede:

  • Mostrar cambios instantáneos: Actualizar la interfaz de usuario tan pronto como ocurre un evento on-chain relevante.
  • Notificar al usuario: Informar a los usuarios sobre eventos importantes (transferencias, ofertas, finalizaciones de votación, etc.).
  • Proporcionar una experiencia fluida: Minimizar las esperas y ofrecer feedback constante al usuario.

WebSockets: La Columna Vertebral de la Comunicación en Tiempo Real 💬

WebSockets proporcionan un canal de comunicación dúplex completo sobre una única conexión TCP. A diferencia de HTTP, que requiere una nueva solicitud para cada intercambio de datos, WebSockets mantienen una conexión persistente, permitiendo que tanto el cliente como el servidor envíen datos en cualquier momento. Esto es ideal para aplicaciones que requieren baja latencia y actualizaciones frecuentes, como chats, juegos multijugador o, en nuestro caso, dApps que necesitan notificaciones instantáneas de eventos de blockchain.

¿Cómo funcionan los WebSockets? ⚙️

  1. Handshake: Un cliente inicia una conexión WebSocket enviando una solicitud HTTP especial (conocida como handshake) al servidor. El servidor responde, y si la negociación es exitosa, la conexión se actualiza a un protocolo WebSocket.
  2. Comunicación Bidireccional: Una vez establecida la conexión, cliente y servidor pueden enviar y recibir mensajes de forma independiente a través del mismo canal.
  3. Conexión Persistente: La conexión permanece abierta hasta que uno de los extremos la cierra explícitamente.
Cliente Servidor HTTP Request (Upgrade: websocket) HTTP Response (101 Switching Protocols) WebSocket Connection Conexión Bidireccional Persistente 1 2 3

Implementación básica de WebSockets con ws en Node.js 🛠️

Para nuestro backend, utilizaremos la popular librería ws para Node.js. Primero, instalémosla:

npm init -y
npm install ws

Ahora, un servidor WebSocket simple:

// server.js
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', ws => {
    console.log('Cliente conectado');

    ws.on('message', message => {
        console.log(`Recibido: ${message}`);
        // Envía el mensaje de vuelta a todos los clientes conectados
        wss.clients.forEach(client => {
            if (client !== ws && client.readyState === WebSocket.OPEN) {
                client.send(`Eco de ${message}`);
            }
        });
    });

    ws.on('close', () => {
        console.log('Cliente desconectado');
    });

    ws.send('¡Bienvenido al servidor WebSocket!');
});

console.log('Servidor WebSocket iniciado en el puerto 8080');

Para el cliente (ejemplo en JavaScript en el navegador):

// client.js (ejecutar en el navegador o con un entorno como Live Server)
const ws = new WebSocket('ws://localhost:8080');

ws.onopen = () => {
    console.log('Conectado al servidor WebSocket');
    ws.send('Hola desde el cliente!');
};

ws.onmessage = event => {
    console.log(`Mensaje del servidor: ${event.data}`);
};

ws.onclose = () => {
    console.log('Desconectado del servidor WebSocket');
};

ws.onerror = error => {
    console.error('Error WebSocket:', error);
};

TheGraph: Indexación de Datos On-Chain a la Velocidad de la Luz 🚀

La blockchain es una base de datos distribuida, pero consultar datos históricos o complejos directamente de un nodo puede ser lento y costoso. TheGraph es un protocolo descentralizado para indexar y consultar datos de blockchain. Permite crear subgraphs, que son APIs abiertas y programables que indexan eventos y datos de contratos inteligentes, haciéndolos fácilmente consultables a través de GraphQL.

¿Por qué TheGraph para dApps? 🎯

  • Consultas Eficientes: Accede a datos complejos de forma rápida y sencilla con GraphQL, evitando la necesidad de iterar sobre bloques o eventos directamente desde el nodo.
  • Datos Históricos: Consulta estados pasados de contratos o eventos que ocurrieron hace mucho tiempo.
  • Descentralización: Los subgraphs pueden ser alojados por indexadores descentralizados, contribuyendo a la robustez de tu dApp.
  • Abstracción: Normaliza los datos de la blockchain en un esquema fácil de usar.

Componentes clave de TheGraph 📖

  1. Subgraph: Define qué datos indexar y cómo transformar esos datos en un esquema GraphQL.
  2. Manifest (subgraph.yaml): Configura los contratos inteligentes a observar, los eventos a escuchar y las funciones de mapeo asociadas.
  3. Esquema GraphQL (schema.graphql): Define la estructura de los datos que tu subgraph expondrá.
  4. Funciones de Mapeo (mapping.ts): Código TypeScript que se ejecuta cuando ocurre un evento de blockchain y actualiza las entidades definidas en tu esquema.
Blockchain (Ethereum) Emite Eventos TheGraph Node (Indexador) Escucha los Eventos Funciones de Mapeo (AssemblyScript) Procesan Eventos Almacén de Datos Datos Indexados actualizados API GraphQL Permite a las dApps consultar los datos Flujo de datos en el protocolo The Graph

Ejemplo: Un Subgraph Simple para Eventos de un Contrato ERC-20 💰

Supongamos que tenemos un contrato ERC-20 simple que emite un evento Transfer cada vez que se transfieren tokens.

1. Contrato Solidity (simplificado):

// Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MyToken {
    string public name = "MyToken";
    string public symbol = "MTK";
    uint256 public totalSupply = 1000000;

    event Transfer(address indexed from, address indexed to, uint256 value);

    mapping(address => uint256) public balances;

    constructor() {
        balances[msg.sender] = totalSupply;
    }

    function transfer(address to, uint256 value) public returns (bool) {
        require(balances[msg.sender] >= value, "Insufficient balance");
        balances[msg.sender] -= value;
        balances[to] += value;
        emit Transfer(msg.sender, to, value);
        return true;
    }
}

2. schema.graphql:

type Transfer @entity {
  id: Bytes!
  from: Bytes!
  to: Bytes!
  value: BigInt!
  timestamp: BigInt!
  blockNumber: BigInt!
  transactionHash: Bytes!
}

3. subgraph.yaml (extracto):

specVersion: 0.0.5
# ... otras configuraciones
sources:
  - kind: ethereum/contract
    name: MyToken
    network: goerli # o la red que uses
    source:
      address: "0xYourContractAddressHere"
      abi: MyToken
    mapping:
      kind: ethereum/events
      apiVersion: 0.0.5
      language: wasm/assemblyscript
      entities:
        - Transfer
      abis:
        - name: MyToken
          file: ./abis/MyToken.json
      eventHandlers:
        - event: Transfer(indexed address,indexed address,uint256)
          handler: handleTransfer
      file: ./src/mapping.ts

4. src/mapping.ts:

import { Transfer as TransferEvent } from '../generated/MyToken/MyToken';
import { Transfer } from '../generated/schema';
import { BigInt } from '@graphprotocol/graph-ts';

export function handleTransfer(event: TransferEvent): void {
  let transfer = new Transfer(event.transaction.hash.toHex() + '-' + event.logIndex.toString());
  transfer.from = event.params.from;
  transfer.to = event.params.to;
  transfer.value = event.params.value;
  transfer.timestamp = event.block.timestamp;
  transfer.blockNumber = event.block.number;
  transfer.transactionHash = event.transaction.hash;
  transfer.save();
}

Con este subgraph desplegado, podemos consultar todos los eventos Transfer de forma muy eficiente.

Integrando WebSockets y TheGraph para Notificaciones en Tiempo Real 🌐

Ahora, la parte interesante: ¡combinar ambos! Nuestro objetivo es que el servidor WebSocket escuche nuevos datos de TheGraph y los envíe a los clientes conectados. Para lograr esto, el servidor WebSocket actuará como un listener o publisher de eventos.

Arquitectura Propuesta 🏗️

  1. Contrato Inteligente: Emite eventos relevantes.
  2. TheGraph: Indexa estos eventos y los hace consultables.
  3. Servidor WebSocket (Backend):
    • Actúa como un cliente de TheGraph, realizando consultas periódicas o suscripciones (si el endpoint de TheGraph lo permite, aunque la mayoría de los hosted services no ofrecen suscripciones GraphQL por HTTP). Una alternativa es usar ethers.js o web3.js para escuchar eventos directamente desde un proveedor de Ethereum y luego "empujar" esos eventos a los clientes de WebSocket.
    • Cuando detecta un nuevo evento on-chain (ya sea por TheGraph o directamente del nodo), lo formatea y lo envía a todos los clientes WebSocket conectados.
  4. Cliente (Frontend dApp):
    • Establece una conexión WebSocket con el servidor.
    • Recibe las notificaciones en tiempo real y actualiza la interfaz de usuario.
    • También puede realizar sus propias consultas a TheGraph para datos más complejos o iniciales.
Arquitectura de Datos dApp Blockchain (Smart Contract) TheGraph (Indexer) WebSocket (Backend) Frontend (dApps) Eventos GraphQL Real-time

Paso a Paso: Construyendo el Backend con ethers.js, TheGraph y WebSockets

En este ejemplo, nuestro servidor de Node.js escuchará directamente los eventos del contrato inteligente usando ethers.js y luego retransmitirá esos eventos a través de WebSockets. Esta es una forma más directa de obtener notificaciones realmente en tiempo real para eventos nuevos, sin depender del ciclo de indexación de TheGraph para las notificaciones.

💡 Consejo: Aunque usamos `ethers.js` para escuchar eventos en tiempo real para las notificaciones, TheGraph sigue siendo indispensable para consultar el *historial* de eventos de manera eficiente o para datos agregados. Ambas herramientas se complementan.

1. Configuración del Proyecto y Dependencias:

mkdir realtime-dapp-backend
cd realtime-dapp-backend
npm init -y
npm install ws ethers dotenv

Crea un archivo .env para tus claves y URLs:

INFURA_PROJECT_ID=tu_proyecto_infura_id
CONTRACT_ADDRESS=0xTuDireccionDeContratoERC20

2. Contrato Inteligente (asumimos el MyToken.sol anterior ya desplegado):

Necesitarás el ABI del contrato MyToken. Guarda el JSON del ABI en abis/MyToken.json.

3. Servidor de Notificaciones WebSocket (server.js):

// server.js
require('dotenv').config();
const WebSocket = require('ws');
const { ethers } = require('ethers');
const fs = require('fs');

// Configuración del servidor WebSocket
const wss = new WebSocket.Server({ port: 8081 }); // Puerto diferente para evitar conflictos si ya tienes otro WS

wss.on('connection', ws => {
    console.log('Cliente WebSocket conectado');
    ws.send(JSON.stringify({ type: 'status', message: 'Conectado al servicio de notificaciones.' }));
});

wss.on('close', () => {
    console.log('Cliente WebSocket desconectado');
});

console.log('Servidor WebSocket de notificaciones iniciado en el puerto 8081');

// Configuración de Ethers.js para escuchar eventos
const provider = new ethers.providers.InfuraProvider(
    'goerli', // o la red que estés usando (mainnet, sepolia, etc.)
    process.env.INFURA_PROJECT_ID
);

const contractAddress = process.env.CONTRACT_ADDRESS;
const contractABI = JSON.parse(fs.readFileSync('./abis/MyToken.json', 'utf8'));

const myTokenContract = new ethers.Contract(contractAddress, contractABI, provider);

console.log(`Escuchando eventos del contrato en: ${contractAddress}`);

// Escuchar el evento 'Transfer'
myTokenContract.on('Transfer', (from, to, value, event) => {
    const transferData = {
        type: 'TransferEvent',
        from: from,
        to: to,
        value: value.toString(), // Convertir BigNumber a string
        transactionHash: event.transactionHash,
        blockNumber: event.blockNumber,
        timestamp: new Date().toISOString() // Añadir timestamp para referencia
    };
    console.log('Nuevo evento Transfer detectado:', transferData);

    // Enviar el evento a todos los clientes WebSocket conectados
    wss.clients.forEach(client => {
        if (client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify(transferData));
        }
    });
});

// Opcional: Manejar errores del proveedor
provider.on('error', (error) => {
    console.error('Error del proveedor de Ethereum:', error);
    // Implementar lógica de reconexión si es necesario
});
⚠️ Advertencia: Asegúrate de que `INFURA_PROJECT_ID` y `CONTRACT_ADDRESS` en tu `.env` sean correctos y que el ABI de tu contrato coincida con el contrato desplegado.

4. Cliente Frontend (index.html con JavaScript):

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>dApp de Notificaciones en Tiempo Real</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
        .container { max-width: 800px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        h1 { color: #0056b3; text-align: center; }
        #status { text-align: center; margin-bottom: 20px; padding: 10px; border-radius: 5px; background-color: #e0f7fa; border: 1px solid #00bcd4; }
        #event-log { border: 1px solid #ddd; padding: 15px; border-radius: 5px; background-color: #e9ecef; max-height: 400px; overflow-y: auto; }
        .event-item { background-color: #ffffff; margin-bottom: 10px; padding: 10px; border-radius: 5px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
        .event-item strong { color: #28a745; }
        .from-to { font-weight: bold; color: #6c757d; }
        .value { color: #dc3545; }
    </style>
</head>
<body>
    <div class="container">
        <h1>Monitor de Eventos ERC-20 en Tiempo Real ⚡</h1>
        <div id="status">Conectando al servidor de notificaciones...</div>
        <div class="progress-bar"><div class="progress-fill" style="width: 100%; background: #007bff;">Conectado</div></div>
        
        <h2>Eventos Recibidos:</h2>
        <div id="event-log"></div>

        <details open><summary>¿Cómo funciona?</summary>
            Este cliente se conecta a un servidor WebSocket. El servidor está escuchando eventos `Transfer` de un contrato ERC-20 en la blockchain (Goerli en este ejemplo). Cada vez que ocurre un `Transfer`, el servidor lo retransmite a este cliente a través del WebSocket, y el evento se muestra aquí en tiempo real.
        </details>
    </div>

    <script>
        const statusDiv = document.getElementById('status');
        const eventLogDiv = document.getElementById('event-log');
        const ws = new WebSocket('ws://localhost:8081'); // Asegúrate de que el puerto coincida con tu servidor

        ws.onopen = () => {
            statusDiv.textContent = '✅ Conectado al servidor de notificaciones.';
            console.log('Conectado al servidor WebSocket');
        };

        ws.onmessage = event => {
            const data = JSON.parse(event.data);
            console.log('Mensaje del servidor:', data);

            if (data.type === 'TransferEvent') {
                const eventItem = document.createElement('div');
                eventItem.className = 'event-item';
                eventItem.innerHTML = `
                    <strong>💸 Nueva Transferencia:</strong><br>
                    <span class="from-to">De:</span> ${data.from}<br>
                    <span class="from-to">A:</span> ${data.to}<br>
                    <span class="value">Valor:</span> ${data.value} Tokens<br>
                    Bloque: ${data.blockNumber}<br>
                    Tx Hash: <a href="https://goerli.etherscan.io/tx/${data.transactionHash}" target="_blank">${data.transactionHash.substring(0, 10)}...</a><br>
                    <small>${data.timestamp}</small>
                `;
                eventLogDiv.prepend(eventItem); // Añadir al principio para ver los más recientes
            } else if (data.type === 'status') {
                // Manejar mensajes de estado si los hay
                statusDiv.textContent = `ℹ️ ${data.message}`;
            }
        };

        ws.onclose = () => {
            statusDiv.textContent = '🔴 Desconectado del servidor de notificaciones.';
            console.log('Desconectado del servidor WebSocket');
        };

        ws.onerror = error => {
            statusDiv.textContent = '❌ Error de conexión al servidor.';
            console.error('Error WebSocket:', error);
        };
    </script>
</body>
</html>

Cómo ejecutar la dApp de Notificaciones 🛠️

Paso 1: Despliega tu contrato `MyToken.sol` en una red de prueba (como Goerli o Sepolia). Anota la dirección del contrato.
Paso 2: Actualiza tu archivo `.env` en el backend con el `INFURA_PROJECT_ID` (o Alchemy, etc.) y la `CONTRACT_ADDRESS` del contrato desplegado. Asegúrate de tener el ABI de tu contrato en `abis/MyToken.json`.
Paso 3: Inicia tu servidor de notificaciones: `node server.js` desde el directorio `realtime-dapp-backend`. Verás mensajes de consola indicando que está escuchando.
Paso 4: Abre el archivo `index.html` en tu navegador. Puedes usar una extensión como Live Server para VS Code, o simplemente abrirlo con doble clic.
Paso 5: Realiza algunas transferencias de tokens desde la cuenta que desplegó el contrato (usando MetaMask, por ejemplo) a otras direcciones en la misma red. Observa cómo los eventos aparecen casi instantáneamente en tu página HTML.

Consultando Datos Históricos con TheGraph en el Frontend (Complementario) 📊

Mientras que nuestro servidor WebSocket nos da notificaciones en tiempo real, para mostrar un historial completo de transferencias o datos agregados, TheGraph es el camino a seguir. Podemos integrar consultas a TheGraph directamente en nuestro frontend.

Para hacer esto, necesitarías desplegar el subgraph que definimos anteriormente para tu contrato MyToken. Una vez desplegado (por ejemplo, en TheGraph Hosted Service), obtendrás un endpoint GraphQL.

// Ejemplo de consulta GraphQL para el frontend (usando fetch)
async function fetchPastTransfers() {
    const subgraphEndpoint = 'https://api.thegraph.com/subgraphs/name/your-username/my-token-subgraph'; // Reemplaza con tu endpoint
    const query = `
        query {
            transfers(first: 10, orderBy: timestamp, orderDirection: desc) {
                id
                from
                to
                value
                timestamp
                blockNumber
                transactionHash
            }
        }
    `;

    try {
        const response = await fetch(subgraphEndpoint, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ query })
        });
        const data = await response.json();
        console.log('Transferencias históricas de TheGraph:', data.data.transfers);
        // Aquí puedes actualizar tu UI con las transferencias históricas
        return data.data.transfers;
    } catch (error) {
        console.error('Error al consultar TheGraph:', error);
    }
}

// Llama a esta función al cargar la página o cuando sea necesario
// fetchPastTransfers();
🔥 Importante: Combinar WebSockets para notificaciones *push* de eventos en tiempo real y TheGraph para consultas *pull* de datos históricos o agregados es una estrategia poderosa para construir dApps robustas y reactivas.

Consideraciones Avanzadas y Optimización 📈

Escalabilidad de WebSockets

Para aplicaciones de producción con muchos usuarios, un único servidor WebSocket puede no ser suficiente. Considera:

  • Load Balancers: Distribuir el tráfico entre múltiples instancias del servidor WebSocket.
  • Clusters de Node.js: Utilizar el módulo cluster de Node.js para aprovechar múltiples núcleos de CPU.
  • Servicios gestionados: Plataformas como Socket.IO (que usa WebSockets por debajo) ofrecen herramientas y patrones para escalar.

Resistencia y Reconexión

Las conexiones WebSocket pueden caer. Implementa lógica de reconexión tanto en el cliente como en el servidor. ethers.js tiene una lógica de reconexión básica, pero es buena idea manejarla explícitamente.

Filtrado de Notificaciones

No todos los eventos son relevantes para todos los usuarios. En el servidor WebSocket, puedes añadir lógica para filtrar eventos y enviarlos solo a los clientes que los hayan suscrito (por ejemplo, un usuario solo quiere notificaciones de transferencias a su propia dirección).

Segurización de WebSockets

  • Usa wss:// (WebSockets seguros) en producción para cifrar la comunicación.
  • Autentica a los usuarios que se conectan a tu servidor WebSocket si el contenido de las notificaciones es sensible o personalizado.

Optimización de Consultas a TheGraph

  • Indexado eficiente: Asegúrate de que tus funciones de mapeo de TheGraph sean lo más eficientes posible.
  • Consultas específicas: Pide solo los datos que necesitas en tus consultas GraphQL.
  • Paginación: Utiliza la paginación (first, skip) para manejar grandes conjuntos de datos.
¿Qué pasa si mi dApp necesita procesar miles de eventos por segundo? Para volúmenes extremadamente altos de eventos, podrías necesitar una arquitectura de streaming de datos más robusta, como Kafka o RabbitMQ, que ingiera los eventos de la blockchain y luego los distribuya a varios consumidores, incluyendo tu servidor WebSocket. TheGraph también puede escalar, pero para la ingesta directa de eventos para notificaciones puras, un sistema de mensajería puede ser más adecuado.

Conclusión ✨

Construir dApps con una excelente experiencia de usuario en Web3 requiere más que solo interactuar con contratos inteligentes. La capacidad de ofrecer actualizaciones en tiempo real y consultas de datos eficientes es fundamental. Al combinar el poder de los WebSockets para la comunicación instantánea y la robustez de TheGraph para la indexación de datos de blockchain, puedes crear dApps que no solo sean descentralizadas, sino también increíblemente reactivas y agradables de usar. ¡Empieza a experimentar con estas tecnologías y lleva tus dApps al siguiente nivel!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!