LXDAO Expert WG|Telegram & TON 生态开发者入门

文章一共9000字左右,一共分为 5 个部分,阅读大概需要 20分钟。

Expert WG:主要由各个领域资深专家组成。工作内容包括但不限于:专家咨询、Code Review、专题研究、技术分享等。

导读
——撰文 |  Lewis、Howe、Kahn 自 2023 年 7 月份以来,Unibot 的火爆,带起了一波 "Telegram Crypto" 的热潮,再加上 Telegram Wallet 的上线,将 TON 再次带回了大家的视线。不同于 2018 年 TON 在 Web3 行业的中文关注者比较少,本次热潮吸引来了众多中文开发者,为此 LXDAO 社区也为大家带来了一些 Telegram & TON 生态开发者入门知识,带你了解这个生态。

正文

文章一共9000字左右,一共分为 5 个部分,阅读大概需要 20分钟。

01 Telegram 应用

Telegram(非正式简称TG、电报、纸飞机)是跨平台的即时通信软件,官方提供手机版(Android、iOS、Windows Phone)、桌面版(Windows、macOS、Linux)和网页版等多种平
台客户端;同时开放应用程序接口(API),因此拥有许多第三方的客户端可供选择。2013 年 8 月,Pavel Durov 与 Nikolai Durov 兄弟创立了 Telegram 。至今,Telegram 仍在稳步发展, 其用户规模持续扩大,也在不断优化产品和功能,月均活跃超 8.6 亿人。

1.1 Telegram Bot

1.1.1 简介

Telegram Bot 可以理解为完全运行在 Telegram 应用程序中的“插件”。用户通过灵活的界面与 Bot 进行交互,这些界面可以支持任何类型的任务或服务。你可以给它发送指令让它完成操作或是实现一些功能(付钱、游戏等等),或者可以在 Channel 或 Group 中发送特定消息。

1.1.2 开发相关

Telegram Bot 的技术原理基于 Telegram Bot API,这是一个基于 HTTP 的接口,允许开发者通过简单的 HTTPS 请求来控制他们的 Bots。下面是一些核心要点:

  • 创建 Bot:开发者首先需要通过与 Telegram 的 @BotFather(一个官方的 Bot,用于创建和管理其他 Bots)的交互来创建一个新的 Bot。BotFather 会提供一个 token,这个 token 是访问 Telegram Bot API 并控制 Bot 的钥匙。

  • Bot API:Telegram Bot API 提供了一系列的方法,允许 Bot 接收消息、发送消息、修改消息、处理回调查询等。开发者可以通过编程方式调用这些方法来控制 Bot 的行为。

  • 消息更新:Bot 可以通过两种方式获取消息更新:Webhooks 和轮询(polling)。

    • Webhooks:开发者可以设置一个 URL(Webhook),Telegram 会在有新的消息时推送更新到这个 URL。

    • Polling:Bot 通过周期性地向 Telegram Bot API 请求未处理的更新来获取消息。

  • 编程语言:开发者可以使用任何能够发送 HTTPS 请求的编程语言来创建和管理 Telegram Bot。常用的语言包括 Python、JavaScript、Java、PHP 等。

  • 托管:Bot 运行在开发者自己的服务器上,而不是 Telegram 的服务器。这意味着开发者需要负责维护 Bot 的服务器和网络基础设施。

  • 集成:Bot 可以集成各种外部服务,例如数据库、支付网关、其他 API 服务等,以实现更复杂的功能。

Telegram Bot 的核心优势在于其简单性和高度的可定制性,使得开发者可以快速地开发出功能丰富的 Bots 来满足各种需求。

02 TON 链

TON 是一个去中心化和开放的互联网平台,由几个组件组成。其中包括:TON 区块链、TON DNS、TON Storage 和 TON Site。TON 区块链是将 TON 的底层基础设施连接在一起以形成更大的 TON 生态系统的核心协议。
TON 专注于实现广泛的跨链互操作性,同时在高度可扩展的安全框架中运行。TON 旨在每秒处理数百万笔交易 (TPS),目标是最终达到数亿用户。
TON Blockchain 被设计为分布式超级计算机或“超级服务器”,旨在提供各种产品和服务,为新互联网的去中心化愿景的发展做出贡献。

