DEV Community

Ahmed Castro
Ahmed Castro

Posted on • Edited on

Cómo crear un Bot de Telegram Seguro🔒 con el EIP-712

Telegram tiene alrededor de un billón de usuarios activos al mes y es una parte fundamental de las dApps y comunidades Web3. Sin embargo, muchos bots de Telegram aún usan de métodos de interacción inseguros.

Al completar esta guía, conocerás una mejor manera de conectar a los usuarios con aplicaciones decentralizadas a través de Telegram. Este tutorial te mostrará cómo autenticar usuarios de Telegram con sus wallets Web3, sin acceder nunca a sus claves privadas.

En este tutorial aprenderás a:

  1. Crear un bot de Telegram seguro: Construir un bot que autentique usuarios mediante firmas de wallets Web3 sin almacenar llaves privadas.
  2. Implementar firmas ERC-712: Crear un flujo de autenticación donde el usuario firma mensajes en un formato legible.
  3. Implementar autenticación flexible: A través de telegram los usuarios pueden interactuar con cualquier tipo de aplicación, desde grupos secreatos hasta cualquier tipo de transacción ya sea en DeFi o airdrops.

Para ver el proyecto completo, revisa el código completo.

Flow
Flujo de la apliación.

1. Crea un nuevo bot de Telegram

Crear un bot de Telegram es muy sencillo. Puedes hacerlo enviando un mensaje directo a la cuenta de Telegram @BotFather. Solo envíale el comando /newbot, sigue las instrucciones y obtendrás un token que usaremos para interactuar con la librería de Telegram en el siguiente paso.

bot father
Envía /newbot al @botfather para crear un nuevo Bot de Telegram.

2. Lanza el backend del bot de Telegram

Comencemos instalando las dependencias. Pero antes de eso, asegúrate de tener Node.js instalado. Te recomiendo usar nvm para instalarlo.

Una vez que tengas Node.js instalado, puedes instalar las dependencias con el siguiente comando:

npm install node-telegram-bot-api ethers dotenv express cors
Enter fullscreen mode Exit fullscreen mode

Now, let's create the .env file with the following variables:

.env

BOT_TOKEN=your_telegram_bot_token
CHAIN_ID=1
WEB_DAPP_URL=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

Cambia la CHAIN_ID dependiendo de la chain que tu dApp o comunidad usa. Puedes usar Chainlist para averiguar la chain qu estás usando. Si quieres que tu bot sea agnóstico a una chain, coloca el id 1, la chain id de Ethereum.

No olivdés crear el archivo .gitignore para evitar subir el archivo .env a un repositorio público.

.gitignore

.env
node_modules
Enter fullscreen mode Exit fullscreen mode

Este es el archivo bot.js, que lanza un bot que escucha mensajes de Telegram y verifica la firma. Los bots de Telegram usan polling para recibir mensajes localmente, lo que significa que estarán haciendo llamadas constantes a los servidores de Telegram en busca de nuevos mensajes. En un entorno de producción con muchos usuarios, esto podría ser un problema, por lo que deberías usar el método de webhook y alojar el servidor de forma remota.

Para este tutorial usaremos el método de polling localmente. Si quieres aprender más sobre el método de webhook, puedes consultar la documentación de la API de Bots de Telegram.

Cuando el usuario envía el comando authenticate al bot, este le responderá con un mensaje para que visite la dApp web. La dApp web firmará un mensaje y lo enviará al bot, que verificará la firma y enviará un mensaje de bienvenida al usuario.

bot.js

// Usaremos la biblioteca oficial node-telegram-bot-api para interactuar con la API de Telegram y ethers para verificar la firma
const TelegramBot = require("node-telegram-bot-api");
const { ethers } = require("ethers");
require("dotenv").config();
const express = require("express");
const cors = require("cors");

const bot = new TelegramBot(process.env.BOT_TOKEN, { polling: true });
const CHAIN_ID = process.env.CHAIN_ID;
const WEB_DAPP_URL = process.env.WEB_DAPP_URL;

