Telegram 机器人的 TON Connect
在本教程中,我们将创建一个示例 Telegram 机器人,使用 Javascript TON Connect SDK 支持 TON Connect 2.0 认证。 我们将分析连接钱包、发送交易、获取关于已连接钱包的数据以及断开钱包连接。
打开演示机器人
查看 GitHub
文档链接
必要条件
- 你需要使用 @BotFather 创建一个 telegram 机器人,并保存其令牌。
- 应安装 Node JS(本教程中我们使用版本 18.1.0)。
- 应安装 Docker。
创建项目
设置依赖
首先,我们要创建一个 Node JS 项目。我们将使用 TypeScript 和 node-telegram-bot-api 库(你也可以选择任何适合你的库)。此外,我们将使用 qrcode 库生成 QR 码,但你可以用任何其他同类库替换。
让我们创建一个目录 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"]
}
添加简单的机器人代码
创建一个 .env 文件,并在其中添加你的机器人令牌、DAppmanifest 和钱包列表缓存存活时间:
了解更多关于 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');
});
我们可以了。你可以运行 npm run compile 和 npm run start,然后给你的机器人发送任何消息。机器人将回复“Received your message”。我们准备好进行 TonConnect 集成了。
目前我们有以下文件结构:
ton-connect-bot
├── src
│   ├── bot.ts
│   └── main.ts
├── package.json
├── package-lock.json
├── .env
└── tsconfig.json
连接钱包
我们已经安装了 @tonconnect/sdk,因此我们可以直接导入它开始使用。
我们将从获取钱包列表开始。我们只需要 http-bridge 兼容的钱包。  在 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());
}
现在我们需要定义一个 TonConnect 存储。TonConnect 在浏览器中运行时使用 localStorage 来保存连接详情,但是 NodeJS 环境中没有 localStorage。这就是为什么我们应该添加一个自定义的简单存储实现。
在 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);
});
让我们分析一下我们在这里做的事情。首先我们获取钱包列表并创建一个TonConnect实例。
之后,我们订阅钱包变化。当用户连接一个钱包时,机器人会发送信息 ${wallet.device.appName} wallet connected!。
接下来我们找到Tonkeeper钱包并创建连接链接。最后我们生成一个带有链接的二维码,并将其作为照片发送给用户。
现在,你可以运行机器人(接着运行 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
创建连接钱包菜单
添加内联键盘
我们已经完成了Tonkeeper钱包的连接。但我们还没有实现通过通用二维码连接所有钱包,并且没有允许用户选择合适的钱包。现在让我们来实现它。
为了更好的用户体验,我们打算使用 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>
让我们开始在 src/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
                        )}`
                    }
                ]
            ]
        }
    });
});
我们需要将TonConnect深层链接包装为https://ton-connect.github.io/open-tc?connect=${encodeURIComponent(link)},因为在Telegram内联键盘中只允许使用`http`链接。 网站https://ton-connect.github.io/open-tc仅将用户重定向到`connect`查询参数中传递的链接,因此这只是在Telegram中打开`tc://`链接的一种间接方式。
注意我们替换了 connector.connect 调用参数。现在我们为所有钱包生成一个统一链接。
接下来我们告诉 Telegram,在用户点击 选择钱包 按钮时以 { "method": "chose_wallet" } 值调用 callback_query 处理程序。
添加选择钱包按钮处理程序
创建文件 src/connect-wallet-menu.ts。
让我们在那里添加“选择钱包”按钮点击处理程序:
// 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:
// 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 ...
我们在这里定义了按钮处理程序列表和 callback_query 解析器。不幸的是,回调数据总是字符串,因此我们必须将JSON传递给 callback_data,并在 callback_query 处理程序中稍后解析它。
然后我们查找请求的方法,并使用传递的参数调用它。
现在我们应该 将 connect-wallet-menu.ts 导入到 src/main.ts
// src/main.ts
// ... other imports
import './connect-wallet-menu';
// ... other code
编译并运行机器人。你现在可以点击选择钱包按钮,机器人将替换内联键盘按钮!
添加其他的 buttons handlers
让我们完成这个菜单并添加其余的命令处理程序。
首先,我们将创建一个工具函数 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 中导入并将 callback_query 入口点从 connect-wallet-menu.ts 移动到 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
                        }
                    ]
                ]
            }
        }
    );
}
这里我们检查用户的钱包是否已连接并处理发送交易。 然后我们向用户发送一条带有按钮的消息,该按钮打开用户的钱包(钱包通用链接,无需额外参数)。 请注意,这个按钮包含一个空的deeplink。这意味着发送交易请求数据通过http-bridge传输,即使用户只是打开钱包应用而没有点击这个按钮,交易也会出现在用户的钱包中。
我们来注册这个处理程序:
// 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 / 交易请求已过期。
发送交易优化
让我们创建一个实用函数,将一个promise包装起来,并在指定的超时后拒绝它:
// 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
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
                        }
                    ]
                ]
            }
        }
    );
}
如果用户在 DELETE_SEND_TX_MESSAGE_TIMEOUT_MS(10分钟)内未确认交易,交易将被取消并且机器人将发送消息“Transaction was not confirmed”。
你可以将此参数设置为 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
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
现在让我们在handleConnectCommand中使用它
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
我们定义了 newConnectRequestListenersMap 来存储每个用户最后一次连接请求的清理回调。
如果用户多次调用 /connect,机器人将删除之前带有二维码的消息。
此外,我们还订阅了连接器到期超时,以便在二维码消息过期时将其删除。
现在我们应该从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 的交互
从 v3 版本开始,TonConnect 支持连接到如 @wallet 这样的 TWA 钱包。目前在教程中,机器人可以连接到 @wallet。
然而,我们应该改进重定向策略以提供更好的用户体验。此外,让我们在第一个(“Universal QR”)屏幕上添加 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();
}
TonConnect 在 Telegram 链接中的参数需要以特殊方式编码,这是为什么我们使用 encodeTelegramUrlParameters 来编码返回策略参数。
我们将使用 addTGReturnStrategy 为 @wallet 提供正确的返回 URL 至演示机器人。
由于我们在两个地方使用通用 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-menu 和 command-handlers 中使用这个函数:
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
请注意,我们将不同的链接放置在 QR 和按钮链接(qrLink 和 buttonLink)中,
因为当用户通过@wallet扫描 QR 时我们不需要重定向,而同时当用户使用按钮链接连接@wallet时我们需要返回到机器人。
现在让我们在 send transaction 处理器中为 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
首先运行 npm i 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
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。
- 你可以在机器人中增加更好的错误处理。