使用钱包智能合约的工作
👋 介绍
在开始智能合约开发之前,学习 TON 上的钱包和交易如何工作是必不可少的。这些知识将帮助开发者了解钱包、交易和智能合约之间的交互,以实现特定的开发任务。
建议在阅读本教程之前先熟悉一下 钱包合约类型 一文。
在本节中,我们将学习如何创建操作,而不使用预配置的函数,以了解开发工作流程。本教程的所有必要参考资料都位于参考章节。
💡 必要条件
本教程要求掌握 JavaScript 和 TypeScript 或 Golang 的基本知识。此外,还需要持有至少 3 TON(可以存储在交易所账户、非托管钱包或使用 Telegram 机器人钱包)。要理解本教程,需要对 cell、address in TON、blockchain of blockchains有基本了解。
在 TON 测试网上工作往往会导致部署错误、难以跟踪交易以及不稳定的网络功能。因此,完成大部分开发工作时间可能好处是建议在 TON Mainnet 上完成,以避免这些问题,这可能需要减少交易数量,从而可能减小费用。
源代码
本教程中使用的所有代码示例都可以在以下 GitHub 存储库 中找到。
✍️ 您开始所需的内容
- 确保 NodeJS 已安装。
- 需要特定的 Ton 库,包括:@ton/ton 13.5.1+、@ton/core 0.49.2+ 和 @ton/crypto 3.2.0+。
** 可选**:如果您喜欢使用 Go 而不是 JS,则必须安装 tonutils-go 库和 GoLand IDE 才能在 TON 上进行开发。本教程将在 GO 版本中使用该库。
- JavaScript
- Golang
npm i --save @ton/ton @ton/core @ton/crypto
go get github.com/xssnick/tonutils-go
go get github.com/xssnick/tonutils-go/adnl
go get github.com/xssnick/tonutils-go/address
⚙ 设置您的环境
为了创建一个 TypeScript 项目,必须按照以下步骤进行操作:
- 创建一个空文件夹(我们将其命名为 WalletsTutorial)。
- 使用 CLI 打开项目文件夹。
- 使用以下命令来设置项目:
npm init -y
npm install typescript @types/node ts-node nodemon --save-dev
npx tsc --init --rootDir src --outDir build \ --esModuleInterop --target es2020 --resolveJsonModule --lib es6 \ --module commonjs --allowJs true --noImplicitAny false --allowSyntheticDefaultImports true --strict false
为了帮助我们完成下一个流程,我们使用了 ts-node
来直接执行 TypeScript 代码,而无需预编译。当检测到目录中的文件更改时,nodemon
会自动重新启动节点应用程序。
"files": [
"\\",
"\\"
]
- 然后,在项目根目录中创建
nodemon.json
配置文件,内容如下:
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": "npx ts-node ./src/index.ts"
}
- 在
package.json
中添加以下脚本到 "test" 脚本的位置:
"start:dev": "npx nodemon"
- 在项目根目录中创建
src
文件夹,然后在该文件夹中创建index.ts
文件。 - 接下来,添加以下代码:
async function main() {
console.log("Hello, TON!");
}
main().finally(() => console.log("Exiting..."));
- 使用 终端运行以下代码:
npm run start:dev
- 最后,控制台将输出以下内容。
TON 社区创建了一个优秀的工具来自动化所有开发过程(部署、合约编写、测试)称为 Blueprint。然而,我们在本教程中不需要这么强大的工具,所以建议遵循上述说明。
可选: 当使用 Golang 时,请按照以下说明进行操作:
- 安装 GoLand IDE。
- 使用以下内容创建项目文件夹和
go.mod
文件(如果使用的当前版本已过时,则可能需要更改 Go 版本):
module main
go 1.20
- 在终端中输入以下命令:
go get github.com/xssnick/tonutils-go
- 在项目根目录中创建
main.go
文件,内容如下:
package main
import (
"log"
)
func main() {
log.Println("Hello, TON!")
}
- 将
go.mod
中的模块名称更改为main
。 - 运行上述代码,直到在终端中显示输出。
也可以使用其他 IDE,因为 GoLand 不是免费的,但建议使用 GoLand。
另外,下面的每个新部分将指定每个新部分所需的特定代码部分,并且需要将新的导入与旧导入合并起来。\
🚀 让我们开始!
我们的主要任务是使用 @ton/ton、@ton/core、@ton/crypto 的各种对象和函数构建交易,以了解大规模交易是怎样的。为了完成这个过程,我们将使用两个主要的钱包版本(v3 和 v4),因为交易所、非托管钱包和大多数用户仅使用这些特定版本。
我们的主要任务是使用 @ton/ton、@ton/core、@ton/crypto(ExternalMessage、InternalMessage、Signing 等)的各种对象和函数构建消息,以了解消息在更大范围内的样子。为了完成这一过程,我们将使用两个主要的钱包版本(v3 和 v4),因为事实上交易所、非托管钱包和大多数用户都只使用这些特定版本。
There may be occasions in this tutorial when there is no explanation for particular details. In these cases, more details will be provided in later stages of this tutorial.
重要: 在本教程中,我们使用了 wallet v3 代码 来更好地理解钱包开发过程。需要注意的是,v3 版本有两个子版本:r1 和 r2。目前,只使用第二个版本,这意味着当我们在本文档中提到 v3 时,它指的是 v3r2 。
💎 TON 区块链钱包
在 TON 区块链上运行的所有钱包实际上都是智能合约,与 TON 上的一切都是智能合约的方式相同。与大多数区块链一样,可以在网络上部署智能合约并根据不同的用途自定义它们。由于这个特性,完全自定义的钱包是可能的。 在 TON 上,钱包智能合约帮助平台与其他智能合约类型进行通信。然而,重要的是要考虑钱包通信是如何进行的。
钱包通信
一般来说,TON区块链上有两种消息类型: internal
和 external
。外部消息允许从外部世界向区块链发送消息,从而允许与接受此类消息的智能合约进行通信。负责执行这一过程的功能如下:
() recv_external(slice in_msg) impure {
;; some code
}
在深入了解有关钱包的更多细节之前,我们先来看看钱包是如何接受外部信息的。在 TON 上,所有钱包都持有所有者的 "公钥 (public key)"、"序列号 (seqno)"和 "子钱包 ID (subwallet_id)"。收到外部信息时,钱包会使用 get_data()
方法从钱包的存储部分检索数据。然后,它会执行几个验证程序,并决定是否接受信息。这个过程如下:
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512); ;; get signature from the message body
var cs = in_msg;
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32)); ;; get rest values from the message body
throw_if(35, valid_until <= now()); ;; check the relevance of the message
var ds = get_data().begin_parse(); ;; get data from storage and convert it into a slice to be able to read values
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256)); ;; read values from storage
ds.end_parse(); ;; make sure we do not have anything in ds variable
throw_unless(33, msg_seqno == stored_seqno);
throw_unless(34, subwallet_id == stored_subwallet);
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
accept_message();
💡 有用的链接:
接下来,我们来详细看一下。
重放保护 - Seqno
钱包智能合约中的消息重放保护与消息序列号(seqno,Sequence Number)直接相关,该序列号可追踪消息的发送顺序。钱包中的单条信息不能重复发送,这一点非常重要,因为这会完全破坏系统的完整性。如果我们进一步检查钱包内的智能合约代码,seqno
通常会按以下方式处理:
throw_unless(33, msg_seqno == stored_seqno);
上面这行代码检查消息中的 seqno
并与存储在智能合约中的 seqno
进行核对。如果两者不匹配,合约就会返回一个带有 33 exit code
的错误。因此,如果发送者传递了无效的 seqno,这意味着他在信息序列中犯了某些错误,而合约可以防止这种情况发生。
还需要确认外部消息可以由任何人发送。这意味着如果您向某人发送 1 TON,其他人也可以重复该消息。 但是,当 seqno 增加时,以前的外部消息失效,并且没有人可以重复该消息,从而防止窃取您的资金。
签名
要执行此过程,首先钱包需要从传入消息中获取签名,从存储中加载公钥,并使用以下过程验证签名:
要执行此过程,首先钱包需要从传入消息中获取签名,从存储中加载公钥,并使用以下过程验证签名:
var signature = in_msg~load_bits(512);
var ds = get_data().begin_parse();
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key));
如果所有验证流程都顺利完成,智能合约接受消息并对其进行处理:
accept_message();
由于消息来自外部世界,它不包含支付交易费用所需的 Toncoin。在使用 accept_message() 函数发送 TON 时,应用gas_credit(在写入时其值为10,000 gas单位),并且只要gas不超过 gas_credit 值,就允许免费进行必要的计算。使用 accept_message() 函数后,从智能合约的账户余额中收取所有已花费的gas(以 TON 计)。可以在此处了解有关此过程的更多信息。
交易过期
用于检查外部报文有效性的另一个步骤是 valid_until
字段。从变量名可以看出,在 UNIX 中,这是报文生效前的时间。如果验证过程失败,合约将完成事务处理,并返回如下的 35 退出码:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
throw_if(35, valid_until <= now());
当信息不再有效,但由于不明原因仍被发送到区块链上时,这种算法可以防止出现各种错误。
钱包 v3 和钱包 v4 的区别
钱包 v3 和钱包 v4 之间的唯一区别是钱包 v4 使用可以安装和删除的 插件
。插件是特殊的智能合约,可以从钱包智能合约请求在特定时间从指定数量的 TON 中。钱包智能合约将相应地发送所需数量的 TON,而无需所有者参与。这类似于为插件创建的 订阅模型。我们不会在本教程中详细介绍这些细节,因为这超出了本教程的范围。
正如我们之前讨论的那样,钱包智能合约接受外部交易,验证它们,如果通过了所有检查,则接受它们。然后,合约开始从外部消息的主体中检索消息,然后创建内部消息并将其发送到区块链,如下所示:
钱包如何促进与智能合约的通信
正如我们前面所讨论的,钱包智能合约会接受外部信息,对其进行验证,并在所有检查都通过的情况下接受它们。然后,合约开始从外部信息正文中检索信息的循环,然后创建内部信息并将其发送到区块链,如下所示:
cs~touch();
while (cs.slice_refs()) {
var mode = cs~load_uint(8); ;; load message mode
send_raw_message(cs~load_ref(), mode); ;; get each new internal message as a cell with the help of load_ref() and send it
}
在 TON 上,所有智能合约都在基于堆栈的 TON 虚拟机(TVM)上运行。~ touch() 将变量 cs
放在栈顶,以优化代码运行,减少 gas 。
由于一个 cell 中 最多可存储 4 个引用,因此每个外部信息最多可发送 4 个内部信息。
💡 Useful links:
📬 外部和内部信息
在本节中,我们将进一步了解 internal
和 external
消息,并创建消息和将其发送到网络,以尽量减少使用预制函数。
这样,Tonkeeper 钱包应用程序将部署钱包合约,我们可以在以下步骤中使用它。
- 安装 钱包应用程序 (例如,作者使用的是 Tonkeeper)
- 将钱包应用程序切换到 v3r2 地址版本
- 向钱包存入 1 TON
- 将信息发送到另一个地址(可以发送给自己,发送到同一个钱包)。
这样,Tonkeeper 钱包应用程序就会部署钱包合约,我们就可以在下面的步骤中使用它了。
在编写本教程时,TON 上的大多数钱包应用程序默认使用钱包 v4 版本。本教程不需要插件,我们将使用钱包 v3 提供的功能。在使用过程中,Tonkeeper 允许用户选择所需的钱包版本。因此,建议部署钱包版本 3(钱包 v3)。
TL-B
在本节中,我们将详细研究 block.tlb。在将来的开发中,此文件将非常有用,因为它描述了不同cell的组装方式。在我们的情况下,它详细描述了内部和外部交易的复杂性。
在本节中,我们将研究 block.tlb。该文件将在未来的开发过程中非常有用,因为它描述了不同 cell 应如何组装。具体到我们的例子,它详细说明了内部和外部信息的复杂性。
本指南将提供基本信息。如需了解更多详情,请参阅我们的 TL-B 文档,了解有关 TL-B 的更多信息。
CommonMsgInfo
您可以从 TL-B 中看到,仅在与 ext_in_msg_info 类型一起使用时才可以使用 CommonMsgInfo。因为交易类型字段,如 src
、created_lt
、created_at
等,由验证者在交易处理期间进行重写。在这种情况下,src
交易类型最重要,因为当发送交易时,发送者是未知的,验证者在验证期间对其在 src
字段中的地址进行重写。这样确保 src
字段中的地址是正确的,并且不能被操纵。
但是,CommonMsgInfo
结构仅支持 MsgAddress
规格,但通常情况下发送方的地址是未知的,并且需要写入 addr_none
(两个零位 00
)。在这种情况下,使用 CommonMsgInfoRelaxed
结构,该结构支持 addr_none
地址。对于 ext_in_msg_info
(用于传入的外部消息),使用 CommonMsgInfo
结构,因为这些消息类型不使用sender,始终使用 MsgAddressExt 结构(addr_none$00
表示两个零位),因此无需覆盖数据。
查看 TL-B,您会注意到当与 ext_in_msg_info 类型一起使用时,只有 CommonMsgInfo 可用。这是因为诸如 src
、created_lt
、created_at
等消息字段会在事务处理过程中被验证器重写。在这种情况下,消息中的 src
字段最为重要,因为在发送消息时,发件人是未知的,验证程序在验证时会写入该字段。这样可以确保 src
字段中的地址是正确的,不会被篡改。
但是,CommonMsgInfo
结构只支持 MsgAddress
规格,但发件人地址通常是未知的,因此需要写入 "addr_none"(两个零位 "00")。在这种情况下,使用支持 addr_none
地址的 CommonMsgInfoRelaxed
结构。对于 ext_in_msg_info
(用于传入的外部报文),则使用 CommonMsgInfo
结构,因为这些消息类型不使用 sender,始终使用MsgAddressExt 结构(addr_none$00
表示两个零位),这意味着无需覆盖数据。
$
符号后面的数字是在某个结构的开始处所要求存储的位,以便在读取时(反序列化)可进一步识别这些结构。