const app = express();
app.use(cors());
app.use(express.json());

// Inicia el bot de Telegram y el servidor API que recibe la firma y la verifica
(async () => {
    try {
        bot.botInfo = await bot.getMe();
        app.listen(8080, () => {
            console.log("\nServer is running on port 8080");
            console.log("Bot is running...");
        });
    } catch (error) {
        console.error(error);
        process.exit(1);
    }
})();

// El endpoint /verify se usa para verificar la firma y enviar un mensaje de bienvenida al usuario
app.post("/verify", async (req, res) => {
    const { userId, message, signature } = req.body;
    try {
        const signerAddress = await getAuthenticationSigner(userId, message, signature);
        await bot.sendMessage(
            Number(userId), 
            `Welcome! You're authenticated as ${signerAddress}.\n\nEnjoy your welcome gift! 🎁`
        );
        res.json({ success: true, signerAddress });
    } catch (error) {
        console.error("Verification error:", error);
        res.status(400).json({ success: false, error: error.message });
    }
});

// getAuthenticationSigner devuelve la dirección del firmante verificando la firma
function getAuthenticationSigner(userId, message, signature) {
    // accessRequest es el esquema de datos real del mensaje que queremos verificar
    const accessRequest = {
        userId: userId,
        message: message,
    };
    // domain es la información general sobre tu dapp, esto es lo mismo para todos los mensajes
    const domain = {
        name: "Telegram Group Access",
        version: "1",
        chainId: CHAIN_ID,
    };
    // types es el esquema de datos del mensaje que queremos verificar
    const types = {
    AccessRequest: [
            { name: "userId", type: "uint256" },
            { name: "message", type: "string" },
        ]
    };
    // verifyTypedData verifica la firma en el formato erc712 y devuelve la dirección del firmante mediante ecrecover
    // No necesitamos preocuparnos por esos detalles, ethers lo hace por nosotros
    return ethers.verifyTypedData(domain, types, accessRequest, signature);
}

// Esta es la función principal que se ejecuta cuando el bot recibe un mensaje
bot.on("message", async (msg) => {
    const text = msg.text || "";
    // Verifica si el mensaje es "authenticate" y si es así, envía un mensaje al usuario para que visite el sitio web
    if (text.toLowerCase() === "authenticate") {
        // userId es el id del usuario en Telegram
        const userId = msg.from.id;
        // Enviamos el usuario a la dapp web para autenticarse
        bot.sendMessage(userId, `Please visit: ${WEB_DAPP_URL}?userId=${userId}`);
        return;
    }
});

console.log("\nBot is running...");
Enter fullscreen mode Exit fullscreen mode

Ahora corre el siguiente comando desde el mismo directorio donde se encuentra el archivo bot.js. Esto iniciariá el bot:

node bot.js
Enter fullscreen mode Exit fullscreen mode

Tu bot ahora está correiendo en http://127.0.0.1:8080.

En el siguiente paso crearemos una dApp web que interactúa con nuestro bot.

3. Crea una dApp Web para Firmar Mensajes

En este paso crearemos 3 archivos:

  • index.html: El archivo HTML que define la conexión con la wallet y el botón para firmar.
  • signature_messaging.js: El archivo que define cómo se firma el mensaje y se envía al backend.
  • wallet_connection.js: El archivo que maneja la conexión con la wallet.

La dApp será un archivo HTML sencillo que contiene un botón Sign que abrirá la wallet del usuario para firmar un mensaje.

erc712 webapp

Al hacer clic en el botón de _Sign, este abrirá el modal de firma ERC712._

