使用钱包智能合约的工作
👋 介绍
在开始智能合约开发之前,学习 TON 上的钱包和交易如何工作是必不可少的。这些知识将帮助开发者了解钱包、交易和智能合约之间的交互,以实现特定的开发任务。
在本节中,我们将学习如何创建操作,而不使用预配置的函数,以了解开发工作流程。本教程的所有必要参考资料都位于参考章节。
💡 必要条件
这个教程需要对 JavaScript、TypeScript 和 Golang 有基本的了解。同时至少需要持有 3 个 TON(可以存储在交易所账户、非托管钱包中,或使用电报机器人钱包进行存储)。此外,还需要对 cell(单元)、TON 地址 和区块链的区块链 有基本的了解,以理解本教程。
在 TON 测试网上工作往往会导致部署错误、难以跟踪交易以及不稳定的网络功能。因此,完成大部分开发工作时间可能好处是建议在 TON Mainnet 上完成,以避免这些问题,这可能需要减少交易数量,从而可能减小费用。
源代码
本教程中使用的所有代码示例都可以在以下 GitHub 存储库 中找到。
✍️ 您开始所需的内容
- 确保 NodeJS 已安装。
- 需要特定的 Ton 库,包括:@ton/ton 13.5.1+、@ton/core 0.49.2+ 和 @ton/crypto 3.2.0+。
可选: 如果您喜欢使用 Golang 而不是使用 JS,那么需要安装 tonutils-go 库以及 GoLand IDE,用于进行 TON 开发。本教程中将使用这个库来进行 Golang 版本的操作。
- 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
会自动重新启动节点应用程序。
- 删除
tsconfig.json
中的以下行:
"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。
所有代码组件都应添加到在⚙ 设置您的环境 部分中创建的 main
函数中。
另外,下面的每个新部分将指定每个新部分所需的特定代码部分,并且需要将新的导入与旧导入合并起来。
🚀 让我们开始!
在本教程中,我们将学习在 TON 区块链上最常使用的钱包(版本 3 和 4),并了解它们的智能合约是如何工作的。这将使开发人员更好地理解 TON 平台上的不同类型的交易,以便更简单地创建交易、将其发送到区块链、部署钱包,并最终能够处理高负载的钱包。
我们的主要任务是使用 @ton/ton、@ton/core、@ton/crypto 的各种对象和函数构建交易,以了解大规模交易是怎样的。为了完成这个过程,我们将使用两个主要的钱包版本(v3 和 v4),因为交易所、非托管钱包和大多数用户仅使用这些特定版本。
在本教程中,可能会有一些细节没有解释。在这些情况下,将在本教程的后续阶段提供更多细节。
重要: 在本教程中,我们使用了 wallet v3 代码 来更好地理解钱包开发过程。需要注意的是,v3 版本有两个子版本:r1 和 r2。目前,只使用第二个版本,这意味着当我们在本文档中提到 v3 时,它指的是 v3r2。
💎 TON 区块链钱包
在 TON 区块链上运行的所有钱包实际上都是智能合约,与 TON 上的一切都是智能合约的方式相同。与大多数区块链一样,可以在网络上部署智能合约并根据不同的用途自定义它们。由于这个特性,完全自定义的钱包是可能的。 在 TON 上,钱包智能合约帮助平台与其他智能合约类型进行通信。然而,重要的是要考虑钱包通信是如何进行的。
钱包通信
通常,在 TON 区块链上有两种交易类型:internal
和 external
。外部交易允许从外部世界向区块链发送消息,从而与接受此类交易的智能合约进行通信。负责执行此过程的函数如下:
() recv_external(slice in_msg) impure {
;; 一些代码
}
在我们深入研究钱包之前,让我们先看看钱包如何接受外部交易。在 TON 上,所有钱包都持有所有者的 公钥
、seqno
和 subwallet_id
。接收到外部交易时,钱包使用 get_data()
方法从钱包的存储部分中检索数据。然后进行多个验证流程,并决定是 否接受此交易。这个过程的完成如下:
() recv_external(slice in_msg) impure {
var signature = in_msg~load_bits(512); ;; 从消息体中获取签名
var cs = in_msg;
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()); ;; 检查交易的有效性
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)); ;; 从存储中读取值
ds.end_parse(); ;; 确保变量 ds 中没有任何数据
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(序列号)直接相关,它跟踪哪些交易以什么顺序发送。不能重复发送钱包中的单个交易非常重要,因为这会完全破坏系统的完整性。如果进一步检查智能合约代码,通常会处理 seqno
如下:
throw_unless(33, msg_seqno == stored_seqno);
上述代码将检查在交易中获得的 seqno
是否与存储在智能合约中的 seqno
相匹配。如果不匹配,则合约返回带有 33 exit code
的错误。因此,如果发送者传递了无效的 seqno
,则意味着他在交易序列中犯了一些错误,合约保护了这些情况。
还需要确认外部消息可以由任何人发送。这意味着如果您向某人发送 1 TON,其他人也可以重复该消息。但是,当 seqno 增加时,以前的外部消息失效,并且没有人可以重复该消息,从而防止窃取您的资金。
签名
如前所述,钱包智能合约接受外部交易。然而,这些交易来自外部世界,这些数据不能 100% 可信。因此,每个钱包都存储所有者的公钥。当钱包接收到所有者使用私钥签名的外部交易时,智能合约使用公钥验证交易签名的合法性。这样可以验证交易实际上是来自合约所有者的。
要执行此过程,首先钱包需要从传入消息中获取签名,从存储中加载公钥,并使用以下过程验证签名:
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 中在有效之前的时间。如果此验证过程失败,则合约完成交易处理并返回 32 退出码,如下所 示:
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); ;; 加载交易模式
send_raw_message(cs~load_ref(), mode); ;; 使用 load_ref() 将每一个新的内部消息作为一个带有 load_ref() 的cell,并发送它
}
在 TON 上,所有智能合约都在基于堆栈的 TON 虚拟机(TVM)上运行。~ touch() 将变量 cs
放置在堆栈的顶部,以优化代码运行以节省 gas。
由于一个cell中最多可以存储 4 个引用,我们可以每个外部消息发送最多 4 个内部消息。
💡 有用的链接:
📬 外部和内部交易
在本节中,我们将学习有关 internal
和 external
交易的更多信息,并创建交易并将其发送到网络中以尽量减少使用预先创建的函数。
为了完成此过程,需要使用一个预先制作的钱包使任务变得更容易。为此:
- 安装 wallet 应用程序(例如,Tonkeeper 是作者使用的)。
- 将钱包应用切换到 v3r2 地址版本。
- 向钱包存入 1 TON。
- 将交易发送到另一个地址(可以发送给自己,发送到同一个钱包)。
这样,Tonkeeper 钱包应用程序将部署钱包合约,我们可以在以下步骤中使用它。
在撰写本文时,TON 上的大多数钱包应用程序默认使用钱包 v4 版本。在本教程中,不需要使用插件的功能,因此我们将使用钱包 v3 提供的功能。在使用过程中,Tonkeeper 允许用户选择他们想要的钱包版本。因此,建议部署钱包版本 3(wallet v3)。
TL-B
如前所述,TON 区块链上的所有内容都是由cell组成的智能合约。为了正确进行序列化和反序列化过程,创建了 TL-B
作为一种通用工具,用于以不同的方式、不同的顺序来描述cell中的不同数据类型。
在本节中,我们将详细研究 block.tlb。在将来的开发中,此文件将非常有用,因为它描述了不同cell的组装方式。在我们的情况下,它详细描述了内部和外部交易的复杂性。
本指南将提供基本信息。有关更多详细信息,请参阅我们的 TL-B 文档,以了解更多关于 TL-B 的知识。
CommonMsgInfo
首先,每个消息必须首先存储 CommonMsgInfo
(TL-B)或 CommonMsgInfoRelaxed
(TL-B)。这允许我们定义与交易类型、交易时间、接收者地址、技术标志位和费用相关的技术细节。
通过阅读 block.tlb
文件,我们可以注意到 CommonMsgInfo有三种不同的类型:int_msg_info$0
、ext_in_msg_info$10
、ext_out_msg_info$11
。我们将不对 ext_out_msg_info
的 TL-B 结构的具体细节进行详细解释。但需要注意的是,它是由智能合约发送的外部交易类型,用作外部日志。要查看此格式的示例,请仔细查看 Elector 合约。
您可以从 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
表示两个零位),因此无需覆盖数据。
$
符号后面的数字是在某个结构的开始处所要求存储的位,以便在读取时(反序列化)可进一步识别这些结构。
创建内部交易
内部交易用于在合约之间发送消息。当分析发送使用合约进行编写的各种合约类型(例如 NFTs 和 Jetons),常常会使用以下代码行:
var msg = begin_cell()
.store_uint(0x18, 6) ;; 或者 0x10 代表不可弹回
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; 默认的消息头(请参阅发送消息页面)
;; 作为存储体
让我们首先考虑 0x18
和 0x10
(x - 16 进制),这些十六进制数是按以下方式排列的(考虑到我们分配了 6 个位):011000
和 010000
。这意味着,可以将上述代码重写为以下内容:
var msg = begin_cell()
.store_uint(0, 1) ;; 这个位表示我们发送了一个内部消息,与 int_msg_info$0 对应
.store_uint(1, 1) ;; IHR 禁用
.store_uint(1, 1) ;; 或者 .store_uint(0, 1) 对于 0x10 | 退回
.store_uint(0, 1) ;; 退回
.store_uint(0, 2) ;; src -> 两个零位代表 addr_none
.store_slice(to_address)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) ;; 默认的消息头(请参阅发送消息页面)
;; 作为存储体
现在我们来详细解释每个选项:
选项 | 说明 |
---|---|
IHR Disabled | 当前此选项被禁用(意味着我们存储 1),因为 Instant Hypercube Routing 尚未完全实现。此外,当网络上有大量 Shardchains 时,这将是必要的。有关禁用 IHR 的更多信息,请阅读tblkch.pdf(第 2 章)。 |
Bounce | 发送交易时,在处理智能合约期间可能发生各种错误。为了避免失去 TON,需要将 Bounce 选项设置为 1(true)。在这种情况下,如果在交易处理过程中发生任何合约错误,该交易将返回给发送者,并会收到总量减去手续费的 TON。有关无法反弹的消息的更多信息,请参阅 此处。 |
Bounced | 弹回交易是发送者返回的交易,因为在处理交易时与智能合约发生了错误。此选项告诉您接收到的交易是否被弹回。 |
Src | Src 是发送者地址。在这种情况下,写入了两个零位以指示 addr_none 地址。 |
接下来的两行代码: |
...
.store_slice(to_address)
.store_coins(amount)
...
- 我们指定了接收方和要发送的 TON 数量。
最后,我们来看剩下的代码行:
...
.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
;; 作为存储体
选项 | 说明 |
---|---|
Extra currency | 这是现有 jettons 的本地实现,目前没有在使用中。 |
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 来显示该body作为引用使用。如果位为 0 ,则body在与消息相同的cell中。 |
上述值(包括 Src)具有以下特征,但不包括 State Init 和 Message Body 位,由验证者重写。
如果数字值适合的位数比指定的位数少,则在值的左侧添加缺少的零位。例如,0x18 适合 5 位 -> 11000
。然而,由于指定了 6 位,最终结果变为 011000
。
接下来,我们将开始准备一个交易,该交易将向另一个钱包 v3 发送 Toncoins。首先,假设用户想要向自己发送 0.5 TON,并附带文本“你好,TON!”,请参阅本文档的这一部分来了解如何发送带有评论的消息。
- JavaScript
- Golang
import { beginCell } from'@ton/core';
let internalMessageBody = beginCell().
storeUint(0, 32). // 写入 32 个零位以指示接下来将有文本注释
storeStringTail("你好,TON!"). // 写入我们的文本注释
endCell();
import (
"github.com/xssnick/tonutils-go/tvm/cell"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32). // 写入 32 个零位以指示接下来将有文本注释
MustStoreStringSnake("你好,TON!"). // 写入我们的文本注释
EndCell()
上面我们创建了 InternalMessageBody
,其中存储了消息的正文。请注意,在存储不能适合单个cell的文本(1023 位)的情况下,根据以下文档 中的 要求,需要将数据拆分为多个cell。但是,在此阶段,高层级库根据要求创建cell,因此现阶段无需担心这个问题。
接下来,根据我们之前学习的信息,创建 InternalMessage
如下所示:
- JavaScript
- Golang
import { toNano, Address, beginCell } from'@ton/ton';
const walletAddress = Address.parse('把你的钱包地址放这里');
let internalMessage = beginCell().
storeUint(0, 1). // 表示它是一条内部消息 -> int_msg_info$0
storeBit(1). // 禁用 IHR
storeBit(1). // bounce
storeBit(0). // bounced
storeUint(0, 2). // src -> addr_none
storeAddress(walletAddress).
storeCoins(toNano("0.2")). // 金额
storeBit(0). // Extra currency
storeCoins(0). // IHR 费用
storeCoins(0). // Forwarding 费用
storeUint(0, 64). // 创建的逻辑时间
storeUint(0, 32). // 创建的 UNIX 时间
storeBit(0). // 没有 State Init
storeBit(1). // 我们将 Message Body 存储为引用
storeRef(internalMessageBody). // 将 Message Body 存储为引用
endCell();
import (
"github.com/xssnick/tonutils-go/address"
"github.com/xssnick/tonutils-go/tlb"
)
walletAddress := address.MustParseAddr("把你的钱包地址放这里")
internalMessage := cell.BeginCell().
MustStoreUInt(0, 1). // 表示它是一条内部消息 -> int_msg_info$0
MustStoreBoolBit(true). // 禁用 IHR
MustStoreBoolBit(true). // bounce
MustStoreBoolBit(false). // bounced
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(walletAddress).
MustStoreCoins(tlb.MustFromTON("0.2").NanoTON().Uint64()). // 数量
MustStoreBoolBit(false). // Extra 货币
MustStoreCoins(0). // IHR 费用
MustStoreCoins(0). // Forwarding 费用
MustStoreUInt(0, 64). // 创建的逻辑时间
MustStoreUInt(0, 32). // 创建的 UNIX 时间
MustStoreBoolBit(false). // 没有 State Init
MustStoreBoolBit(true). // 我们将 Message Body 存储为引用
MustStoreRef(internalMessageBody). // 将 Message Body 存储为引用
EndCell()
创建消息
有必要检索我们的钱包智能合约的seqno
(序列号)。为此,我们创建了一个Client
,用于发送请求以运行我们的钱包的Get方法“seqno”。还需要添加种子短语(在创建钱包时保存的种子短语)以通过以下步骤对我们的交易进行签名:
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
import { mnemonicToWalletKey } from '@ton/crypto';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
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
。现在需要为我们的钱包创建一条 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). // Transaction expiration time, +60 = 1 minute
storeUint(seqno, 32). // store seqno
storeUint(3, 8). // store mode of our internal transaction
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). // Transaction expiration time, +60 = 1 minute
MustStoreUInt(seqno.Uint64(), 32). // store seqno
MustStoreUInt(3, 8). // store mode of our internal transaction
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()
注意,这里在toSign
的定义中没有使用 .endCell()
。事实上,在这种情况下,需要将toSign
的内容直接传递给消息主体。如果需要编写cell,必须将其保存为引用。
:::提示 钱包 V4
除了我们在钱包 V3中所学习到的基本验证流程,钱包 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个零位(\u0000 )数据类型的数组(addr_none TL-B)。 |
Import Fee | 用于支付导入传入的外部消息的费用的费用。 |
State Init | 和内部消息不同,外部消息中的State Init需要从外部世界部署合约。将State Init与内部消息一起使用,可以使一个合约可以部署另一个合约。 |
Message Body | 必须发送到合约以进行处理的消息。 |
:::提示 0b10
0b10
(b表示二进制)表示一个二进制记录。在此过程中,存储了两个位:1
和 0
,因此我们指定为 ext_in_msg_info$10
。
:::
现在我们有一条准备好发送给我们的合约的消息。为此,首先需要将其序列化为 BOC
(cell集合),然后使用以下代码将其发送:
- 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
}
💡 有用的链接:
结果是,在控制台上得到了我们Boc的输出,并将交易发送到我们的钱包。您可以复制 base64 编码的字符串,然后可以手动发送我们的交易并使用 toncenter 检索哈希。
👛 部署钱包
我们已经学会了创建消息的基础知识,这对于部署钱包非常有帮助。 以前,我们通过钱包应用程序部署钱包,但在这种情况下,我们将需要手动部署钱包。
在本节中,我们将介绍如何从头开始创建钱包(钱包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
私钥用于签署交易,公钥存储在钱包的智能合约中。
需要将生成的助记词种子短语输出到控制台,然后保存和使用它(如前面的部分中所述),以便每次运行钱包代码时都使用同一对密钥。
子钱包 ID
钱包作为智能合约的最显着优势之一是能够仅使用一个私钥创建大量的钱包。这是因为TON区块链上的智能合约地址是使用多个因素计算出来的,其中包括stateInit
。stateInit包含了代码
和初始数据
,这些数据存储在区块链的智能合约存储中。
通过在stateInit中只更改一个位,可以生成一个不同的地址。这就是为什么最初创建了subwallet_id
。subwallet_id
存储在合约存储中,可以用于使用一个私钥创建许多不同的钱包(具有不同的子钱包ID)。当将不同钱包类型与交易所等集中服务集成时,这种功能非常有用。
根据TON区块链的源代码中的代码行,默认的subwallet_id
值为698983191
:
res.wallet_id = td::as<td::uint32>(res.config.zero_state_id.root_hash.as_slice().data());
可以从配置文件中获取创世块信息(zero_state)。了解其复杂性和细节并非必要,但重要的是要记 住subwallet_id
的默认值为698983191
。
每个钱包合约都会检查外部交易的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
编译钱包代码
既然我们已经明确定义了私钥、公钥和子钱包ID,我们需要编译钱包代码。为此,我们将使用官方库中的钱包v3代码。
为了编译钱包代码,我们需要使用@ton-community/func-js库。使用这个库,我们可以编译FunC代码并检索包含代码的cell。要开始使用,需要安装库并将其保存(--save)到package.json
中,如下所示:
npm i --save @ton-community/func-js
我们将仅使用JavaScript来编 译代码,因为用于编译代码的库基于JavaScript。 但是,一旦编译完成,只要我们拥有编译后的cell的base64输出,就可以在其他编程语言(如Go等)中使用这些编译后的代码。
首先,我们需要创建两个文件:wallet_v3.fc
和stdlib.fc
。编译器和stdlib.fc库一起使用。库中创建了所有必需的基本函数,这些函数对应于asm
指令。可以从这里下载stdlib.fc文件。在wallet_v3.fc
文件中,需要复制上面的代码。
现在,我们为我们正在创建的项目有了以下结构:
.
├── 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";
冲突,这没关系。
请记住,在wallet_v3.fc
文件的开头添加以下行,以指示将在下面使用stdlib中的函数:
#include "stdlib.fc";
现在,让我们编写代码来编译我们的智能合约并使用npm run start:dev
来运行它:
import { compileFunc } from '@ton-community/func-js';
import fs from 'fs'; // 我们使用fs来读取文件内容
import { Cell } from '@ton/core';
const result = await compileFunc({
targets: ['wallet_v3.fc'], // 您的项目的目标
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]; // 从base64编码的BOC中获取缓冲区,并从该缓冲区获取cell
// 现在我们获得了包含编译代码的base64编码的BOC
console.log('Code BOC: ' + result.codeBoc);
console.log('\nHash: ' + codeCell.hash().toString('base64')); // 获取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==" // 保存我们从编译器保存的base64编码输出到 变量
codeCellBytes, _ := base64.StdEncoding.DecodeString(base64BOC) // 解码base64以获取字节数组
codeCell, err := cell.FromBOC(codeCellBytes) // 从字节数组获取包含代码的cell
if err != nil { // 检查是否有任何错误
panic(err)
}
log.Println("Hash:", base64.StdEncoding.EncodeToString(codeCell.Hash())) // 获取cell的哈希,将其编码为base64,因为它具有[]byte类型,并输出到终端
将会在终端输出以下内容:
idlku00WfSC36ujyK2JVT92sMBEpCNRUXOGO4sJVBPA=
完成上述过程后,确认我们的cell中正在使用正确的代码,因为哈希值相匹配。
创建部署的初始化状态
在构建交易之前,了解State Init非常重要。首先让我们了解TL-B方案:
选项 | 说明 |
---|---|
split_depth | 此选项适用于可以拆分并位于多个分片链上的高负载智能合约。有关此工作原理的更多详细信息,请参见tblkch.pdf(4.1.6)。只存储0 ,因为它仅在钱包智能合约内使用。 |
special | 用于TicTok。这些智能合约会在每个区块自动调用,常规智能合约不需要。关于此的信息可以在此章节中或tblkch.pdf 中找到。此规范中仅存储0 ,因为我们不需要此功能。 |
code | 1 位表示智能合约代码的存在。 |
data | 1 位表示智能合约数据的存在。 |
library | 操作主链上的库,可以由不同的智能合约使用。对于钱包,不会使用它,因此设置为0 。有关此的信息可以在tblkch.pdf(1.8.4)中找到。 |
接下来我们将准备“初始数据”,这将在部署后立即出现在我们合约的存储中:
- 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()
在这个阶段,智能合约代码
和初始数据
都存在。有了这些数据,我们可以生成我们的钱包地址。钱包的地址取决于State Init,其中包括代码和初始数据。
- JavaScript
- Golang
import { Address } from '@ton/core';
const stateInit = beginCell().
storeBit(0). // 没有split_depth
storeBit(0). // 没有special
storeBit(1). // 表示有代码
storeRef(codeCell).
storeBit(1). // 表示有数据
storeRef(dataCell).
storeBit(0). // 没有library
endCell();
const contractAddress = new Address(0, stateInit.hash()); // 获取stateInit的哈希,以获取我们的智能合约在`ID`为0的工作链中的地址
console.log(`Contract address: ${contractAddress.toString()}`); // 将智能合约地址输出到控制台
import (
"github.com/xssnick/tonutils-go/address"
)
stateInit := cell.BeginCell().
MustStoreBoolBit(false). // 没有split_depth
MustStoreBoolBit(false). // 没有special
MustStoreBoolBit(true). // 表示有代码
MustStoreRef(codeCell).
MustStoreBoolBit(true). // 表示有数据
MustStoreRef(dataCell).
MustStoreBoolBit(false). // 没有library
EndCell()
contractAddress := address.NewAddress(0, 0, stateInit.Hash()) // 获取stateInit的哈希,以获取我们的智能合约在`ID`为0的工作链中的地址
log.Println("Contract address:", contractAddress.String()) // 将智能合约地址输出到控制台
使用State Init,我们现在可以构建交易并发送到区块链。要执行此过程,需要一个最低交易余额为0.1 TON(余额可以更低,但此金额足够)。要完成这个操作,我们需要运行教程中提到的代码,获取正确的钱包地址,并向该地址发送0.1 TON。
让我们从构建类似于我们在上一节构建的交易开始:
- JavaScript
- Golang
import { sign } from '@ton/crypto';
import { toNano } from '@ton/core';
const internalMessageBody = beginCell().
storeUint(0, 32).
storeStringTail("Hello, TON!").
endCell();
const internalMessage = beginCell().
storeUint(0x10, 6). // 不使用反弹
storeAddress(Address.parse("put your first wallet address from were you sent 0.1 TON")).
storeCoins(toNano("0.03")).
storeUint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1). // 保存1表示body是引用
storeRef(internalMessageBody).
endCell();
// 用于我们的钱包的交易
const toSign = beginCell().
storeUint(subWallet, 32).
storeUint(Math.floor(Date.now() / 1e3) + 60, 32).
storeUint(0, 32). // 我们将seqno设置为0,因为在部署之后,钱包将将0存储为seqno
storeUint(3, 8).
storeRef(internalMessage);
const signature = sign(toSign.endCell().hash(), keyPair.secretKey);
const body = beginCell().
storeBuffer(signature).
storeBuilder(toSign).
endCell();
import (
"github.com/xssnick/tonutils-go/tlb"
"time"
)
internalMessageBody := cell.BeginCell().
MustStoreUInt(0, 32).
MustStoreStringSnake("Hello, TON!").
EndCell()
internalMessage := cell.BeginCell().
MustStoreUInt(0x10, 6). // 没有反弹
MustStoreAddr(address.MustParseAddr("put your first wallet address from were you sent 0.1 TON")).
MustStoreBigCoins(tlb.MustFromTON("0.03").NanoTON()).
MustStoreUInt(1, 1 + 4 + 4 + 64 + 32 + 1 + 1). // 保存1表示body是引用
MustStoreRef(internalMessageBody).
EndCell()
// 用于我们的钱包的交易
toSign := cell.BeginCell().
MustStoreUInt(subWallet, 32).
MustStoreUInt(uint64(time.Now().UTC().Unix()+60), 32).
MustStoreUInt(0, 32). // 我们将seqno设置为0,因为在部署之后,钱包将将0存储为seqno
MustStoreUInt(3, 8).
MustStoreRef(internalMessage)
signature := ed25519.Sign(privateKey, toSign.EndCell().Hash())
body := cell.BeginCell().
MustStoreSlice(signature, 512).
MustStoreBuilder(toSign).
EndCell()
完成后,结果是正确的State Init和消息体。
发送外部交易
主要的区别将在外部消息的存在上,因为State Init被存储用于正确的合约部署。由于合约尚无自己的代码,因此无法处理任何内部消息。因此,接下来,我们将在成功部署后发送其代码和初始数据,以便可处理我们带有“Hello, TON!”评论的消息:
- JavaScript
- Golang
const externalMessage = beginCell().
storeUint(0b10, 2). // 表示它是一笔外部传入的交易
storeUint(0, 2). // src -> addr_none
storeAddress(contractAddress).
storeCoins(0). // 导入费用
storeBit(1). // 我们有State Init
storeBit(1). // 我们将State Init存储为引用
storeRef(stateInit). // 将State Init存储为引用
storeBit(1). // 我们将消息体存储为引用
storeRef(body). // 将消息体存储为引用
endCell();
externalMessage := cell.BeginCell().
MustStoreUInt(0b10, 2). // 表示它是一笔外部传入的交易
MustStoreUInt(0, 2). // src -> addr_none
MustStoreAddr(contractAddress).
MustStoreCoins(0). // 导入费用
MustStoreBoolBit(true). // 我们有State Init
MustStoreBoolBit(true). // 我们将State Init存储为引用
MustStoreRef(stateInit). // 将State Init存储为引用
MustStoreBoolBit(true). // 我们将消息体存储为引用
MustStoreRef(body). // 将消息体存储为引用
EndCell()
最后,我们可以将我们的交易发送到区块链上部署我们的钱包并使用它。
- JavaScript
- Golang
import { TonClient } from '@ton/ton';
const client = new TonClient({
endpoint: "https://toncenter.com/api/v2/jsonRPC",
apiKey: "put your api key" // 你可以从Telegram中的@tonapibot获得API密钥
});
client.sendFile(externalMessage.toBoc());
import (
"context"
"github.com/xssnick/tonutils-go/liteclient"
"github.com/xssnick/tonutils-go/tl"
"github.com/xssnick/tonutils-go/ton"
)
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)
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
}
请注意,我们使用mode3
发送了一个内部消息。如果需要重复部署相同的钱包,智能合约将被销毁。为此,请正确设置的mode,通过添加128(取整个智能合约的余额)+ 32(销毁智能合约),以获取剩余的TON余额并再次部署钱包。
重要说明:对于每个新的交易,seqno需要增加1。