使用钱包智能合约的工作
👋 介绍
在开始智能合约开发之前,学习 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
表示两个零位),这意味着无需覆盖数据。
$
符号后面的数字是在某个结构的开始处所要求存储的位,以便在读取时(反序列化)可进一步识别这些结构。
创建内部信息
让我们首先考虑 0x18
和 0x10
(x - 16 进制),这些十六进制数是按以下方式排列的(考虑到我们分配了 6 个位):011000
和 010000
。这意味着,可以将上述代码重写为以下内容:
var msg = begin_cell()
.store_uint(0x18, 6) ;; or 0x10 for non-bounce
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
现在我们来详细解释每个选项:
var msg = begin_cell()
.store_uint(0, 1) ;; this bit indicates that we send an internal message according to int_msg_info$0
.store_uint(1, 1) ;; IHR Disabled
.store_uint(1, 1) ;; or .store_uint(0, 1) for 0x10 | bounce
.store_uint(0, 1) ;; bounced
.store_uint(0, 2) ;; src -> two zero bits for addr_none
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; default message headers (see sending messages page)
;; store something as a body
现在,让我们详细了解每个选项:
选项 | 说明 |
---|---|
IHR Disabled | 目前,由于即时超立方路由(Instant Hypercube Routing)尚未完全实现,因此该选项被禁用(即存储 1)。此外,当网络上有大量 Shardchains 时,也需要使用该选项。有关禁用 IHR 选项的更多信息,请参阅 tblkch.pdf(第 2 章)。 |
Bounce | 在发送信息时,智能合约处理过程中可能会出现各种错误。为避免损失 TON,有必要将 Bounce 选项设置为 1(true)。在这种情况下,如果在交易处理过程中出现任何合约错误,信息将被退回给发送方,同时会收到扣除费用后的相同数量的 TON。关于不可反弹报文的更多信息,请参阅 此处。 |
Bounced | 退回信息是指由于智能合约处理交易时发生错误而退回给发件人的信息。该选项会告诉你收到的信息是否被退回。 |
Src | Src 是发件人地址。在这种情况下,会写入两个 0 位来表示 addr_none 地址。 |
最后,我们来看剩下的代码行:
...
.store_slice(to_address)
.store_coins(amount)
...
- 我们指定收件人和要发送的 TON 数。
上述值(包括 Src)具有以下特征,但不包括 State Init 和 Message Body 位,由验证者重写。
...
.store_uint(0, 1) ;; Extra currency
.store_uint(0, 4) ;; IHR fee
.store_uint(0, 4) ;; Forwarding fee
.store_uint(0, 64) ;; Logical time of creation
.store_uint(0, 32) ;; UNIX time of creation
.store_uint(0, 1) ;; State Init
.store_uint(0, 1) ;; Message body
;; store something as a body
选项 | 说明 |
---|---|
Extra currency | 这是现有 jetton 的本机实现,目前尚未使用。 |
IHR fee | 如前所述,目前 IHR 尚未启用,因此该费用始终为零。您可以在 tblkch.pdf(第 3.1.8 节)中了解更多相关信息。 |
Forwarding fee | 转发信息费用。更多信息请参阅费用文档。 |
Logical time of creation | 用于创建正确信息队列的时间。 |
UNIX time of creation | 信息在 UNIX 中创建的时间。 |
State Init | 部署智能合约的代码和源代码。如果该位被设置为 0 ,则表示我们没有 State Init。但如果该位被设置为 1 ,则需要写入另一位,该位表示 State Init 是存储在同一 cell 中(0)还是作为引用写入(1)。 |
Message body | 这部分定义了如何存储报文正文。有时,信息正文太大,无法放入信息本身。在这种情况下,应将其存储为 引用,将该位设置为 1 ,表示正文被用作引用。如果该位为 0 ,则正文与信息存放在同一 cell 中。 |
接下来,我们将开始准备一个交易,该交易将向另一个钱包 v3 发送 Toncoins。首先,假设用户想要向自己发送 0.5 TON,并附带文本“你好,TON!”,请参阅本文档的这一部分来了解如何发送带有评论的消息。
如果数字值适合的比特数少于指定的比特数,那么缺失的零将被添加到数值的左边。例如,0x18 适合 5 位 -> 11000
。但是,由于指定的是 6 位,最终结果就变成了 011000
。
接下来,我们开始准备一条消息,将 Toncoin 发送到另一个钱包 v3。 首先,假设用户想给自己发送 0.5 TON,并附上文字 "Hello, TON!",请参考我们文档中的这部分内容(如何发送带注释的消息)。
- JavaScript
- Golang
import { beginCell } from '@ton/core';
let internalMessageBody = beginCell()
.storeUint(0, 32) // write 32 zero bits to indicate that a text comment will follow
.storeStringTail("Hello, TON!") // write our text comment
.endCell();
import (
"github.com/xssnick/tonutils-go/tvm/cell"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32). // write 32 zero bits to indicate that a text comment will follow
MustStoreStringSnake("Hello, TON!"). // write our text comment
EndCell()
上面我们创建了一个 InternalMessageBody
(内部消息体),消息的正文就存储在其中。请注意,当存储的文本不适合单个 cell (1023 位)时,有必要根据 以下文档 将数据分割成多个 cell。不过,在这种情况下,高级库会根据要求创建 cell ,因此现阶段无需担心这个问题。
接下来,我们将根据之前研究的信息创建 内部消息
(InternalMessage),具体如下:
- JavaScript
- Golang
import { toNano, Address } from '@ton/ton';
const walletAddress = Address.parse('put your wallet address');
let internalMessage = beginCell()
.storeUint(0, 1) // indicate that it is an internal message -> int_msg_info$0
.storeBit(1) // IHR Disabled
.storeBit(1) // bounce
.storeBit(0) // bounced
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress)
.storeCoins(toNano("0.2")) // amount
.storeBit(0) // Extra currency
.storeCoins(0) // IHR Fee
.storeCoins(0) // Forwarding Fee
.storeUint(0, 64) // Logical time of creation
.storeUint(0, 32) // UNIX time of creation
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(internalMessageBody) // Store Message Body as a reference
.endCell();
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
)
walletAddress := address.MustParseAddr("put your address")
internalMessage := cell.BeginCell().
MustStoreUInt(0, 1). // indicate that it is an internal message -> int_msg_info$0
MustStoreBoolBit(true). // IHR Disabled
MustStoreBoolBit(true). // bounce
MustStoreBoolBit(false). // bounced
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress).
MustStoreCoins(tlb.MustFromTON("0.2").NanoTON().Uint64()). // amount
MustStoreBoolBit(false). // Extra currency
MustStoreCoins(0). // IHR Fee
MustStoreCoins(0). // Forwarding Fee
MustStoreUInt(0, 64). // Logical time of creation
MustStoreUInt(0, 32). // UNIX time of creation
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(internalMessageBody). // Store Message Body as a reference
EndCell()
创建信息
有必要检索我们钱包智能合约的 seqno
(序列号)。为此,需要创建一个 Client
,用来发送请求,运行钱包的获取方法 seqno
。此外,还需要添加一个种子短语(在创建钱包 此处 时保存),以便通过以下步骤签署我们的信息:
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
import { mnemonicToWalletKey } from '@ton/crypto';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC", // you can replace it on https://testnet.toncenter.com/api/v2/jsonRPC for testnet
apiKey: "put your api key" // you can get an api key from @tonapibot bot in Telegram
});
const mnemonic = 'put your mnemonic'; // word1 word2 word3
let getMethodResult = await client.runMethod(walletAddress, "seqno"); // run "seqno" GET method from your wallet contract
let seqno = getMethodResult.stack.readNumber(); // get seqno from response
const mnemonicArray = mnemonic.split(' '); // get array from string
const keyPair = await mnemonicToWalletKey(mnemonicArray); // get Secret and Public keys from mnemonic
import (
"context"
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/ton"
"golang.org/x/crypto/pbkdf2"
"log"
"strings"
)
mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
connection := liteclient.NewConnectionPool()
configUrl := "https://ton-blockchain.github.io/global.config.json"
err := connection.AddConnectionsFromConfigUrl(context.Background(), configUrl)
if err != nil {
panic(err)
}
client := ton.NewAPIClient(connection) // create client
block, err := client.CurrentMasterchainInfo(context.Background()) // get current block, we will need it in requests to LiteServer
if err != nil {
log.Fatalln("CurrentMasterchainInfo err:", err.Error())
return
}
getMethodResult, err := client.RunGetMethod(context.Background(), block, walletAddress, "seqno") // run "seqno" GET method from your wallet contract
if err != nil {
log.Fatalln("RunGetMethod err:", err.Error())
return
}
seqno := getMethodResult.MustInt(0) // get seqno from response
// The next three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. With the tonutils-go library, this is all implemented, but we’re doing it again to get a full understanding.
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
privateKey := ed25519.NewKeyFromSeed(k)
因此,需要发送 seqno
、keys
和 internal message
。现在,我们需要为钱包创建一个 消息,并按照教程开头使用的序列将数据存储在该消息中。具体步骤如下
- JavaScript
- Golang
import { sign } from '@ton/crypto';
let toSign = beginCell()
.storeUint(698983191, 32) // subwallet_id | We consider this further
.storeUint(Math.floor(Date.now() / 1e3) + 60, 32) // Message expiration time, +60 = 1 minute
.storeUint(seqno, 32) // store seqno
.storeUint(3, 8) // store mode of our internal message
.storeRef(internalMessage); // store our internalMessage as a reference
let signature = sign(toSign.endCell().hash(), keyPair.secretKey); // get the hash of our message to wallet smart contract and sign it to get signature
let body = beginCell()
.storeBuffer(signature) // store signature
.storeBuilder(toSign) // store our message
.endCell();
import (
"time"
)
toSign := cell.BeginCell().
MustStoreUInt(698983191, 32). // subwallet_id | We consider this further
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32). // Message expiration time, +60 = 1 minute
MustStoreUInt(seqno.Uint64(), 32). // store seqno
MustStoreUInt(uint64(3), 8). // store mode of our internal message
MustStoreRef(internalMessage) // store our internalMessage as a reference
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash()) // get the hash of our message to wallet smart contract and sign it to get signature
body := cell.BeginCell().
MustStoreSlice(signature, 512). // store signature
MustStoreBuilder(toSign). // store our message
EndCell()
要从外部世界将任何内部消息传递到区块链中,需要将其包含在外部交易中发送。正如我们之前讨论的那样,仅需要使用 ext_in_msg_info$10
结构,因为目标是将外部消息发送到我们的合约中。现在,我们创建一个外部消息,将发送到我们的钱包:
除了基本的验证过程外,我们还了解到 Wallet V3、Wallet V4 智能合约 提取操作码以确定是简单翻译还是与插件相关的消息 是必需的。为了与该版本相匹配,有必要在写入 seqno(序列号)之后和指定交易模式之前添加 storeUint(0, 8).
(JS/TS), MustStoreUInt(0, 8).
(Golang)函数。
创建外部信息
要从外部世界向区块链传递任何内部消息,都必须在外部消息中发送。正如我们之前所考虑的,只需使用 ext_in_msg_info$10
结构即可,因为我们的目标是向我们的合约发送外部消息。现在,让我们创建一条将发送到钱包的外部消息:
- JavaScript
- Golang
let externalMessage = beginCell()
.storeUint(0b10, 2) // 0b10 -> 10 in binary
.storeUint(0, 2) // src -> addr_none
.storeAddress(walletAddress) // Destination address
.storeCoins(0) // Import Fee
.storeBit(0) // No State Init
.storeBit(1) // We store Message Body as a reference
.storeRef(body) // Store Message Body as a reference
.endCell();
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // 0b10 -> 10 in binary
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress). // Destination address
MustStoreCoins(0). // Import Fee
MustStoreBoolBit(false). // No State Init
MustStoreBoolBit(true). // We store Message Body as a reference
MustStoreRef(body). // Store Message Body as a reference
EndCell()
选项 | 说明 |
---|---|
Src | 发件人地址。由于收到的外部报文不可能有发件人,因此总是有 2 个零位(addr_none TL-B)。 |
Import Fee | 用于支付导入外部信息的费用。 |
State Init | 与内部信息不同,外部信息中的 State Init 是 从外部世界 部署合约所必需的。状态初始与内部报文结合使用,可以让一个合约部署另一个合约。 |
Message Body | 必须发送给合约进行处理的信息。 |
0b10(b - 二进制)表示二进制记录。在此过程中,会存储两个比特:1
和 0
。因此,我们指定为 ext_in_msg_info$10
。
现在,我们有了一条已完成的消息,可以发送给我们的合约了。要做到这一点,首先应将其序列化为BOC
(Bag of Cells),然后使用以下代码发送:
- JavaScript
- Golang
console.log(externalMessage.toBoc().toString("base64"))
client.sendFile(externalMessage.toBoc());
import (
"encoding/base64"
"github.com/xssnick/tonutils-go/tl"
)
log.Println(base64.StdEncoding.EncodeToString(externalMessage.ToBOCWithFlags(false)))
var resp tl.Serializable
err = client.Client().QueryLiteserver(context.Background(), ton.SendMessage{Body: externalMessage.ToBOCWithFlags(false)}, &resp)
if err != nil {
log.Fatalln(err.Error())
return
}
💡 Useful link:
在本节中,我们将介绍如何从头开始创建钱包(钱包v3)。您将学习如何为钱包智能合约编译代码,生成助记词短语,获得钱包地址,并使用外部交易和State Init部署钱包。
生成助记词
正确定义钱包所需的第一件事是检索private
和public
密钥。为了完成这个任务,需要生成助记词种子短语,然后使用加密库提取私钥和公钥。
通过以下方式实现:
生成助记符
要正确创建钱包,首先需要获取 "私钥 "和 "公钥"。要完成这项任务,需要生成一个助记种子短语,然后使用加密库提取私钥和公钥。
具体做法如下
- JavaScript
- Golang
import { mnemonicToWalletKey, mnemonicNew } from '@ton/crypto';
// const mnemonicArray = 'put your mnemonic'.split(' ') // get our mnemonic as array
const mnemonicArray = await mnemonicNew(24); // 24 is the number of words in a seed phrase
const keyPair = await mnemonicToWalletKey(mnemonicArray); // extract private and public keys from mnemonic
console.log(mnemonicArray) // if we want, we can print our mnemonic
import (
"crypto/ed25519"
"crypto/hmac"
"crypto/sha512"
"log"
"github.com/xssnick/tonutils-go/ton/wallet"
"golang.org/x/crypto/pbkdf2"
"strings"
)
// mnemonic := strings.Split("put your mnemonic", " ") // get our mnemonic as array
mnemonic := wallet.NewSeed() // get new mnemonic
// The following three lines will extract the private key using the mnemonic phrase. We will not go into cryptographic details. It has all been implemented in the tonutils-go library, but it immediately returns the finished object of the wallet with the address and ready methods. So we’ll have to write the lines to get the key separately. Goland IDE will automatically import all required libraries (crypto, pbkdf2 and others).
mac := hmac.New(sha512.New, []byte(strings.Join(mnemonic, " ")))
hash := mac.Sum(nil)
k := pbkdf2.Key(hash, []byte("TON default seed"), 100000, 32, sha512.New) // In TON libraries "TON default seed" is used as salt when getting keys
// 32 is a key len
privateKey := ed25519.NewKeyFromSeed(k) // get private key
publicKey := privateKey.Public().(ed25519.PublicKey) // get public key from private key
log.Println(publicKey) // print publicKey so that at this stage the compiler does not complain that we do not use our variable
log.Println(mnemonic) // if we want, we can print our mnemonic
钱包作为智能合约的最显着优势之一是能够仅使用一个私钥创建大量的钱包。这是因为TON区块链上的智能合约地址是使用多个因素计算出来的,其中包括stateInit
。stateInit包含了代码
和初始数据
,这些数据存储在区块链的智能合约存储中。
有必要将生成的助记符种子短语输出到控制台,然后保存并使用(如上一节所述),以便每次运行钱包代码时使用相同的配对密钥。
子钱包 ID
根据TON区块链的源代码中的代码行,默认的subwallet_id
值为698983191
:
可以从配置文件中获取创世块信息(zero_state)。了解其复杂性和细节并非必要,但重要的是要记住subwallet_id
的默认值为698983191
。
每个钱包合约都会检查外部交易的subwallet_id字段,以避免将请求发送到具有不同ID的钱包的情况:
res.wallet_id = td::as<td::uint32>(res.config.zero_state_id.root_hash.as_slice().data());
我们需要将以上的值添加到合约的初始数据中,所以变量需要保存如下:
每个钱包合约都会检查外部信息的 subwallet_id 字段,以避免请求被发送到另一个 ID 的钱包:
var (subwallet_id, valid_until, msg_seqno) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32));
var (stored_seqno, stored_subwallet, public_key) = (ds~load_uint(32), ds~load_uint(32), ds~load_uint(256));
throw_unless(34, subwallet_id == stored_subwallet);
我们需要将上述值添加到合约的初始数据中,因此需要按如下方式保存变量:
- JavaScript
- Golang
const subWallet = 698983191;
var subWallet uint64 = 698983191
编译钱包代码
我们将仅使用JavaScript来编译代码,因为用于编译代码的库基于JavaScript。 但是,一旦编译完成,只要我们拥有编译后的cell的base64输出,就可以在其他编程语言(如Go等)中使用这些编译后的代码。
首先,我们需要创建两个文件:wallet_v3.fc
和stdlib.fc
。编译器和stdlib.fc库一起使用。库中创建了所有必需的基本函数,这些函数对应于asm
指令。可以从这里下载stdlib.fc文件。在wallet_v3.fc
文件中,需要复制上面的代码。
npm i --save @ton-community/func-js
现在,我们为我们正在创建的项目有了以下结构:
首先,我们需要创建两个文件:wallet_v3.fc
和 stdlib.fc
。编译器使用 stdlib.fc 库。库中创建了与 asm
指令相对应的所有必要的基本函数。可下载 stdlib.fc 文件 此处。在 wallet_v3.fc
文件中,需要复制上述代码。
请记住,在wallet_v3.fc
文件的开头添加以下行,以指示将在下面使用stdlib中的函数:
.
├── src/
│ ├── main.ts
│ ├── wallet_v3.fc
│ └── stdlib.fc
├── nodemon.json
├── package-lock.json
├── package.json
└── tsconfig.json
如果您的 IDE 插件与 stdlib.fc
文件中的 () set_seed(int) impure asm "SETRAND";
冲突,也没关系。
现在,让我们编写代码来编译我们的智能合约并使用npm run start:dev
来运行它:
#include "stdlib.fc";
终端的输出结果如下:
import { compileFunc } from '@ton-community/func-js';
import fs from 'fs'; // we use fs for reading content of files
import { Cell } from '@ton/core';
const result = await compileFunc({
targets: ['wallet_v3.fc'], // targets of your project
sources: {
"stdlib.fc": fs.readFileSync('./src/stdlib.fc', { encoding: 'utf-8' }),
"wallet_v3.fc": fs.readFileSync('./src/wallet_v3.fc', { encoding: 'utf-8' }),
}
});
if (result.status === 'error') {
console.error(result.message)
return;
}
const codeCell = Cell.fromBoc(Buffer.from(result.codeBoc, "base64"))[0]; // get buffer from base64 encoded BOC and get cell from this buffer
// now we have base64 encoded BOC with compiled code in result.codeBoc
console.log('Code BOC: ' + result.codeBoc);
console.log('\nHash: ' + codeCell.hash().toString('base64')); // get the hash of cell and convert in to base64 encoded string. We will need it further
完成后,可以使用其他库和语言使用我们的钱包代码检索相同的cell(使用base64编码的输出):
Code BOC: te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==
Hash: idlku00WfSC36ujyK2JVT92sMBEpCNRUXOGO4sJVBPA=
一旦完成,就可以使用其他库和语言,用我们的钱包代码检索相同的 cell (使用 base64 编码输出):
- Golang
import (
"encoding/base64"
"github.com/xssnick/tonutils-go/tvm/cell"
)
base64BOC := "te6ccgEBCAEAhgABFP8A9KQT9LzyyAsBAgEgAgMCAUgEBQCW8oMI1xgg0x/TH9MfAvgju/Jj7UTQ0x/TH9P/0VEyuvKhUUS68qIE+QFUEFX5EPKj+ACTINdKltMH1AL7AOgwAaTIyx/LH8v/ye1UAATQMAIBSAYHABe7Oc7UTQ0z8x1wv/gAEbjJftRNDXCx+A==" // save our base64 encoded output from compiler to variable
codeCellBytes, _ := base64.StdEncoding.DecodeString(base64BOC) // decode base64 in order to get byte array
codeCell, err := cell.FromBOC(codeCellBytes) // get cell with code from byte array
if err != nil { // check if there are any error
panic(err)
}
log.Println("Hash:", base64.StdEncoding.EncodeToString(codeCell.Hash())) // get the hash of our cell, encode it to base64 because it has []byte type and output to the terminal
完成上述过程后,确认我们的cell中正在使用正确的代码,因为哈希值相匹配。
idlku00WfSC36ujyK2JVT92sMBEpCNRUXOGO4sJVBPA=
在构建交易之前,了解State Init非常重要。首先让我们了解TL-B方案:
为部署创建状态初始
在创建信息之前,了解什么是 State Init 是非常重要的。首先让我们来了解一下 TL-B 方案:
选项 | 说明 |
---|---|
split_depth | 该选项适用于高负载智能合约,这些合约可以拆分并位于多个 shardchains。 有关其工作原理的详细信息,请参阅 tblkch.pdf (4.1.6)。 由于只在钱包智能合约中使用,因此只存储0 位。 |
special | 用于 TicTok。每个区块都会自动调用这些智能合约,普通智能合约不需要。相关信息可参见 此章节 或 tblkch.pdf (4.1.6)。本规范中只存储了 0 位,因为我们不需要这样的函数。 |
code | 1 位表示存在智能合约代码作为参考。 |
data | 1 位表示存在智能合约数据作为参考。 |
library | 在 主链 上运行的库,可用于不同的智能合约。它不会用于钱包,因此其位设置为 0 。相关信息可参见 tblkch.pdf (1.8.4)。 |
接下来,我们将准备 initial data
,这些数据将在部署后立即出现在我们的合约存储中:
- JavaScript
- Golang
import { beginCell } from '@ton/core';
const dataCell = beginCell()
.storeUint(0, 32) // Seqno
.storeUint(698983191, 32) // Subwallet ID
.storeBuffer(keyPair.publicKey) // Public Key
.endCell();
dataCell := cell.BeginCell().
MustStoreUInt(0, 32). // Seqno
MustStoreUInt(698983191, 32). // Subwallet ID
MustStoreSlice(publicKey, 256). // Public Key
EndCell()
在这个阶段,合约的 code
和 initial data
都已存在。有了这些数据,我们就可以生成钱包地址。钱包地址取决于 State Init,其中包括代码和初始数据。
- JavaScript
- Golang
import { Address } from '@ton/core';
const stateInit = beginCell()
.storeBit(0) // No split_depth
.storeBit(0) // No special
.storeBit(1) // We have code
.storeRef(codeCell)
.storeBit(1) // We have data
.storeRef(dataCell)
.storeBit(0) // No library
.endCell();
const contractAddress = new Address(0, stateInit.hash()); // get the hash of stateInit to get the address of our smart contract in workchain with ID 0
console.log(`Contract address: ${contractAddress.toString()}`); // Output contract address to console