2.1  TON 技术特点

  • 多区块链架构:TON 采用了一种独特的多区块链架构,包括一个主链和多个附属链(工作链、分片链、虚拟账户链)。这种设计允许系统在保持高度安全性的同时,实现极高的可扩展性和效率。相对于比特币和以太坊主要基于单一区块链架构,有明显区别。

  • 安全性和去中心化:TON 强调网络的安全性和去中心化特性,采用了先进的加密技术和分布式共识机制来保护网络免受攻击,并确保去中心化。

  • 自适应分片机制:TON 的分片机制是自适应的,意味着它可以根据网络负载动态调整分片数量,从而优化处理能力和资源利用率。

  • 异步通信机制:在 TON 中,节点之间的通信是异步的。这意味着节点在发送消息或执行交易时不需要等待即时的响应,从而大大提高了网络的效率和响应速度。相对于其他区块链的同步调用,TON 的异步通信机制可谓独树一帜,开发者需要重点理解此机制。

  • 高吞吐量和可扩展性:TON 旨在处理大量的交易和活动,具有高吞吐量和低延迟的特点。它通过动态分片和优化的区块生成算法来实现这一目标,理论上能够支持数百万 TPS(每秒交易数)。

以下是 TON、Ethereum、Solana 的一些数据对比:

2.2  TON 钱包

用户要想使用 TON 区块链上的资产必然离不开钱包,目前 TON 的钱包有 TonKeeper、Wallet、TonSpace、Tonhub、MyTonWallet、OpenMask、Crypto Bot、Trust Wallet、SafePal 等。篇幅有限这里着重介绍 TonKeeper、Wallet、TonSpace 这三款钱包 。

2.2.1 TonKeeper

介绍:保护您的加密资产、NFT 并轻松访问 TON 应用程序。
支持平台:苹果、安卓、Chrome、Firefox等。
托管:非托管。
下载入口:官网(https://tonkeeper.com/)下载钱包

2.2.2 Wallet

介绍:Wallet 是 Telegram 的认证钱包,可以直接在 Telegram 中打开。
支持平台:Telegram
托管:托管
入口:Telegram 搜索框中输入 @Wallet

2.2.3 TON Space

介绍:集成在 Wallet 中的自托管钱包,无法在同一个账号的不同终端中共享信息。
支持平台: Telegram
托管:自托管
入口:Wallet 中选择 TON Space

03 TON和 Telegram 的关系

TON 最初是由 Telegram 的创始人 Pavel 和 Nikolai Durov 兄弟为 Telegram 设计的 Layer 1 链。2019 年,受到美国证券交易委员会 (SEC) 起诉,导致 TON 的发展受阻。2020 年,Telegram 与 SEC 达成和解,停止了 TON 的开发。后来开源社区接手了 TON 的开发, 开源社区后更名为 TON 基金会。

2023 年 9 月 Telegram 和 TON 基金会,宣布双方达成正式合作关系,整合了 TON 所推出的自托管加密钱包“TON Space” 。两者的合作是双赢关系, Telegram 为 TON 带来用户,得益于 Telegram 的 8 亿的海量用户, TON 也存在被 Mass Adoption (大规模采用) 的可能;TON 为在 Telegram 上构建 DApp 的开发者提供了 Web3 基础设施的支持。

TON 官方公布的生态系统中已经收录了 500 多个应用程序,这些应用覆盖了质押、钱包、浏览器、跨链桥、公共设施、NFT、社交、游戏、去中心化交易所、游戏等多个领域。

04 Telegram Bot  与 TON 集成

4.1 生态对象调用流程图

以下是一张简单的 Telegram Bot 与 TON 生态对象调用流程图,理清它们之间的调用关系更有助于早期技术学习。

  • TON Blockchain 指的是 TON 链。

  • TON Client 指的是应用程序与 TON 链交互的 SDK。

  • Bot 的存在可以有多种形式。

  • 后端集成 Bot 是打通 Telegram 各个功能的关键。

  • 与 TON 进行交互,可以是 Mini App,也可以是 Bot 后端。

4.2 Telegram Bot 创建

在 Telegram 搜索框输入 @BotFather, 点击进入,然后输入 "/newbot" 创建新 Bot, 保存 token、link等信息。

4.3 Telegram Bot 开发

下面是一个 Bot 的开发流程讲解:

  1. 首先 Clone 仓库( https://github.com/ton-connect/demo-telegram-bot )到本地。

  2. 按照 4.2 指引创建一个 telegram Bot。

  3. 拷贝.env.example 为 .env 文件, 修改 TELEGRAM_BOT_TOKEN、TELEGRAM_BOT_LINK 地址修改成 4.2 中获取的值, MANIFEST_URL 则是传递元信息给钱包, 包括 url、name、iconUrl、 termsOfUseUrl、privacyPolicyUrl等内容。

  1. 打开工程,查看 src 下的目录结

    ├── bot.ts
    ├── commands-handlers.ts
    ├── connect-wallet-menu.ts
    ├── main.ts
    ├── ton-connect
    │   ├── connector.ts
    │   ├── storage.ts
    │   └── wallets.ts
    └── utils.ts
  2. Telegram Bot 对接

    • 先查看 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 });

