Telegram 机器人的 TON Connect
本指南介绍了将 TON Connect 与 Telegram 机器人集成的过时方法。如需更安全、更现代的方法,请考虑使用 Telegram 小程序。
在本教程中,我们将使用支持 TON Connect 2.0 身份验证的 JavaScript TON Connect SDK 开发一个 Telegram 机器人示例。 本指南包括钱包连接、发送交易、检索钱包信息和断开钱包连接。
打开演示机器人
查看 GitHub
文件链接
先决条件
- 您需要使用 @BotFather 创建一个电报机器人,并保存其令牌。
- 应安装 Node JS(本教程中使用的是 18.1.0 版本)。
- 应安装 Docker。
设置依赖
设置依赖关系
让我们创建一个目录 ton-connect-bot
。在那里添加以下的 package.json 文件:
创建目录 ton-connect-bot
.在其中添加以下 package.json 文件:
{
"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"
}
}
运行 npm i
安装依赖项。
添加 tsconfig.json
创建 tsconfig.json
:
tsconfig.json 代码
{
"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"]
}
添加简单的机器人代码
了解更多关于 tonconnect-manifes.json
查看有关 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
创建目录 src
,并在其中创建文件 bot.ts
。让我们在这里创建一个 TelegramBot 实例:
// 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 });
现在,我们可以在 src
目录中创建一个入口点文件 main.ts
:
// 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');
});
目前我们有以下文件结构:
目前,我们的文件结构如下:
ton-connect-bot
├── src
│ ├── bot.ts
│ └── main.ts
├── package.json
├── package-lock.json
├── .env
└── tsconfig.json
连接钱包
我们将从获取钱包列表开始。我们只需要 http-bridge 兼容的钱包。在 src
中创建文件夹 ton-connect
并添加 wallets.ts
文件:
我们还定义了函数 getWalletInfo
来通过其 appName
查询详细的钱包信息。
name
和 appName
之间的区别是 name
是钱包的人类可读标签,而 appName
是钱包的唯一标识符。
我们将从获取钱包列表开始。我们只需要与 http 桥兼容的钱包。在 src
中创建文件夹 ton-connect
并在其中添加 wallets.ts
文件:
我们还定义了函数 getWalletInfo
,该函数通过 appName
查询钱包的详细信息。
name
和 appName
的区别在于, name
是钱包的可读标签,而 appName
是钱包的统一标识符。
// 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());
}
在 ton-connect
目录内创建 storage.ts
:
在 ton-connect
目录中创建 storage.ts
:
// 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;
}
}
我们继续实现钱包连接。
修改 src/main.ts
并添加 connect
命令。我们将在此命令处理程序中实现钱包连接。
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);
});
现在,你可以运行机器人(接着运行 npm run compile
和 npm run start
)并向机器人发送 /connect
信息。机器人应该会回复二维码。用Tonkeeper钱包扫描它。你将在聊天中看到 Tonkeeper wallet connected!
的信息。
我们会在许多地方使用连接器,因此让我们将创建连接器的代码移动到一个单独的文件中:
我们将在很多地方使用连接器,因此我们将创建连接器的代码移到一个单独的文件中:
// 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)
});
}
并在 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);
});
目前,我们的文件结构如下:
bot-demo
├── src
│ ├── ton-connect
│ │ ├── connector.ts
│ │ ├── wallets.ts
│ │ └── storage.ts
│ ├── bot.ts
│ └── main.ts
├── package.json
├── package-lock.json
├── .env
└── tsconfig.json
添加内联键盘
添加内嵌键盘
为了更好的用户体验,我们打算使用 Telegram 的 callback_query
和 inline_keyboard
功能。如果你不熟悉,可以在这里阅读更多。
我们将为钱包连接实现以下用户体验:
我们将为钱包连接实施以下用户体验:
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>
首先,让我们在 main.ts
中为 /connect
命令处理程序添加内联键盘
// 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
)}`
}
]
]
}
});
});
注意我们替换了 connector.connect
调用参数。现在我们为所有钱包生成一个统一链接。
接下来我们告诉 Telegram,在用户点击 选择钱包
按钮时以 { "method": "chose_wallet" }
值调用 callback_query
处理程序。
接下来 我们告诉 Telegram,在用户点击 选择钱包
按钮时以 { "method": "chose_wallet" }
值调用 callback_query
处理程序。
添加选择钱包按钮处理程序
让我们在那里添加“选择钱包”按钮点击处理程序:
让我们在这里添加 "选择钱包 "按钮点击处理程序:
// src/connect-wallet-menu.ts
async function onChooseWalletClick(query: CallbackQuery, _: string): Promise<void> {
const wallets = await getWallets();
await bot.editMessageReplyMarkup(
{
inline_keyboard: [
wallets.map(wallet => ({
text: wallet.name,
callback_data: JSON.stringify({ method: 'select_wallet', data: wallet.appName })
})),
[
{
text: '« Back',
callback_data: JSON.stringify({
method: 'universal_qr'
})
}
]
]
},
{
message_id: query.message!.message_id,
chat_id: query.message!.chat.id
}
);
}
现在我们将添加全局 callback_query
处理程序并在其中注册 onChooseWalletClick
:
现在,我们将添加全局 callback_query
处理程序,并在其中注册 onChooseWalletClick
:
// src/connect-wallet-menu.ts
import { CallbackQuery } from 'node-telegram-bot-api';
import { getWallets } from './ton-connect/wallets';
import { bot } from './bot';
export const walletMenuCallbacks = { // Define buttons callbacks
chose_wallet: onChooseWalletClick
};
bot.on('callback_query', query => { // Parse callback data and execute corresponing function
if (!query.data) {
return;
}
let request: { method: string; data: string };
try {
request = JSON.parse(query.data);
} catch {
return;
}
if (!walletMenuCallbacks[request.method as keyof typeof walletMenuCallbacks]) {
return;
}
walletMenuCallbacks[request.method as keyof typeof walletMenuCallbacks](query, request.data);
});
// ... other code from the previous ster
async function onChooseWalletClick ...
现在我们应该将 connect-wallet-menu.ts
导入到 src/main.ts
现在,我们应该在 main.ts
中添加 connect-wallet-menu.ts
导入
// src/main.ts
// ... other imports
import './connect-wallet-menu';
// ... other code
编译并运行机器人。您可以点击 "选择钱包 "按钮,机器人将取代内嵌键盘按钮!
添加其他按钮处理程序
首先,我们将创建一个工具函数 editQR
。编辑消息媒体(QR 图像)有点棘手。我们需要将图像存储到文件中并将其发送到 Telegram 服务器。然后我们可以删除这个文件。
首先,我们将创建一个实用程序函数 editQR
。编辑消息媒体(QR 图像)有点麻烦。我们需要将图片存储到文件中并发送 到 Telegram 服务器。然后我们可以删除该文件。
// src/connect-wallet-menu.ts
// ... other code
async function editQR(message: TelegramBot.Message, link: string): Promise<void> {
const fileName = 'QR-code-' + Math.round(Math.random() * 10000000000);
await QRCode.toFile(`./${fileName}`, link);
await bot.editMessageMedia(
{
type: 'photo',
media: `attach://${fileName}`
},
{
message_id: message?.message_id,
chat_id: message?.chat.id
}
);
await new Promise(r => fs.rm(`./${fileName}`, r));
}
在 onOpenUniversalQRClick
处理程序中,我们只需重新生成 QR 和 deeplink 并修改信息:
// src/connect-wallet-menu.ts
// ... other code
async function onOpenUniversalQRClick(query: CallbackQuery, _: string): Promise<void> {
const chatId = query.message!.chat.id;
const wallets = await getWallets();
const connector = getConnector(chatId);
connector.onStatusChange(wallet => {
if (wallet) {
bot.sendMessage(chatId, `${wallet.device.appName} wallet connected!`);
}
});
const link = connector.connect(wallets);
await editQR(query.message!, link);
await bot.editMessageReplyMarkup(
{
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
)}`
}
]
]
},
{
message_id: query.message?.message_id,
chat_id: query.message?.chat.id
}
);
}
// ... other code
在 onWalletClick
处理程序中,我们将为选定的钱包创建特殊的 QR 和通用链接,并修改信息。
// src/connect-wallet-menu.ts
// ... other code
async function onWalletClick(query: CallbackQuery, data: string): Promise<void> {
const chatId = query.message!.chat.id;
const connector = getConnector(chatId);
connector.onStatusChange(wallet => {
if (wallet) {
bot.sendMessage(chatId, `${wallet.device.appName} wallet connected!`);
}
});
const selectedWallet = await getWalletInfo(data);
if (!selectedWallet) {
return;
}
const link = connector.connect({
bridgeUrl: selectedWallet.bridgeUrl,
universalLink: selectedWallet.universalLink
});
await editQR(query.message!, link);
await bot.editMessageReplyMarkup(
{
inline_keyboard: [
[
{
text: '« Back',
callback_data: JSON.stringify({ method: 'chose_wallet' })
},
{
text: `Open ${selectedWallet.name}`,
url: link
}
]
]
},
{
message_id: query.message?.message_id,
chat_id: chatId
}
);
}
// ... other code
现在,我们必须将这些函数注册为回调函数(walletMenuCallbacks
):
// src/connect-wallet-menu.ts
import TelegramBot, { CallbackQuery } from 'node-telegram-bot-api';
import { getWallets } from './ton-connect/wallets';
import { bot } from './bot';
import * as fs from 'fs';
import { getConnector } from './ton-connect/connector';
import QRCode from 'qrcode';
export const walletMenuCallbacks = {
chose_wallet: onChooseWalletClick,
select_wallet: onWalletClick,
universal_qr: onOpenUniversalQRClick
};
// ... other code
目前,src/connect-wallet-menu.ts 看起来是这样的
// src/connect-wallet-menu.ts
import TelegramBot, { CallbackQuery } from 'node-telegram-bot-api';
import { getWallets, getWalletInfo } from './ton-connect/wallets';
import { bot } from './bot';
import { getConnector } from './ton-connect/connector';
import QRCode from 'qrcode';
import * as fs from 'fs';
export const walletMenuCallbacks = {
chose_wallet: onChooseWalletClick,
select_wallet: onWalletClick,
universal_qr: onOpenUniversalQRClick
};
bot.on('callback_query', query => { // Parse callback data and execute corresponing function
if (!query.data) {
return;
}
let request: { method: string; data: string };
try {
request = JSON.parse(query.data);
} catch {
return;
}
if (!callbacks[request.method as keyof typeof callbacks]) {
return;
}
callbacks[request.method as keyof typeof callbacks](query, request.data);
});
async function onChooseWalletClick(query: CallbackQuery, _: string): Promise<void> {
const wallets = await getWallets();
await bot.editMessageReplyMarkup(
{
inline_keyboard: [
wallets.map(wallet => ({
text: wallet.name,
callback_data: JSON.stringify({ method: 'select_wallet', data: wallet.appName })
})),
[
{
text: '« Back',
callback_data: JSON.stringify({
method: 'universal_qr'
})
}
]
]
},
{
message_id: query.message!.message_id,
chat_id: query.message!.chat.id
}
);
}
async function onOpenUniversalQRClick(query: CallbackQuery, _: string): Promise<void> {
const chatId = query.message!.chat.id;
const wallets = await getWallets();
const connector = getConnector(chatId);
connector.onStatusChange(wallet => {
if (wallet) {
bot.sendMessage(chatId, `${wallet.device.appName} wallet connected!`);
}
});
const link = connector.connect(wallets);
await editQR(query.message!, link);
await bot.editMessageReplyMarkup(
{
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
)}`
}
]
]
},
{
message_id: query.message?.message_id,
chat_id: query.message?.chat.id
}
);
}
async function onWalletClick(query: CallbackQuery, data: string): Promise<void> {
const chatId = query.message!.chat.id;
const connector = getConnector(chatId);
connector.onStatusChange(wallet => {
if (wallet) {
bot.sendMessage(chatId, `${wallet.device.appName} wallet connected!`);
}
});
const selectedWallet = await getWalletInfo(data);
if (!selectedWallet) {
return;
}
const link = connector.connect({
bridgeUrl: selectedWallet.bridgeUrl,
universalLink: selectedWallet.universalLink
});
await editQR(query.message!, link);
await bot.editMessageReplyMarkup(
{
inline_keyboard: [
[
{
text: '« Back',
callback_data: JSON.stringify({ method: 'chose_wallet' })
},
{
text: `Open ${selectedWallet.name}`,
url: link
}
]
]
},
{
message_id: query.message?.message_id,
chat_id: chatId
}
);
}
async function editQR(message: TelegramBot.Message, link: string): Promise<void> {
const fileName = 'QR-code-' + Math.round(Math.random() * 10000000000);
await QRCode.toFile(`./${fileName}`, link);
await bot.editMessageMedia(
{
type: 'photo',
media: `attach://${fileName}`
},
{
message_id: message?.message_id,
chat_id: message?.chat.id
}
);
await new Promise(r => fs.rm(`./${fileName}`, r));
}
您可能会注意到,我们还没有考虑到 QR 代码过期和停止连接器的问题。我们稍后会处理这个问题。
目前我们有以下文件结构:
目前,我们的文件结构如下:
bot-demo
├── src
│ ├── ton-connect
│ │ ├── connector.ts
│ │ ├── wallets.ts
│ │ └── storage.ts
│ ├── bot.ts
│ ├ ── connect-wallet-menu.ts
│ └── main.ts
├── package.json
├── package-lock.json
├── .env
└── tsconfig.json
执行交易发送
在编写发送事务的新代码之前,我们先清理一下代码。我们将为机器人命令处理程序创建一个新文件("/connect"、"/send_tx"...)
// src/commands-handlers.ts
import { bot } from './bot';
import { getWallets } from './ton-connect/wallets';
import QRCode from 'qrcode';
import TelegramBot from 'node-telegram-bot-api';
import { getConnector } from './ton-connect/connector';
export async function handleConnectCommand(msg: TelegramBot.Message): Promise<void> {
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 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
)}`
}
]
]
}
});
}
让我们在 main.ts
中导入它,并将 connect-wallet-menu.ts
中的 callback_query
入口点移到 main.ts
:
// src/main.ts
import dotenv from 'dotenv';
dotenv.config();
import { bot } from './bot';
import './connect-wallet-menu';
import { handleConnectCommand } from './commands-handlers';
import { walletMenuCallbacks } from './connect-wallet-menu';
const callbacks = {
...walletMenuCallbacks
};
bot.on('callback_query', query => {
if (!query.data) {
return;
}
let request: { method: string; data: string };
try {
request = JSON.parse(query.data);
} catch {
return;
}
if (!callbacks[request.method as keyof typeof callbacks]) {
return;
}
callbacks[request.method as keyof typeof callbacks](query, request.data);
});
bot.onText(/\/connect/, handleConnectCommand);
// src/connect-wallet-menu.ts
// ... imports
export const walletMenuCallbacks = {
chose_wallet: onChooseWalletClick,
select_wallet: onWalletClick,
universal_qr: onOpenUniversalQRClick
};
async function onChooseWalletClick(query: CallbackQuery, _: string): Promise<void> {
// ... other code
现在我们可以添加 send_tx
命令处理程序:
// src/commands-handlers.ts
// ... other code
export async function handleSendTXCommand(msg: TelegramBot.Message): Promise<void> {
const chatId = msg.chat.id;
const connector = getConnector(chatId);
await connector.restoreConnection();
if (!connector.connected) {
await bot.sendMessage(chatId, 'Connect wallet to send transaction');
return;
}
connector
.sendTransaction({
validUntil: Math.round(Date.now() / 1000) + 600, // timeout is SECONDS
messages: [
{
amount: '1000000',
address: '0:0000000000000000000000000000000000000000000000000000000000000000'
}
]
})
.then(() => {
bot.sendMessage(chatId, `Transaction sent successfully`);
})
.catch(e => {
if (e instanceof UserRejectsError) {
bot.sendMessage(chatId, `You rejected the transaction`);
return;
}
bot.sendMessage(chatId, `Unknown error happened`);
})
.finally(() => connector.pauseConnection());
let deeplink = '';
const walletInfo = await getWalletInfo(connector.wallet!.device.appName);
if (walletInfo) {
deeplink = walletInfo.universalLink;
}
await bot.sendMessage(
chatId,
`Open ${walletInfo?.name || connector.wallet!.device.appName} and confirm transaction`,
{
reply_markup: {
inline_keyboard: [
[
{
text: 'Open Wallet',
url: deeplink
}
]
]
}
}
);
}
我们来注册这个处理程序:
让我们注册这个处理程序:
// src/main.ts
// ... other code
bot.onText(/\/connect/, handleConnectCommand);
bot.onText(/\/send_tx/, handleSendTXCommand);
目前我们有以下文件结构:
目前,我们的文件结构如下:
bot-demo
├── src
│ ├── ton-connect
│ │ ├── connector.ts
│ │ ├── wallets.ts
│ │ └── storage.ts
│ ├── bot.ts
│ ├── connect-wallet-menu.ts
│ ├── commands-handlers.ts
│ └── main.ts
├── package.json
├── package-lock.json
├── .env
└── tsconfig.json
添加断开连接和显示已连接钱包的命令
这个命令的实现非常简单:
// src/commands-handlers.ts
// ... other code
export async function handleDisconnectCommand(msg: TelegramBot.Message): Promise<void> {
const chatId = msg.chat.id;
const connector = getConnector(chatId);
await connector.restoreConnection();
if (!connector.connected) {
await bot.sendMessage(chatId, "You didn't connect a wallet");
return;
}
await connector.disconnect();
await bot.sendMessage(chatId, 'Wallet has been disconnected');
}
export async function handleShowMyWalletCommand(msg: TelegramBot.Message): Promise<void> {
const chatId = msg.chat.id;
const connector = getConnector(chatId);
await connector.restoreConnection();
if (!connector.connected) {
await bot.sendMessage(chatId, "You didn't connect a wallet");
return;
}
const walletName =
(await getWalletInfo(connector.wallet!.device.appName))?.name ||
connector.wallet!.device.appName;
await bot.sendMessage(
chatId,
`Connected wallet: ${walletName}\nYour address: ${toUserFriendlyAddress(
connector.wallet!.account.address,
connector.wallet!.account.chain === CHAIN.TESTNET
)}`
);
}
并注册此命令:
// src/main.ts
// ... other code
bot.onText(/\/connect/, handleConnectCommand);
bot.onText(/\/send_tx/, handleSendTXCommand);
bot.onText(/\/disconnect/, handleDisconnectCommand);
bot.onText(/\/my_wallet/, handleShowMyWalletCommand);
编译并运行机器人,检查上述命令是否正确。
优化
我们已经完成了所有基本命令。但需要注意的是,每个连接器都会一直打开 SSE 连接,直到暂停为止。
此外,我们没有处理用户多次调用 /connect
,或调用 /connect
或 /send_tx
,但没有扫描 QR 的情况。我们应该设置超时并关闭连接,以节省服务器资源。
然后通知 用户 QR 或事务请求已过期。
发送交易优化
让我们创建一个实用程序,它可以封装一个承诺,并在指定超时后拒绝接受它:
// src/utils.ts
export const pTimeoutException = Symbol();
export function pTimeout<T>(
promise: Promise<T>,
time: number,
exception: unknown = pTimeoutException
): Promise<T> {
let timer: ReturnType<typeof setTimeout>;
return Promise.race([
promise,
new Promise((_r, rej) => (timer = setTimeout(rej, time, exception)))
]).finally(() => clearTimeout(timer)) as Promise<T>;
}
在 .env
中添加超时参数值
让我们在 .env
中添加一个超时参数值
# .env
TELEGRAM_BOT_TOKEN=<YOUR BOT TOKEN, LIKE 1234567890:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
MANIFEST_URL=https://raw.githubusercontent.com/ton-connect/demo-telegram-bot/master/tonconnect-manifest.json
WALLETS_LIST_CACHE_TTL_MS=86400000
DELETE_SEND_TX_MESSAGE_TIMEOUT_MS=600000
现在,我们将改进 handleSendTXCommand
函数,并将 tx 发送包入 pTimeout
中。
// src/commands-handlers.ts
// export async function handleSendTXCommand(msg: TelegramBot.Message): Promise<void> { ...
pTimeout(
connector.sendTransaction({
validUntil: Math.round(
(Date.now() + Number(process.env.DELETE_SEND_TX_MESSAGE_TIMEOUT_MS)) / 1000
),
messages: [
{
amount: '1000000',
address: '0:0000000000000000000000000000000000000000000000000000000000000000'
}
]
}),
Number(process.env.DELETE_SEND_TX_MESSAGE_TIMEOUT_MS)
)
.then(() => {
bot.sendMessage(chatId, `Transaction sent successfully`);
})
.catch(e => {
if (e === pTimeoutException) {
bot.sendMessage(chatId, `Transaction was not confirmed`);
return;
}
if (e instanceof UserRejectsError) {
bot.sendMessage(chatId, `You rejected the transaction`);
return;
}
bot.sendMessage(chatId, `Unknown error happened`);
})
.finally(() => connector.pauseConnection());
// ... other code
完整的 handleSendTXCommand 代码
export async function handleSendTXCommand(msg: TelegramBot.Message): Promise<void> {
const chatId = msg.chat.id;
const connector = getConnector(chatId);
await connector.restoreConnection();
if (!connector.connected) {
await bot.sendMessage(chatId, 'Connect wallet to send transaction');
return;
}
pTimeout(
connector.sendTransaction({
validUntil: Math.round(
(Date.now() + Number(process.env.DELETE_SEND_TX_MESSAGE_TIMEOUT_MS)) / 1000
),
messages: [
{
amount: '1000000',
address: '0:0000000000000000000000000000000000000000000000000000000000000000'
}
]
}),
Number(process.env.DELETE_SEND_TX_MESSAGE_TIMEOUT_MS)
)
.then(() => {
bot.sendMessage(chatId, `Transaction sent successfully`);
})
.catch(e => {
if (e === pTimeoutException) {
bot.sendMessage(chatId, `Transaction was not confirmed`);
return;
}
if (e instanceof UserRejectsError) {
bot.sendMessage(chatId, `You rejected the transaction`);
return;
}
bot.sendMessage(chatId, `Unknown error happened`);
})
.finally(() => connector.pauseConnection());
let deeplink = '';
const walletInfo = await getWalletInfo(connector.wallet!.device.appName);
if (walletInfo) {
deeplink = walletInfo.universalLink;
}
await bot.sendMessage(
chatId,
`Open ${walletInfo?.name || connector.wallet!.device.appName} and confirm transaction`,
{
reply_markup: {
inline_keyboard: [
[
{
text: 'Open Wallet',
url: deeplink
}
]
]
}
}
);
}
你可以将此参数设置为 5000
,编译并重新运行机器人,测试其行为。
您可以将该参数设置为 5000
,编译并重新运行机器人,测试其行为。
优化钱包连接流程
目前,我们通过钱包连接菜单步骤在每个导航上创建一个新的连接器。 这很糟糕,因为在创建新连接器时,我们并没有关闭之前的连接器连接。 让我们改进这种行为,为用户连接创建一个缓存映射。
src/ton-connect/connector.ts 代码
// src/ton-connect/connector.ts
import TonConnect from '@tonconnect/sdk';
import { TonConnectStorage } from './storage';
import * as process from 'process';
type StoredConnectorData = {
connector: TonConnect;
timeout: ReturnType<typeof setTimeout>;
onConnectorExpired: ((connector: TonConnect) => void)[];
};
const connectors = new Map<number, StoredConnectorData>();
export function getConnector(
chatId: number,
onConnectorExpired?: (connector: TonConnect) => void
): TonConnect {
let storedItem: StoredConnectorData;
if (connectors.has(chatId)) {
storedItem = connectors.get(chatId)!;
clearTimeout(storedItem.timeout);
} else {
storedItem = {
connector: new TonConnect({
manifestUrl: process.env.MANIFEST_URL,
storage: new TonConnectStorage(chatId)
}),
onConnectorExpired: []
} as unknown as StoredConnectorData;
}
if (onConnectorExpired) {
storedItem.onConnectorExpired.push(onConnectorExpired);
}
storedItem.timeout = setTimeout(() => {
if (connectors.has(chatId)) {
const storedItem = connectors.get(chatId)!;
storedItem.connector.pauseConnection();
storedItem.onConnectorExpired.forEach(callback => callback(storedItem.connector));
connectors.delete(chatId);
}
}, Number(process.env.CONNECTOR_TTL_MS));
connectors.set(chatId, storedItem);
return storedItem.connector;
}
当getConnector
被调用时,我们检查此chatId
(用户)是否已在缓存中存在一个存在的连接器。如果存在,我们重置清理超时并返回连接器。
这允许保持活跃用户的连接器在缓存中。如果缓存中没有连接器,我们创建一个新的,注册一个超时清理函数并返回此连接器。
为了使它工作,我们必须在.env
中添加一个新参数
为使其正常工作,我们必须在 .env
中添加一个新参数
# .env
TELEGRAM_BOT_TOKEN=<YOUR BOT TOKEN, LIKE 1234567890:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
MANIFEST_URL=https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json
WALLETS_LIST_CACHE_TTL_MS=86400000
DELETE_SEND_TX_MESSAGE_TIMEOUT_MS=600000
CONNECTOR_TTL_MS=600000
现在让我们在 handelConnectCommand 中使用它
src/commands-handlers.ts 代码
// src/commands-handlers.ts
import {
CHAIN,
isWalletInfoRemote,
toUserFriendlyAddress,
UserRejectsError
} from '@tonconnect/sdk';
import { bot } from './bot';
import { getWallets, getWalletInfo } from './ton-connect/wallets';
import QRCode from 'qrcode';
import TelegramBot from 'node-telegram-bot-api';
import { getConnector } from './ton-connect/connector';
import { pTimeout, pTimeoutException } from './utils';
let newConnectRequestListenersMap = new Map<number, () => void>();
export async function handleConnectCommand(msg: TelegramBot.Message): Promise<void> {
const chatId = msg.chat.id;
let messageWasDeleted = false;
newConnectRequestListenersMap.get(chatId)?.();
const connector = getConnector(chatId, () => {
unsubscribe();
newConnectRequestListenersMap.delete(chatId);
deleteMessage();
});
await connector.restoreConnection();
if (connector.connected) {
const connectedName =
(await getWalletInfo(connector.wallet!.device.appName))?.name ||
connector.wallet!.device.appName;
await bot.sendMessage(
chatId,
`You have already connect ${connectedName} wallet\nYour address: ${toUserFriendlyAddress(
connector.wallet!.account.address,
connector.wallet!.account.chain === CHAIN.TESTNET
)}\n\n Disconnect wallet firstly to connect a new one`
);
return;
}
const unsubscribe = connector.onStatusChange(async wallet => {
if (wallet) {
await deleteMessage();
const walletName =
(await getWalletInfo(wallet.device.appName))?.name || wallet.device.appName;
await bot.sendMessage(chatId, `${walletName} wallet connected successfully`);
unsubscribe();
newConnectRequestListenersMap.delete(chatId);
}
});
const wallets = await getWallets();
const link = connector.connect(wallets);
const image = await QRCode.toBuffer(link);
const botMessage = 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
)}`
}
]
]
}
});
const deleteMessage = async (): Promise<void> => {
if (!messageWasDeleted) {
messageWasDeleted = true;
await bot.deleteMessage(chatId, botMessage.message_id);
}
};
newConnectRequestListenersMap.set(chatId, async () => {
unsubscribe();
await deleteMessage();
newConnectRequestListenersMap.delete(chatId);
});
}
// ... other code
现在我们应该从connect-wallet-menu.ts
中的函数中移除connector.onStatusChange
订阅,
因为它们使用相同的连接器实例,且在handleConnectCommand
中一个订阅足够了。
现在,我们应该从 connect-wallet-menu.ts
函数中移除 connector.onStatusChange
订阅,
,因为它们在 handleConnectCommand
中使用了同一个连接器实例和一个订阅。
src/connect-wallet-menu.ts 代码
// src/connect-wallet-menu.ts
// ... other code
async function onOpenUniversalQRClick(query: CallbackQuery, _: string): Promise<void> {
const chatId = query.message!.chat.id;
const wallets = await getWallets();
const connector = getConnector(chatId);
const link = connector.connect(wallets);
await editQR(query.message!, link);
await bot.editMessageReplyMarkup(
{
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
)}`
}
]
]
},
{
message_id: query.message?.message_id,
chat_id: query.message?.chat.id
}
);
}
async function onWalletClick(query: CallbackQuery, data: string): Promise<void> {
const chatId = query.message!.chat.id;
const connector = getConnector(chatId);
const wallets = await getWallets();
const selectedWallet = wallets.find(wallet => wallet.name === data);
if (!selectedWallet) {
return;
}
const link = connector.connect({
bridgeUrl: selectedWallet.bridgeUrl,
universalLink: selectedWallet.universalLink
});
await editQR(query.message!, link);
await bot.editMessageReplyMarkup(
{
inline_keyboard: [
[
{
text: '« Back',
callback_data: JSON.stringify({ method: 'chose_wallet' })
},
{
text: `Open ${data}`,
url: link
}
]
]
},
{
message_id: query.message?.message_id,
chat_id: chatId
}
);
}
// ... other code
就是这样!编译并运行机器人,尝试调用两次 /connect
。
改进与 @wallet 的互动
首先,让我们创建一些实用函数:
首先,让我们创建一些实用功能:
// src/utils.ts
import { encodeTelegramUrlParameters, isTelegramUrl } from '@tonconnect/sdk';
export const AT_WALLET_APP_NAME = 'telegram-wallet';
// ... other code
export function addTGReturnStrategy(link: string, strategy: string): string {
const parsed = new URL(link);
parsed.searchParams.append('ret', strategy);
link = parsed.toString();
const lastParam = link.slice(link.lastIndexOf('&') + 1);
return link.slice(0, link.lastIndexOf('&')) + '-' + encodeTelegramUrlParameters(lastParam);
}
export function convertDeeplinkToUniversalLink(link: string, walletUniversalLink: string): string {
const search = new URL(link).search;
const url = new URL(walletUniversalLink);
if (isTelegramUrl(walletUniversalLink)) {
const startattach = 'tonconnect-' + encodeTelegramUrlParameters(search.slice(1));
url.searchParams.append('startattach', startattach);
} else {
url.search = search;
}
return url.toString();
}
由于我们在两个地方使用通用 QR 页面创建代码,我们将其移动到单独的函数中:
由于我们在两个地方使用通用 QR 页面创建代码,因此我们将其移至单独的功能中:
// src/utils.ts
// ... other code
export async function buildUniversalKeyboard(
link: string,
wallets: WalletInfoRemote[]
): Promise<InlineKeyboardButton[]> {
const atWallet = wallets.find(wallet => wallet.appName.toLowerCase() === AT_WALLET_APP_NAME);
const atWalletLink = atWallet
? addTGReturnStrategy(
convertDeeplinkToUniversalLink(link, atWallet?.universalLink),
process.env.TELEGRAM_BOT_LINK!
)
: undefined;
const 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)}`
}
];
if (atWalletLink) {
keyboard.unshift({
text: '@wallet',
url: atWalletLink
});
}
return keyboard;
}
在此,我们为第一屏幕(通用 QR 屏幕)添加了 @wallet 的单独按钮。剩下的工作就是在 connect-wallet 菜单和命令处理程序中使用该功能:
src/connect-wallet-menu.ts 代码
// src/connect-wallet-menu.ts
// ... other code
async function onOpenUniversalQRClick(query: CallbackQuery, _: string): Promise<void> {
const chatId = query.message!.chat.id;
const wallets = await getWallets();
const connector = getConnector(chatId);
const link = connector.connect(wallets);
await editQR(query.message!, link);
const keyboard = await buildUniversalKeyboard(link, wallets);
await bot.editMessageReplyMarkup(
{
inline_keyboard: [keyboard]
},
{
message_id: query.message?.message_id,
chat_id: query.message?.chat.id
}
);
}
// ... other code
src/commands-handlers.ts 代码
// src/commands-handlers.ts
// ... other code
export async function handleConnectCommand(msg: TelegramBot.Message): Promise<void> {
const chatId = msg.chat.id;
let messageWasDeleted = false;
newConnectRequestListenersMap.get(chatId)?.();
const connector = getConnector(chatId, () => {
unsubscribe();
newConnectRequestListenersMap.delete(chatId);
deleteMessage();
});
await connector.restoreConnection();
if (connector.connected) {
const connectedName =
(await getWalletInfo(connector.wallet!.device.appName))?.name ||
connector.wallet!.device.appName;
await bot.sendMessage(
chatId,
`You have already connect ${connectedName} wallet\nYour address: ${toUserFriendlyAddress(
connector.wallet!.account.address,
connector.wallet!.account.chain === CHAIN.TESTNET
)}\n\n Disconnect wallet firstly to connect a new one`
);
return;
}
const unsubscribe = connector.onStatusChange(async wallet => {
if (wallet) {
await deleteMessage();
const walletName =
(await getWalletInfo(wallet.device.appName))?.name || wallet.device.appName;
await bot.sendMessage(chatId, `${walletName} wallet connected successfully`);
unsubscribe();
newConnectRequestListenersMap.delete(chatId);
}
});
const wallets = await getWallets();
const link = connector.connect(wallets);
const image = await QRCode.toBuffer(link);
const keyboard = await buildUniversalKeyboard(link, wallets);
const botMessage = await bot.sendPhoto(chatId, image, {
reply_markup: {
inline_keyboard: [keyboard]
}
});
const deleteMessage = async (): Promise<void> => {
if (!messageWasDeleted) {
messageWasDeleted = true;
await bot.deleteMessage(chatId, botMessage.message_id);
}
};
newConnectRequestListenersMap.set(chatId, async () => {
unsubscribe();
await deleteMessage();
newConnectRequestListenersMap.delete(chatId);
});
}
// ... other code
现在,当用户点击第二屏(选择钱包)上的钱包按钮时,我们将正确处理 TG 链接:
src/connect-wallet-menu.ts 代码
// src/connect-wallet-menu.ts
// ... other code
async function onWalletClick(query: CallbackQuery, data: string): Promise<void> {
const chatId = query.message!.chat.id;
const connector = getConnector(chatId);
const selectedWallet = await getWalletInfo(data);
if (!selectedWallet) {
return;
}
let buttonLink = connector.connect({
bridgeUrl: selectedWallet.bridgeUrl,
universalLink: selectedWallet.universalLink
});
let qrLink = buttonLink;
if (isTelegramUrl(selectedWallet.universalLink)) {
buttonLink = addTGReturnStrategy(buttonLink, process.env.TELEGRAM_BOT_LINK!);
qrLink = addTGReturnStrategy(qrLink, 'none');
}
await editQR(query.message!, qrLink);
await bot.editMessageReplyMarkup(
{
inline_keyboard: [
[
{
text: '« Back',
callback_data: JSON.stringify({ method: 'chose_wallet' })
},
{
text: `Open ${selectedWallet.name}`,
url: buttonLink
}
]
]
},
{
message_id: query.message?.message_id,
chat_id: chatId
}
);
}
// ... other code
现在让我们在 send transaction
处理器中为 TG 链接添加返回策略:
现在,让我们在 "发送事务 "处理程序中为 TG 链接添加返回策略:
src/commands-handlers.ts 代码
// src/commands-handlers.ts
// ... other code
export async function handleSendTXCommand(msg: TelegramBot.Message): Promise<void> {
const chatId = msg.chat.id;
const connector = getConnector(chatId);
await connector.restoreConnection();
if (!connector.connected) {
await bot.sendMessage(chatId, 'Connect wallet to send transaction');
return;
}
pTimeout(
connector.sendTransaction({
validUntil: Math.round(
(Date.now() + Number(process.env.DELETE_SEND_TX_MESSAGE_TIMEOUT_MS)) / 1000
),
messages: [
{
amount: '1000000',
address: '0:0000000000000000000000000000000000000000000000000000000000000000'
}
]
}),
Number(process.env.DELETE_SEND_TX_MESSAGE_TIMEOUT_MS)
)
.then(() => {
bot.sendMessage(chatId, `Transaction sent successfully`);
})
.catch(e => {
if (e === pTimeoutException) {
bot.sendMessage(chatId, `Transaction was not confirmed`);
return;
}
if (e instanceof UserRejectsError) {
bot.sendMessage(chatId, `You rejected the transaction`);
return;
}
bot.sendMessage(chatId, `Unknown error happened`);
})
.finally(() => connector.pauseConnection());
let deeplink = '';
const walletInfo = await getWalletInfo(connector.wallet!.device.appName);
if (walletInfo) {
deeplink = walletInfo.universalLink;
}
if (isTelegramUrl(deeplink)) {
const url = new URL(deeplink);
url.searchParams.append('startattach', 'tonconnect');
deeplink = addTGReturnStrategy(url.toString(), process.env.TELEGRAM_BOT_LINK!);
}
await bot.sendMessage(
chatId,
`Open ${walletInfo?.name || connector.wallet!.device.appName} and confirm transaction`,
{
reply_markup: {
inline_keyboard: [
[
{
text: `Open ${walletInfo?.name || connector.wallet!.device.appName}`,
url: deeplink
}
]
]
}
}
);
}
// ... other code
就是这样。现在用户能够使用主屏幕上的特殊按钮连接 @wallet,我们也为 TG 链接提供了适当的返回策略。
添加一个永久存储空间
目前,我们将 TonConnect 会话存储在 Map 对象中。但你可能希望将其存储到数据库或其他永久存储中,以便在重新启动服务器时保存会话。 我们将使用 Redis 来实现,但你可以选择任何永久存储。
设置 redis
要使用 redis,你必须启动 redis 服务器。我们将使用 Docker 镜像:
docker run -p 6379:6379 -it redis/redis-stack-server:latest
现在将 redis 连接参数添加到 .env
。默认的 redis url 是 redis://127.0.0.1:6379
。
现在在 .env
中添加 redis 连接参数。默认的 redis 网址是 redis://127.0.0.1:6379
。
# .env
TELEGRAM_BOT_TOKEN=<YOUR BOT TOKEN, LIKE 1234567890:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>
MANIFEST_URL=https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json
WALLETS_LIST_CACHE_TTL_MS=86400000
DELETE_SEND_TX_MESSAGE_TIMEOUT_MS=600000
CONNECTOR_TTL_MS=600000
REDIS_URL=redis://127.0.0.1:6379
让我们将 redis 集成到 TonConnectStorage
中:
// src/ton-connect/storage.ts
import { IStorage } from '@tonconnect/sdk';
import { createClient } from 'redis';
const client = createClient({ url: process.env.REDIS_URL });
client.on('error', err => console.log('Redis Client Error', err));
export async function initRedisClient(): Promise<void> {
await client.connect();
}
export class TonConnectStorage implements IStorage {
constructor(private readonly chatId: number) {}
private getKey(key: string): string {
return this.chatId.toString() + key;
}
async removeItem(key: string): Promise<void> {
await client.del(this.getKey(key));
}
async setItem(key: string, value: string): Promise<void> {
await client.set(this.getKey(key), value);
}
async getItem(key: string): Promise<string | null> {
return (await client.get(this.getKey(key))) || null;
}
}
为了使其正常工作,我们必须在 main.ts
中等待 redis 初始化。让我们将该文件中的代码封装为一个异步函数:
// src/main.ts
// ... imports
async function main(): Promise<void> {
await initRedisClient();
const callbacks = {
...walletMenuCallbacks
};
bot.on('callback_query', query => {
if (!query.data) {
return;
}
let request: { method: string; data: string };
try {
request = JSON.parse(query.data);
} catch {
return;
}
if (!callbacks[request.method as keyof typeof callbacks]) {
return;
}
callbacks[request.method as keyof typeof callbacks](query, request.data);
});
bot.onText(/\/connect/, handleConnectCommand);
bot.onText(/\/send_tx/, handleSendTXCommand);
bot.onText(/\/disconnect/, handleDisconnectCommand);
bot.onText(/\/my_wallet/, handleShowMyWalletCommand);
}
main();
摘要
下一步是什么?
- 如果要在生产环境中运行机器人,可能需要安装和使用进程管理器,如 pm2。
- 您可以在机器人中添加更好的错误处理功能。