Comencemos con el archivo HTML que contiene el botón Sign que abrirá la wallet del usuario para firmar un mensaje.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
</head>
<body>
  <div>
    <!-- Botón de conexión a la Wallet, solo es visible si la Wallet no está conectada -->
    <input id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: none"></input>
    <h1>Telegram Bot Authentication</h1>
    <p id="web3_message"></p>
    <h3>Sign Authentication Request</h3>
    <!-- Este es el botón de autenticación, levantará el modal de firma -->
    <button type="button" id="sign" onclick="_signMessage()">Sign</button>
    <p id="signature"></p>
  </div>
    <!-- En este tutorial usaremos web3.js, puedes obener los mismos resultados con ethers.js -->
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
  <script type="text/javascript" src="wallet_connection.js"></script>
  <script type="text/javascript" src="signature_messaging.js"></script>
</body>

  <script>
    // The signMessage function is called when the user clicks the sign button
    function _signMessage()
    {
      const urlParams = new URLSearchParams(window.location.search);
      const userId = urlParams.get('userId');
      signMessage(userId, "I'm requesting access to the Telegram group.")
    }
  </script>
</html>
Enter fullscreen mode Exit fullscreen mode

El archivo signature_messaging.js define cómo se firma el mensaje y cómo se envía al backend. Usaremos el estándar ERC712 para firmar el mensaje, ya que este permite mostrar una explicación legible del mensaje en la wallet al momento de firmar.

Para lograr esto, necesitamos definir el dominio y los tipos del mensaje, lo cual define el esquema de datos del mensaje a firmar. Además, utilizaremos la función eth_signTypedData_v4, que es el método más avanzado y de actualidad para firmar mensajes y ya es compatible con todas las wallets modernas.

signature_messaging.js

const BOT_API_URL = 'http://localhost:8080'
// Firma el mensaje usando el formato ERC712
async function signMessage(userId, message)
{
  // ERC712 espera que envíes el mensaje en un formato específico
  // Definimos el Domain, que tiene información general sobre la dapp y debe ser el mismo para todos los mensajes
  // Luego, definimos los tipos del mensaje, que son los campos del mensaje
  // Finalmente, definimos el mensaje a firmar
  const msgParams = JSON.stringify({
    types: {
      EIP712Domain: [
        { name: 'name', type: 'string' },
        { name: 'version', type: 'string' },
        { name: 'chainId', type: 'uint256' },
      ],
      AccessRequest: [
        { name: 'userId', type: 'uint256' },
        { name: 'message', type: 'string' }
      ],
    },
    primaryType: 'AccessRequest',
    domain: {
      name: 'Telegram Group Access',
      version: '1',
      chainId: NETWORK_ID,
    },
    message: {
      userId: userId,
      message: message,
    },
  });

  // ERC712 introdujo el método eth_signTypedData_v4, que ahora es ampliamente soportado por todas las wallets
  const signature = await ethereum.request({
    method: "eth_signTypedData_v4",
    params: [accounts[0], msgParams],
  });

  document.getElementById("signature").textContent="Signature: " + signature;

  // Envía el mensaje al bot de Telegram
  await sendSignature(userId, message, signature);
}

// Envía la firma al bot de Telegram
async function sendSignature(userId, message, signature) {
  // Empecemos agrupando los datos para enviar al bot de Telegram
  const requestData = {
    userId: userId,
    message: message,
    signature: signature
  };

  try {
    // Envía los datos al bot de Telegram llamando al endpoint POST /verify
    // Si la firma es válida, el bot enviará un mensaje al usuario
    const response = await fetch(BOT_API_URL + '/verify', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(requestData)
    });

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    alert("Message sent successfully!");
  } catch (error) {
    console.error('Error:', error);
    alert("Failed to send message: " + error.message);
  }
}
Enter fullscreen mode Exit fullscreen mode

El archivo wallet_connection.js maneja la conexión a la wallet del usuario, hace un pop up con el bontón de Conectar y refresca la página cuando la wallet ha cambiado.

wallet_connection.js

// Constantes
const NETWORK_ID = 534352
var accounts
var web3

