本来我们来学习如何使用Tact编写NFT合约。
本来我们来学习如何使用 Tact 编写 NFT 合约。在继续之前,建议先阅读上一篇关于编写 Jetton 的文章,有很多概念和细节我们在这篇文章就不再重复了。
TON 上的 NFT 与 Jetton 相同,不能使用无界数据结构。Jetton 中存在大量的用户,因此需要将用户的数据部分独立拆分出来。NFT 中可能存在成千上万个 Item(即 TokenId),因此也需要将 NFT 的合约拆分。
NFT 的合约拆分成了如下两部分:
Collection 合约类似于 Jetton 的 Master 合约,主要存储 NFT 的核心信息,例如 Metadata。Item 则以 NFT 的 TokenId 做区分,每一个 TokenId 是一个 Item,其拥有各自独立的合约。一些基础操作例如 transfer
等都是在 Item 合约上操作。
NFT 与 Jetton 相比,合约架构拆分的角度不同。对于 Jetton 来说,可能用无数个用户,每个用户的余额不尽相同,因此以用户维度来拆分。
对于 NFT 来说,以 Item 维度拆分更加合适。假设一共 Mint 了一万个 Item,那么就有一万个 Item 合约,每个 Item 都有自己的 owner,token uri 等。这些信息都存储在 Item 合约中,每个 Item 都是独立的。
NFT 的架构图例如下:
每套 Collection 包含一个 Collection 合约和 N 个 Item 合约,每个 Item 合约拥有自己的 owner。
TON 为 NFT 制定了一套标准,常用的是 TEP-62,我们在编写 NFT 时,都要遵循这个范式。
本文继续参考 [Ton-Dynasty](https://github.com/Ton-Dynasty/tondynasty-contracts) 的 NFT 实现。
在合约库中找到这几个文件(二级菜单是文件中包含的合约名):
前两个文件对应的分别是 Collection 和 Item 合约,但是它们目前还是抽象基类,第三个 nft_example.tact
包含的是对 Collection 和 Item 的实现。
NFTCollectionStandard
中包含三个重要的变量,分别是:
其中 collection_content
包含该 NFT 的 Metadata,遵循 TEP-64 的标准,Jetton 的 Metadata 也遵循该标准。
纵观 NFTCollectionStandard
合约,可以看到它其实主要是包含了一些 get 方法,方便为链下提供相关的数据。
我们以 get_collection_data
为例,看看其中的一些技术细节:
get fun get_collection_data(): CollectionData {
return self._get_collection_data();
}
virtual inline fun _get_collection_data(): CollectionData {
let builder: StringBuilder = beginString();
let urlPrefix: String = self.collection_content.asSlice().asString();
builder.append(urlPrefix);
builder.append(self.NFT_COLLECTION_STANDARD_METADATA);
return CollectionData {
next_item_index: self.next_item_index,
collection_content: builder.toCell(),
owner_address: self.owner_address
};
}
beginString()
创建并返回一个新的 StringBuilder
,它的作用与其它语言中的类似,可以存储字符串。
我们之前说过 TON 上的所有数据都存储的 Cell 结构中,Storage 变量 collection_content
同样存在于 Cell 中,我们想要读取它,但是 Cell 不能直接被读取。Slice 是一个可以帮助我们读取 Cell 的中间介质,需要使用 asSlice()
将 Cell 转化为 Slice 类型,然后再使用 asString()
将其转化为字符串。
随后将相关的字符串数据都存放在 builder
变量中,最后返回 CollectionData
类型数据。注意 Tact 中不支持方法返回多个变量,因此如果希望返回多个变量,需要额外定义结构体,将相关的数据都放进去。CollectionData
中的 collection_content
是 Cell 类型,因此需要将 builder
通过 toCell()
转化为 Cell 类型。
我们现在来看 nft_example.tact
中的 ExampleNFTCollection
合约,它继承了 NFTCollectionStandard
,重点来看其中的 Mint
接收方法。
Mint NFT 的流程如下:
NFT 的 Owner 向 Collection 合约发送 Mint 消息,随后再流转到 Item 合约。Mint 接收方法如下:
receive("Mint") {
let ctx: Context = context();
let nftItemInit: StateInit =
self._get_nft_item_state_init(self.next_item_index);
send(SendParameters {
to: contractAddress(nftItemInit),
value: self.estimate_rest_value(ctx),
bounce: false,
mode: SendIgnoreErrors,
body: Transfer {
query_id: 0,
new_owner: ctx.sender,
response_destination: ctx.sender,
custom_payload: emptyCell(),
forward_amount: 0,
forward_payload: emptySlice()
}.toCell(),
code: nftItemInit.code,
data: nftItemInit.data
});
self.next_item_index = self.next_item_index + 1;
}
_get_nft_item_state_init
方法获取即将 Mint 的 Item 的合约示例。与 Jetton 中的 Mint 类似,Mint 时发送的消息要附带 code
和 data
。如果该合约还没有被部署,则使用该 code
和 data
来部署合约示例,如果合约之前已经部署了,则忽略。
可以看到这里发送的是 Transfer
消息,与 EVM 中的 NFT Mint 类似,这里可以理解成 0 地址向某地址转账了一个 NFT。
先来看看 Item 合约中都有什么 Storage 变量:
接着继续来看 Mint 过程在 Item 中的部分,由于 Collection 合约中的 Mint 向 Item 合约发送了 Transfer
消息,因此我们需要看 Transfer
接收方法,但是该方法不仅用于 Mint,也用于正常的 Transfer 操作,也就是说用户的正常 Transfer 也是使用的该方法。
receive(msg: Transfer){
let ctx: Context = context();
let remain: Int = self._transfer_estimate_rest_value(ctx);
self._transfer_validate(ctx, msg, remain);
if (self.is_initialized == false) {
self.mint(ctx, msg);
} else {
self.transfer(ctx, msg, remain);
}
}
_transfer_estimate_rest_value
方法用于计算一些预留的 Storage Gas 费用。
_transfer_validate
方法校验该 Transfer
消息的 sender 必须是 Collection 合约或者该 Item 的 owner。当 sender 是 Collection
合约时,为 Mint 操作,当 sender 是 Item 的 owner 时,为 Transfer 操作。
is_initialized
字段代表该 Item 合约是否已经初始化,也就是是否已经部署,默认是 false。如果为 false,那么说明当前操作是 Mint,需要调用 mint
方法。如果为 true,说明当前操作是 Transfer。我们来看 mint
的实现:
virtual inline fun mint(ctx: Context, msg: Transfer) {
require(ctx.sender == self.collection_address, "NFTItemStandard: Only the collection can initialize the NFT item");
self.is_initialized = true;
self.owner = msg.new_owner;
send(SendParameters{
to: msg.response_destination,
value: 0,
mode: SendIgnoreErrors + SendRemainingValue,
body: Excesses { query_id: msg.query_id }.toCell()
});
}
Mint 操作要求当前的 sender 是 Collection 合约。随后将 is_initialized
字段置为 true,说明后续的 Transfer 接收方法都是转账的场景。设置 owner。最后将剩余的 Gas 返还给初始 sender。
到此 Mint 操作就已经完成,大家可以与前面的流程图做对比,看看是否对应。
最后来看 Transfer 部分,Transfer 的流程图如下:
可以看到,用户 Transfer 时,只与 Item 合约交互。之后便向新 owner 的钱包发送 transfer notification
消息,以及返还 Gas。与 Jetton 的 Transfer 相比,简单许多。
回到上面 receive(msg: Transfer)
方法的 transfer
部分:
virtual inline fun transfer(ctx: Context, msg: Transfer, remain: Int) {
self.owner = msg.new_owner;
if (msg.forward_amount > 0) {
send(SendParameters{
to: msg.new_owner,
value: msg.forward_amount,
mode: SendIgnoreErrors,
bounce: false,
body: OwnershipAssigned{
query_id: msg.query_id,
prev_owner: ctx.sender,
forward_payload: msg.forward_payload
}.toCell()
});
}
remain = remain - ctx.readForwardFee();
if (
msg.response_destination != newAddress(0, 0)
&& remain > msg.forward_amount
) {
send(SendParameters{
to: msg.response_destination,
value: remain - msg.forward_amount,
mode: SendPayGasSeparately,
body: Excesses { query_id: msg.query_id }.toCell()
});
}
}
该方法主要有两部分。第一部分是向新的 owner 钱包发送 transfer notification
消息,第二部分是返还 Gas。
与 Jetton 的 notification 类似,当消息中的 forward_amount
字段大于 0 时,说明需要向 owner 钱包发送通知消息,附带的 TON 数量是 msg.forward_amount
。
我们重点来看 remain
变量,它是通过前面的 _transfer_estimate_rest_value
方法计算出来的,代表的是当前合约运行中去除各种费用之后的剩余 TON 数量,也就是可以发送出去的数量。
进入到 transfer
方法之后,remain
部分首先拆分出去了 msg.forward_amount
,在第一个 if
结束时,实际剩余的 TON 数量为 remain - msg.forward_amount
。如果剩余的数量大于 0 则需要返还,小于 0 则无需返还。由于在发送 Excesses
时使用的是 SendPayGasSeparately
机制,因此发送该消息的 邮费需要额外支付,这部分邮费就是 ctx.readForwardFee()
,所以最后在 remain - msg.forward_amount
数量的基础上,仍然需要留有 ctx.readForwardFee()
的数量才能将消息发送出去。
综上所述,最终仍需发送剩余 Gas 的前提是:
remain - msg.forward_amount - ctx.readForwardFee() > 0
理解了这个不等式,就可以理解代码中 remain
相关的计算。
至此,NFT 代码的核心逻辑就已经讲完,还有一些比较简单的方法大家可以自行学习理解。
本文介绍了 TON 上 NFT 的 Tact 实现。TON 上的 NFT 需要将 Collection 和 Item 拆分成两个合约,NFT 的每个 TokenId 就是一个 Item。用户对于 NFT 的转账操作只需要与 Item 合约交互即可,相对 Jetton 的转账逻辑比较简单。大家对于这部分还是要多看多思考,这样可以对 TON 上 NFT 的运行方式有更深的理解。
欢迎和我交流
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!