TON Connect for Telegram Bots
This guide explains an outdated method of integrating TON Connect with Telegram bots. For a more secure and modern approach, consider using [Telegram Mini Apps](/v3/guidelines/dapps/tma/overview for a more modern and secure integration.
In this tutorial, we will develop a sample Telegram bot using the JavaScript TON Connect SDK, supporting TON Connect 2.0 authentication. This guide covers wallet connections, sending transactions, retrieving wallet information, and disconnecting wallets.
Open Demo Bot
Check out GitHub
Documentation links
Prerequisites
- You need to create a telegram bot using @BotFather and save its token.
- Node JS should be installed (we use version 18.1.0 in this tutorial).
- Docker should be installed.
Creating project
Setting up dependencies
Start by creating a Node.js project. We will use TypeScript and the node-telegram-bot-api library, though you can choose an alternative library if preferred. Also, we will use qrcode library for QR codes generation, but you can replace it with any other same library.
Let's create a directory ton-connect-bot
. Add the following package.json file there:
{
"name": "ton-connect-bot",
"version": "1.0.0",
"scripts": {
"compile": "npx rimraf dist && tsc",
"run": "node ./dist/main.js"
},
"dependencies": {
"@tonconnect/sdk": "^3.0.0-beta.1",
"dotenv": "^16.0.3",
"node-telegram-bot-api": "^0.61.0",
"qrcode": "^1.5.1"
},
"devDependencies": {
"@types/node-telegram-bot-api": "^0.61.4",
"@types/qrcode": "^1.5.0",
"rimraf": "^3.0.2",
"typescript": "^4.9.5"
}
}
Run npm i
to install dependencies.
Add a tsconfig.json
Create a tsconfig.json
:
tsconfig.json code
{
"compilerOptions": {
"declaration": true,
"lib": ["ESNext", "dom"],
"resolveJsonModule": true,
"experimentalDecorators": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
"noUnusedLocals": false,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"useUnknownInCatchVariables": false,
"noUncheckedIndexedAccess": true,
"emitDecoratorMetadata": false,
"importHelpers": false,
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"allowJs": true,
"outDir": "./dist"
},
"include": ["src"],
"exclude": [
"./tests","node_modules", "lib", "types"]
}
Add simple bot code
Create a .env
file and add your bot token, DAppmanifest and wallets list cache time to live there:
See more about tonconnect-manifes.json
# .env
TELEGRAM_BOT_TOKEN=<YOUR BOT TOKEN, E.G 1234567890:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
TELEGRAM_BOT_LINK=<YOUR TG BOT LINK HERE, E.G. https://t.me/ton_connect_example_bot>
MANIFEST_URL=https://raw.githubusercontent.com/ton-connect/demo-telegram-bot/master/tonconnect-manifest.json
WALLETS_LIST_CACHE_TTL_MS=86400000
Create directory src
and file bot.ts
inside. Let's create a TelegramBot instance there:
// src/bot.ts
import TelegramBot from 'node-telegram-bot-api';
import * as process from 'process';
const token = process.env.TELEGRAM_BOT_TOKEN!;
export const bot = new TelegramBot(token, { polling: true });
Now we can create an entrypoint file main.ts
inside the src
directory:
// src/main.ts
import dotenv from 'dotenv';
dotenv.config();
import { bot } from './bot';
bot.on('message', msg => {
const chatId = msg.chat.id;
bot.sendMessage(chatId, 'Received your message');
});
Here we go. You can run npm run compile
and npm run start
, and send any message to your bot. Bot will reply "Received your message". We are ready for the TonConnect integration.
At the moment we have the following files structure:
ton-connect-bot
├── src
│ ├── bot.ts
│ └── main.ts
├── package.json
├── package-lock.json
├── .env
└── tsconfig.json
Connecting a wallet
After installing the @tonconnect/sdk
, we can begin by importing it to initialize wallet connections.
We will start with getting wallets list. We need only http-bridge-compatible wallets. Create a folder ton-connect
into src
and add wallets.ts
file there:
We also define function getWalletInfo
that queries detailed wallet info by its appName
.
The difference between name
and appName
is that name
is a human-readable label of the wallet, and appName
is the wallet's uniq identifier.
// src/ton-connect/wallets.ts
import { isWalletInfoRemote, WalletInfoRemote, WalletsListManager } from '@tonconnect/sdk';
const walletsListManager = new WalletsListManager({
cacheTTLMs: Number(process.env.WALLETS_LIST_CACHE_TTL_MS)
});
export async function getWallets(): Promise<WalletInfoRemote[]> {
const wallets = await walletsListManager.getWallets();
return wallets.filter(isWalletInfoRemote);
}
export async function getWalletInfo(walletAppName: string): Promise<WalletInfo | undefined> {
const wallets = await getWallets();
return wallets.find(wallet => wallet.appName.toLowerCase() === walletAppName.toLowerCase());
}
Now we need to define a TonConnect storage. TonConnect uses localStorage
to save connection details when running in the browser, however there is no localStorage
in NodeJS environment. That's why we should add a custom simple storage implementation.
See details about TonConnect storage
Create storage.ts
inside ton-connect
directory:
// src/ton-connect/storage.ts
import { IStorage } from '@tonconnect/sdk';
const storage = new Map<string, string>(); // temporary storage implementation. We will replace it with the redis later
export class TonConnectStorage implements IStorage {
constructor(private readonly chatId: number) {} // we need to have different stores for different users
private getKey(key: string): string {
return this.chatId.toString() + key; // we will simply have different keys prefixes for different users
}
async removeItem(key: string): Promise<void> {
storage.delete(this.getKey(key));
}
async setItem(key: string, value: string): Promise<void> {
storage.set(this.getKey(key), value);
}
async getItem(key: string): Promise<string | null> {
return storage.get(this.getKey(key)) || null;
}
}
We are moving on implementing a wallet connection.
Modify src/main.ts
and add connect
command. We are going to implement a wallet connection in this command handler.
import dotenv from 'dotenv';
dotenv.config();
import { bot } from './bot';
import { getWallets } from './ton-connect/wallets';
import TonConnect from '@tonconnect/sdk';
import { TonConnectStorage } from './ton-connect/storage';
import QRCode from 'qrcode';
bot.onText(/\/connect/, async msg => {
const chatId = msg.chat.id;
const wallets = await getWallets();
const connector = new TonConnect({
storage: new TonConnectStorage(chatId),
manifestUrl: process.env.MANIFEST_URL
});
connector.onStatusChange(wallet => {
if (wallet) {
bot.sendMessage(chatId, `${wallet.device.appName} wallet connected!`);
}
});
const tonkeeper = wallets.find(wallet => wallet.appName === 'tonkeeper')!;
const link = connector.connect({
bridgeUrl: tonkeeper.bridgeUrl,
universalLink: tonkeeper.universalLink
});
const image = await QRCode.toBuffer(link);
await bot.sendPhoto(chatId, image);
});
Let's analyze what we are doing here. Firstly we fetch the wallets list and create a TonConnect instance.
After that we subscribe to wallet change. When user connects a wallet, bot will send a message ${wallet.device.appName} wallet connected!
.
Next we find the Tonkeeper wallet and create connection link. In the end we generate a QR code with the link and send it as a photo to the user.
Now you can run the bot (npm run compile
and npm run start
then) and send /connect
message to the bot. Bot should reply with the QR. Scan it with the Tonkeeper wallet. You will see a message Tonkeeper wallet connected!
in the chat.
We will use connector in many places, so let's move connector creating code to a separate file:
// src/ton-connect/connector.ts
import TonConnect from '@tonconnect/sdk';
import { TonConnectStorage } from './storage';
import * as process from 'process';
export function getConnector(chatId: number): TonConnect {
return new TonConnect({
manifestUrl: process.env.MANIFEST_URL,
storage: new TonConnectStorage(chatId)
});
}
And import it in the src/main.ts
// src/main.ts
import dotenv from 'dotenv';
dotenv.config();
import { bot } from './bot';
import { getWallets } from './ton-connect/wallets';
import QRCode from 'qrcode';
import { getConnector } from './ton-connect/connector';
bot.onText(/\/connect/, async msg => {
const chatId = msg.chat.id;
const wallets = await getWallets();
const connector = getConnector(chatId);
connector.onStatusChange(wallet => {
if (wallet) {
bot.sendMessage(chatId, `${wallet.device.appName} wallet connected!`);
}
});
const tonkeeper = wallets.find(wallet => wallet.appName === 'tonkeeper')!;
const link = connector.connect({
bridgeUrl: tonkeeper.bridgeUrl,
universalLink: tonkeeper.universalLink
});
const image = await QRCode.toBuffer(link);
await bot.sendPhoto(chatId, image);
});
At the moment we have the following files structure:
bot-demo
├── src
│ ├── ton-connect
│ │ ├── connector.ts
│ │ ├── wallets.ts
│ │ └── storage.ts
│ ├── bot.ts
│ └── main.ts
├── package.json
├── package-lock.json
├── .env
└── tsconfig.json
Creating connect wallet menu
Add inline keyboard
We've done the Tonkeeper wallet connection. But we didn't implement connection via universal QR code for all wallets, and didn't allow the user to choose suitable wallet. Let's cover it now.
For better UX we are going to use callback_query
and inline_keyboard
Telegram features. If you don't fill familiar with that, you can read more about it here.
We will implement following UX for wallet connection:
First screen:
<Unified QR>
<Open @wallet>, <Choose a wallet button (opens second screen)>, <Open wallet unified link>
Second screen:
<Unified QR>
<Back (opens first screen)>
<@wallet button (opens third screen)>, <Tonkeeper button (opens third screen)>, <Tonhub button (opens third screen)>, <...>
Third screen:
<Selected wallet QR>
<Back (opens second screen)>
<Open selected wallet link>
Let's start with adding inline keyboard to the /connect
command handler in the main.ts
// src/main.ts
bot.onText(/\/connect/, async msg => {
const chatId = msg.chat.id;
const wallets = await getWallets();
const connector = getConnector(chatId);
connector.onStatusChange(async wallet => {
if (wallet) {
const walletName =
(await getWalletInfo(wallet.device.appName))?.name || wallet.device.appName;
bot.sendMessage(chatId, `${walletName} wallet connected!`);
}
});
const link = connector.connect(wallets);
const image = await QRCode.toBuffer(link);
await bot.sendPhoto(chatId, image, {
reply_markup: {
inline_keyboard: [
[
{
text: 'Choose a Wallet',
callback_data: JSON.stringify({ method: 'chose_wallet' })
},
{
text: 'Open Link',
url: `https://ton-connect.github.io/open-tc?connect=${encodeURIComponent(
link
)}`
}
]
]
}
});
});