// Refresca la página si la wallet cambia
function metamaskReloadCallback() {
  window.ethereum.on('accountsChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Account changed, refreshing...";
    window.location.reload()
  })
  window.ethereum.on('networkChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Network changed, refreshing...";
    window.location.reload()
  })
}

// Obtén una instancia de Web3
const getWeb3 = async () => {
  return new Promise((resolve, reject) => {
    if(document.readyState=="complete")
    {
      if (window.ethereum) {
        const web3 = new Web3(window.ethereum)
        window.location.reload()
        resolve(web3)
      } else {
        reject("must install MetaMask")
        document.getElementById("web3_message").textContent="Error: Please connect to MetaMask";
      }
    }else
    {
      window.addEventListener("load", async () => {
        if (window.ethereum) {
          const web3 = new Web3(window.ethereum)
          resolve(web3)
        } else {
          reject("must install MetaMask")
          document.getElementById("web3_message").textContent="Error: Please install Metamask";
        }
      });
    }
  });
};

// Carga la dApp, carga la dApp y obtén la instancia de Web3
async function loadDapp() {
  metamaskReloadCallback()
  document.getElementById("web3_message").textContent="Please connect to Metamask"
  var awaitWeb3 = async function () {
    web3 = await getWeb3()
    web3.eth.net.getId((err, netId) => {
      if (netId == NETWORK_ID) {
        var awaitContract = async function () {
          document.getElementById("web3_message").textContent="You are connected to Metamask"
          web3.eth.getAccounts(function(err, _accounts){
            accounts = _accounts
            if (err != null)
            {
              console.error("An error occurred: "+err)
            } else if (accounts.length > 0)
            {
              onWalletConnectedCallback()
              document.getElementById("account_address").style.display = "block"
            } else
            {
              document.getElementById("connect_button").style.display = "block"
            }
          });
        };
        awaitContract();
      } else {
        document.getElementById("web3_message").textContent="Please connect to Scroll";
      }
    });
  };
  awaitWeb3();
}

// Conecta tu wallet
async function connectWallet() {
  await window.ethereum.request({ method: "eth_requestAccounts" })
  accounts = await web3.eth.getAccounts()
  onWalletConnectedCallback()
}

// Callback de cuando tu wallet se conecta, en este ejemplo no la usamos
const onWalletConnectedCallback = async () => {
}

// Inicia la conexión
loadDapp()
Enter fullscreen mode Exit fullscreen mode

Instala un servidor web para correr tu dApp. Te recomiendo lite-server.

npm install -g lite-server
Enter fullscreen mode Exit fullscreen mode

Ahora corre tu dapp corriendo el siguiente comando en el mismo directorio en el que se encuentra el archivo index.html:

lite-server
Enter fullscreen mode Exit fullscreen mode

Tu dApp ahora está correiendo en http://127.0.0.1:3000.

4. Prueba tu Bot

Ahora vamos a probar el bot.

  1. Envía el comando authenticate al bot.
  2. Ve al sitio web que el bot te envió, conéctate a Scroll Mainnet y firma el mensaje con tu wallet.
  3. Revisa la respuesta del bot en Telegram.

telegram auth web3 message

Con este flujo es posible autenticar usuarios de forma segura y permitir acciones en su nombre sin almacenar claves privadas.

Próximos pasos

En este tutorial aprendimos cómo autenticar usuarios en Telegram usando una wallet Web3. Utilizamos el estándar ERC712 para firmar mensajes y el método eth_signTypedData_v4 para realizar la firma. También usamos la API de Telegram para enviar mensajes al usuario y recibir su respuesta.

En el próximo tutorial aprenderemos cómo usar esta autenticación para controlar el acceso a un grupo de Telegram.

¡Gracias por ver este artículo!

Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.

Top comments (3)

Collapse
 
gnurub profile image
Rubén

Me gustan tus tutos bro!

Collapse
 
turupawn profile image
Ahmed Castro

Gracias amigo!

Collapse
 
zyrav23 profile image
Zyra-V23

TOP! CAPO!