这里引入了 node-telegram-bot-api 包,导入了 token 的环境变量 。“new TelegramBot(token, { polling: true })” ,参数 token 为 Telegram  Bot 的 toekn, “{ polling : ture}” 表示消息轮训的方式采用轮询的方式。

  • 再看 main.ts 文件
async function main(): Promise<void> {
    ... 
    bot.onText(/\/start/, (msg: TelegramBot.Message) => {
        bot.sendMessage(
            msg.chat.id,
            `
This is an example of a telegram bot for connecting to TON wallets and sending transactions with TonConnect.

Commands list: 
/connect - Connect to a wallet
/my_wallet - Show connected wallet
/send_tx - Send transaction
/disconnect - Disconnect from the wallet

GitHub: https://github.com/ton-connect/demo-telegram-bot
`
        );
    });
}

这里是 Bot 处理命令的例子, bot.onText 为 TelegramBot 命名空间下的 onText 函数。“/\start/” 对应的命令, 后面为其对应的处理函数。使用 “ bot.sendMessage” 给当前的 msg.chat.id  会话发送信息。

4.4 Example 1: Telegram Bot 集成 Wallet

接下来根据 TON 文档(https://docs.ton.org/develop/dapps/ton-connect/tg-bot-integration),看一个 Telegram Bot 集成 Wallet 的例子。下面是一个调用流程图。

TON Connect:  支持 TON 生态系统中的钱包和应用程序之间的通信的组件。

Wallets:红色图标钱包可以是 TON 钱包段落提到的。

TON BlockChain: 指的是 TON 链。

EVM BlockChain:指的是 EVM 兼容链(现在有很多 Apps Center 中的钱包就是支持以太坊生态的)。

4.4.1 代码讲解

  1. 先看 tonconnect/connector.ts 文件
import TonConnect from '@tonconnect/sdk';
...
export function getConnector(
    chatId: number,
    onConnectorExpired?: (connector: TonConnect) => void
): TonConnect {
    ... 
        storedItem = {
            connector: new TonConnect({
                manifestUrl: process.env.MANIFEST_URL,
                storage: new TonConnectStorage(chatId)
            }),
            onConnectorExpired: []
        } as unknown as StoredConnectorData;

    ...
}
  1. 再看 wallet.ts 文件
import { isWalletInfoRemote, WalletInfoRemote, WalletsListManager } from '@tonconnect/sdk';

export async function getWallets(): Promise<WalletInfoRemote[]> {
    const wallets = await walletsListManager.getWallets();
    return wallets.filter(isWalletInfoRemote);
}
  1. 然后看 commands-handlers.ts 文件
...
import { getConnector } from './ton-connect/connector';
...

export async function handleConnectCommand(msg: TelegramBot.Message): Promise<void> {
    ... 
    const connector = getConnector(chatId, () => {
        unsubscribe();
        newConnectRequestListenersMap.delete(chatId);
        deleteMessage();
    });
    ...

    const wallets = await getWallets();

    const link = connector.connect(wallets);

    ...
 }

handleConnectCommand 为连接钱包命令的处理函数, 使用初始化 TonnConnect, 等待选择钱包后,在进行连接钱包。

4.4.2 运行效果

输入“/connect”命令后,对应选择钱包的效果,点击 @wallet 后(也可选择 Choose Wallet 按钮),弹出。

点击 Wallet 之后,就连接上了 wallet,然后 Bot 中显示如下内容。

4.5 TON 智能合约开发语言

对于公链而言,智能合约就是生态的核心,编程语言、性能、安全等都是开发者比较关心的,那么 TON 智能合约的开发语言是怎么样呢?以下主要介绍 FunC 和 Tact。

4.5.1 FunC

FunC 是一种专门针对 TON 智能合约开发的编程语言,具备以下特性:

  • 原子类型:

  • int:表示 257 位的有符号整数,自带溢出检测功能。

  • cell:代表 TVM(区块链虚拟机)单元,是 TON 区块链的基础数据存储结构。每个单元可以包含最多 1023 位数据和四个引用。

  • slice:单元片段,用于操作 cell 中的数据和引用。

  • builder:单元构建器,允许存储数据和引用,并最终构建成一个新的 cell。

  • tuple:是 TVM 中的元组类型,最多可包含 255 个不同类型的元素。

  • cont:是 TVM 的延续的类型,用于控制 TVM 程序的执行流程。

  • 布尔类型的处理:

  • 在 FunC 中,布尔值以整数形式表示,0 代表假,-1(257 个二进制1 )代表真。

  • 空值处理:

  • 用 Null 类型来表示某个原子类型值的缺失。某些函数可能会返回空值。

  • 类型推断:

  • 使用 _ 和 var 表示类型空缺,这些空缺会在类型检查过程中被实际类型填充。

  • 复合类型:

  • 函数类型:如 A -> B,表示从 A 类型到 B 类型的函数。

  • 张量类型:如 (A, B, ...),表示多个有序的 A、B 等类型值的集合。

  • 元组类型:如 [A, B, ...],表示具有特定长度和组件类型的 TVM 元组。

  • 泛型和类型变量:

  • 支持泛型函数和类型变量。例如,forall X -> (X, X) duplicate(X value) 是一个泛型函数。

  • 用户自定义类型:

  • 目前,FunC 不支持定义除上述类型构造之外的类型。

  • 类型宽度

  • 类型宽度指的是一个类型的所有值占用的堆栈条目数量。目前,只有已知固定宽度的类型可以定义泛型函数。

总结来说,FunC 是一个专为区块链设计的语言,其独特的类型系统和结构特别适用于处理 TON 区块链上的复杂数据和操作。

4.5.2 Tact

Tact 注重效率和简单性,且其设计易于学习和使用,非常适合智能合约。同时 Tact 是一种静态类型语言,具有简单的语法和强大的类型系统。

  • 类型

    • 基本类型

      • Int:所有整数都是 257 位有符号整数。
      • Bool:经典的布尔类型,具有真 / 假值。
      • Address:标准地址。
      • Slice、Cell、Builder:TVM 的低级原语。
      • String:代表 TON VM 中的文本字符串。
      • StringBuilder:辅助类型,允许以高效的方式连接字符串。
  • 映射:用于将数据与相应的键关联的类型。键和值的类型有限,包括整数、布尔、单元、地址以及结构体/消息。

  • 结构体和消息:结构体和消息几乎相同,唯一的区别是消息在其序列化中有一个头部,因此可以用作接收器。同时需要注意的是结构体和消息目前不支持循环类型。

  • 合约:合约是 TON 区块链上智能合约的主要入口,包含初始化函数、获取器和接收器。同时合约中可以定义变量,通过 receive 函数处理外部调用,且有助于实现状态管理。

  • 特性:引入了 “特质” 的概念,定义了函数、接收器和必需字段。但不规定字段存储的方式。所有特性中的字段必须在合同中显式声明。特性本身没有构造函数,所有字段的初始化也必须在主合同中完成。

4.5.3 FunC 与 Tact 的关系

由于 FunC 的学习门槛比较高,所以 TON 社区开发了类似于 TypeScript 和 Rust 的高级编程语言 Tact。

由此我们可以得出 FunC 与 Tact 之间的关系如下:
FunC — compile to --> Fift
Tact — compile to --> FunC — compile to --> Fift

这里提到的 Fift 是一种基于堆栈的通用编程语言,针对创建、调试和管理 TON 区块链智能合约进行了优化。Fift 专门设计用于与 TON 虚拟机(TVM)和 TON 区块链交互。

4.6 TON 简单合约源码分析

下面是一个参考 TON 文档(https://ton-community.github.io/tutorials/02-contract/)的简单例子,完整代码可参考:GitHub - DestinyWei/Ton-Counter: TON Counter Contract,接下来我们进行源码分析。

  1. 首先我们需要查看配置文件示例.env.example,复制创建名为.env的文件****
    MNEMONIC= word1 word2 word3 word4

MNEMONIC 填写的是 TON 钱包的助记词,这里使用的是 Tonkeeper 钱包。

  1. 项目目录结构如下
Ton-Counter
├─ .gitignore
├─ .env.example
├─ contracts
│  ├─ counter.fc
│  └─ imports
│     └─ stdlib.fc
├─ jest.config.ts
├─ package-lock.json
├─ package.json
├─ README.md
├─ scripts
│  ├─ deployCounter.ts
│  ├─ getCounter.ts
│  └─ sendIncrement.ts
├─ tests
│  └─ Counter.spec.ts
├─ tsconfig.json
└─ wrappers
   ├─ Counter.compile.ts
   └─ Counter.ts

counter.fc是关于 Counter 的 FunC 合约代码,主要是关于发送链上消息、读写链上数据。

wrappers 中的Counter.ts则是用于封装用于与 FunC 合约的交互接口。

scrpits 中的deployCounter.ts、getCounter.ts、sendIncrement.ts则是用于部署、读写 Counter 合约。

3.counter.fc合约源码分析

#include "imports/stdlib.fc";

(int) load_data() inline {                   ;; read function declaration - returns int as result
    var ds = get_data().begin_parse();       ;; load the storage cell and start parsing as a slice
    return (ds~load_uint(64));               ;; read a 64 bit unsigned int from the slice and return it
}

() save_data(int counter) impure inline {    ;; write function declaration - takes an int as arg
    set_data(begin_cell()                    ;; store the storage cell and create it with a builder
    .store_uint(counter, 64)                 ;; write a 64 bit unsigned int to the builder
    .end_cell());                            ;; convert the builder to a cell
}

() recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure {  ;; well known function signature
    if (in_msg_body.slice_empty?()) {         ;; check if incoming message is empty (with no body)
        return ();                            ;; return successfully and accept an empty message
    }
    int op = in_msg_body~load_uint(32);       ;; parse the operation type encoded in the beginning of msg body
    var (counter) = load_data();              ;; call our read utility function to load values from storage
    if (op == 1) {                            ;; handle op #1 = increment
        save_data(counter + 1);               ;; call our write utility function to persist values to storage
    }
}

int counter() method_id {          ;; getter declaration - returns int as result
    var (counter) = load_data();   ;; call our read utility function to load value
    return counter;
}
  • load_data()是一个内联函数,用于从存储单元中加载数据并返回一个整数结果。具体步骤如下:

    • 调用get_data()函数获取存储单元,并将其作为切片开始解析。

    • 从切片中读取一个 64 位无符号整数(uint64)。

    • 将读取到的整数作为结果返回。

  • save_data(int counter)是一个内联函数,用于将数据写入存储单元。具体步骤如下:

    • 创建一个存储单元的构建器,并将其作为初始状态。

    • 将整数counter以 64 位无符号整数的形式写入构建器。

    • 将构建器转换为一个存储单元,并保存在存储单元中。

  • recv_internal(int msg_value, cell in_msg, slice in_msg_body)是一个接收函数,用于处理接收到的内部消息。具体步骤如下:

    • 检查接收到的消息体是否为空,如果为空则返回。

    • 解析消息体开头编码的操作类型,使用 load_uint(32) 从切片中读取一个 32 位无符号整数。

    • 调用load_data()函数从存储中加载计数器的值。

    • 如果操作类型为 1(代表增量操作),则将计数器的值加 1,并调用save_data()函数将新值保存到存储中。

  • counter() method_id函数用于获取计数器的值。具体步骤如下:

    • 调用load_data()函数从存储中加载计数器的值,并将其作为结果返回。

4.Counter.ts分析

export default class Counter implements Contract {

  static createForDeploy(code: Cell, initialCounterValue: number): Counter {
    const data = beginCell()
      .storeUint(initialCounterValue, 64)
      .endCell();
    const workchain = 0; // deploy to workchain 0
    const address = contractAddress(workchain, { code, data });
    return new Counter(address, { code, data });
  }

  constructor(readonly address: Address, readonly init?: { code: Cell, data: Cell }) {}

  async sendDeploy(provider: ContractProvider, via: Sender) {
    await provider.internal(via, {
      value: "0.01", // send 0.01 TON to contract for rent
      bounce: false
    });
  }

  async sendIncrement(provider: ContractProvider, via: Sender) {
    const messageBody = beginCell()
      .storeUint(1, 32) // op (op #1 = increment)
      .storeUint(0, 64) // query id
      .endCell();
    await provider.internal(via, {
      value: "0.002", // send 0.002 TON for gas
      body: messageBody
    });
  }

  async getCounter(provider: ContractProvider) {
    const { stack } = await provider.get("counter", []);
    return stack.readBigNumber();
  }
}
  • createForDeploy方法用于创建一个 Counter 合约的实例,用于部署合约。

  • sendDeploy方法用于部署合约。它接受一个合约提供者provider和一个发送者via作为参数。在部署过程中,它会向合约发送 0.01 TON 的价值用于支付租金,同时不允许合约反弹(bounce)。

  • sendIncrement方法用于发送增量操作。它接受一个合约提供者provider和一个发送者via作为参数。在调用过程中,它会创建一个消息体(message body),其中包含增量操作的指令和查询 ID,并向合约发送 0.002 TON 的价值用于支付 gas 费用。

  • getCounter方法用于获取计数器值。它接受一个合约提供者provider作为参数,并从合约的状态中获取计数器的值,并返回该值。

5.deployCounter.ts分析

export async function run() {
    // initialize ton rpc client on testnet
    const endpoint = await getHttpEndpoint({ network: "testnet" });
    const client = new TonClient({ endpoint });

    // prepare Counter's initial code and data cells for deployment
    const counterCode = Cell.fromBoc(fs.readFileSync("build/counter.cell"))[0]; // compilation output from step 6
    const initialCounterValue = Date.now(); // to avoid collisions use current number of milliseconds since epoch as initial value
    const counter = Counter.createForDeploy(counterCode, initialCounterValue);

    // exit if contract is already deployed
    console.log("contract address:", counter.address.toString());
    if (await client.isContractDeployed(counter.address)) {
        return console.log("Counter already deployed");
    }

    // open wallet v4 (notice the correct wallet version here)
    const mnemonic = process.env.MNEMONIC!; // your 24 secret words (replace ... with the rest of the words)
    const key = await mnemonicToWalletKey(mnemonic.split(" "));
    const wallet = WalletContractV4.create({ publicKey: key.publicKey, workchain: 0 });
    if (!await client.isContractDeployed(wallet.address)) {
        return console.log("wallet is not deployed");
    }

    // open wallet and read the current seqno of the wallet
    const walletContract = client.open(wallet);
    const walletSender = walletContract.sender(key.secretKey);
    const seqno = await walletContract.getSeqno();

    // send the deploy transaction
    const counterContract = client.open(counter);
    await counterContract.sendDeploy(walletSender);

    // wait until confirmed
    let currentSeqno = seqno;
    while (currentSeqno == seqno) {
        console.log("waiting for deploy transaction to confirm...");
        await sleep(1500);
        currentSeqno = await walletContract.getSeqno();
    }
    console.log("deploy transaction confirmed!");
}

function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
  • run()函数的主要功能是部署 Counter 合约和相关的钱包合约。具体步骤如下:

    • ‍初始化 TonClient,并连接到测试网(testnet)的 Ton RPC 端点。

    • 准备 Counter 合约的代码和数据单元(cells)用于部署。

    • 创建 Counter 合约的实例。

    • 检查合约是否已经部署,如果已经部署,则退出脚本。

    • 打开钱包合约(WalletContractV4)。

    • 检查钱包合约是否已经部署,如果未部署,则退出脚本。

    • 打开钱包合约并获取当前序列号(seqno)。

    • 发送部署交易并等待确认。

    • 当部署交易确认后,输出相应的信息。

6.getCounter.ts分析

export async function run() {
    // initialize ton rpc client on testnet
    const endpoint = await getHttpEndpoint({ network: "testnet" });
    const client = new TonClient({ endpoint });

    // open Counter instance by address
    const counterAddress = Address.parse("EQDYOmv_rQXifWNqkrHzPERRJEBwHyBOplPgXJDGbu7p3mp8"); // replace with your address from step 8
    const counter = new Counter(counterAddress);
    const counterContract = client.open(counter);

    // call the getter on chain
    const counterValue = await counterContract.getCounter();
    console.log("value:", counterValue.toString());
}
  • run()函数的主要功能是通过合约地址打开 Counter 合约实例,并调用合约上的getCounter()方法获取计数器的值。具体步骤如下:

    • 初始化 TonClient,并连接到测试网(testnet)的 Ton RPC 端点。

    • 解析 Counter 合约的地址,创建 Counter 合约的实例。

    • 打开 Counter 合约实例。

    • 调用合约的getCounter()方法获取计数器的值。

    • 输出计数器的值。

7.sendIncrement.ts分析

export async function run() {
    // initialize ton rpc client on testnet
    const endpoint = await getHttpEndpoint({ network: "testnet" });
    const client = new TonClient({ endpoint });

    // open wallet v4 (notice the correct wallet version here)
    const mnemonic = process.env.MNEMONIC!; // your 24 secret words (replace ... with the rest of the words)
    const key = await mnemonicToWalletKey(mnemonic.split(" "));
    const wallet = WalletContractV4.create({ publicKey: key.publicKey, workchain: 0 });
    if (!await client.isContractDeployed(wallet.address)) {
        return console.log("wallet is not deployed");
    }

    // open wallet and read the current seqno of the wallet
    const walletContract = client.open(wallet);
    const walletSender = walletContract.sender(key.secretKey);
    const seqno = await walletContract.getSeqno();

    // open Counter instance by address
    const counterAddress = Address.parse("EQDYOmv_rQXifWNqkrHzPERRJEBwHyBOplPgXJDGbu7p3mp8"); // replace with your address from step 8
    const counter = new Counter(counterAddress);
    const counterContract = client.open(counter);

    // send the increment transaction
    await counterContract.sendIncrement(walletSender);

    // wait until confirmed
    let currentSeqno = seqno;
    while (currentSeqno == seqno) {
        console.log("waiting for transaction to confirm...");
        await sleep(1500);
        currentSeqno = await walletContract.getSeqno();
    }
    console.log("transaction confirmed!");
}

function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
}
  • run()函数的主要功能是执行以下操作:

    • ‍‍初始化 TonClient,并连接到测试网(testnet)的 Ton RPC 端点。

    • 从环境变量中获取钱包的助记词(mnemonic)。

    • 将助记词转换为钱包密钥。

    • 创建钱包合约实例(WalletContractV4)。

    • 检查钱包合约是否已经部署,如果未部署,则退出脚本。

    • 打开钱包合约并获取当前序列号(seqno)。

    • 解析 Counter 合约的地址,创建 Counter 合约的实例。

    • 打开 Counter 合约实例。

    • 向 Counter 合约发送增量交易。

    • 等待交易确认。

    • 输出交易确认的信息。


    

4.7 Example 2: Telegram Bot 与 TON 合约集成

4.7.1 代码讲解

讲解完简单的合约例子源码,下一步我们讲解 DApp 非常核心的部分, Telegram Bot 与 TON 合约是如何集成的。

  1. 这里用 4.6 节的 Counter 合约作为集成的目标,实现在 Bot 里获取 counter 值,自增 counter 值的效果。

  2. 首先新增一个 counter.ts 文件,内容可以直接拷贝 4.6节中的 Counter 合约操作的封装(class Counter)。其中有 Counter类对合约的调用的封装,其中 getCounter 函数去获取当前 counter 的值, sendIncrement 是自增 counter 的值。

  3. 新增 Counter 值的读取函数。

在 commands-handlers.ts 文件中调用 getCounter 函数, 新增 handleGetCounterCommand 获取 Counter 值

import { TonClient } from '@ton/ton';

export async function handleGetCounterCommand(msg: TelegramBot.Message): Promise<void> {
    const chatId = msg.chat.id;

    const client = new TonClient({
        endpoint: `https://testnet.toncenter.com/api/v2/jsonRPC`,
        apiKey: process.env.TONCENTER_API_KEY,
    });

    const contract = new Counter(
        Address.parse('EQBYLTm4nsvoqJRvs_L-IGNKwWs5RKe19HBK_lFadf19FUfb') // replace with your address from tutorial 2 step 8
      );
    const counterContract =  client.open(contract) as unknown  as OpenedContract<Counter>;
    const val = await counterContract.getCounter();
    console.log(val);

    await bot.sendMessage(chatId, 'counter value : ' + val);
}

handleGetCounterCommand 函数主要功能是执行以下操作:

a. 从 “@ton/ton” 包中引入 TonClient 类,TonClient 可以理解为连接 TON 相关接口的封装类库
b. 使用 TON RPC 地址和 TONCENTER_API_KEY (从 “@tonapibot” 的 Telegram Bot 中获取), 使用 Counter 合约地址初始化 Counter
c. 调用 “counterContract.getCounter()” 获取 Counter 合约中 counter 的值

  1. 新增 Counter 值的自增函数

在 commands-handlers.ts 文件中新增 handleIncrementCounterCommand 函数,执行自增 counter 值

export async function handleIncrementCounterCommand(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 client = new TonClient({
        endpoint: `https://testnet.toncenter.com/api/v2/jsonRPC`,
        apiKey: process.env.TONCENTER_API_KEY,
    });

    const contract = new Counter(
        Address.parse('EQBYLTm4nsvoqJRvs_L-IGNKwWs5RKe19HBK_lFadf19FUfb') // replace with your address from tutorial 2 step 8
      );
    const counterContract =  client.open(contract) as unknown  as OpenedContract<Counter>;

    const sender = {
        send: async (args: SenderArguments) => {
            connector.sendTransaction({
                network: CHAIN.TESTNET,
                validUntil: Math.round(
                    (Date.now() + Number(process.env.DELETE_SEND_TX_MESSAGE_TIMEOUT_MS)) / 1000
                ),
                messages: [
                    {
                        address: args.to.toString(),
                        amount: args.value.toString(),
                        payload: args.body?.toBoc().toString('base64'),
                    }
                ]
            });
        }
    }

    counterContract?.sendIncrement(sender);
    //Todo: seqno verfiy

    await bot.sendMessage(chatId, "increment counter success");
}

handleIncrementCounterCommand 函数主要功能是执行以下操作:

a. 从 “@ton/ton” 包中引入 TonClient 类,TonClient 可以理解为连接 TON 相关接口的封装类库
b. 使用 TON RPC 地址和 TONCENTER_API_KEY (从 “@tonapibot” 的 Telegram Bot 中获取), 使用 Counter 合约地址初始化 Counter
c.client.open 建立 TonClient 和 链上合约的联系
d. 定义 sender 函数,用于 TonConnect 发送交易的封装
e. 调用counterContract的 sendIncrement 函数,  这里可以优化的点是对 seqno 的判断,可以参考4.6节 sendIncrement 中对 seqno 处理。

  1. 在 main.ts 文件, main 函数中添加获取 counter, 自增 counter 的入口。
bot.onText(/\/get_counter/, handleGetCounterCommand);
bot.onText(/\/increment_counter/, handleIncrementCounterCommand);

修改完代码完成后,运行 bot 服务 。

4.7.2 运行效果

“/get_counter” 如下图

"/increment_counter" 如下图

05 总结

以上就是 Telegram & TON 生态的一些概念科普、关系梳理、example 代码讲解&效果展示。对于初学开发者来说,弄清楚官方提供的技术栈与各种功能特性如何相结合,是非常重要的,特别是与 Bot 部分,当然涉及到产品和开发相关细节是远不止这些的。

另外 TON 采用非 EVM 的标准,创造了一套完全不同于市面上任何一种(Solidity、Yul、Rust、Move、C++ 等)智能合约的技术栈,所以开发者在 Telegram & TON 生态开发 DApp,是有一定门槛的。当前除了官方文档,现存的技术参考资料相对较少,另外相对容易上手的 Tact 编程语言官方库也正处于建设阶段,离成熟还有不小距离,开发者们要有足够的心理预期。

最后希望此篇文章能给读者们带来一些帮助!

撰文 |  Lewis、Howe、Kahn
编辑&排版 | Cikey
设计 | WangTeng

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
LXDAO_Official
LXDAO_Official
0xad84...8db8
LXDAO is an R&D-focused DAO in Web3