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:
- Crear un bot de Telegram seguro: Construir un bot que autentique usuarios mediante firmas de wallets Web3 sin almacenar llaves privadas.
- Implementar firmas ERC-712: Crear un flujo de autenticación donde el usuario firma mensajes en un formato legible.
- 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.
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.
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
Now, let's create the .env
file with the following variables:
.env
BOT_TOKEN=your_telegram_bot_token
CHAIN_ID=534352
WEB_DAPP_URL=http://localhost:3000
No olivdés crear el archivo .gitignore
para evitar subir el archivo .env
a un repositorio público.
.gitignore
.env
node_modules
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...");
Ahora corre el siguiente comando desde el mismo directorio donde se encuentra el archivo bot.js
. Esto iniciariá el bot:
node bot.js
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.
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
</span>
lang="en">
charset="utf-8">
id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: none">
Telegram Bot Authentication
id="web3_message">
Sign Authentication Request
type="button" id="sign" onclick="_signMessage()">Sign
id="signature">
<span class="na">type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js">
<span class="na">type="text/javascript" src="wallet_connection.js">
<span class="na">type="text/javascript" src="signature_messaging.js">
// 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.")
}
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.
Envía el comando authenticate al bot.
Ve al sitio web que el bot te envió, conéctate a Scroll Mainnet y firma el mensaje con tu wallet.
Revisa la respuesta del bot en Telegram.
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.