Appearance
TEP: 62 - NFT 标准
- TEP: 62
- 标题: NFT 标准
- 状态: 活跃
- 类型: 合约接口
- 作者: EmelyanenkoK, Tolya
- 创建日期: 01.02.2022
- 替代: -
- 被替代: -
摘要
一个非同质化代币(NFT)的标准接口。
动机
标准接口将大大简化不同实体之间的交互和所有权表示。
NFT 标准描述了:
- 所有权变更的方式。
- 项目集合的方式。
- 集合公共部分的去重方式。
指南
非同质化代币(NFT)代表对独特数字资产(如小猫图片、产权证书、艺术品等)的所有权。每个单独的代币是一个 NFT 项目。将 NFT 项目归入 NFT 集合 也是方便的。在 TON 中,每个 NFT 项目和 NFT 集合都是独立的智能合约。
NFT 元数据
主要文章: TEP-64
每个 NFT 项目和 NFT 集合本身都有其自己的元数据(TEP-64)。它包含一些关于 NFT 的信息,如标题和相关图像。元数据可以存储在链下(智能合约只包含一个指向 JSON 的链接)或链上(所有数据都存储在智能合约中)。
集合元数据示例(链下):
json
{
"image": "https://ton.org/_next/static/media/smart-challenge1.7210ca54.png",
"name": "TON Smart Challenge #2",
"description": "TON Smart Challenge #2 Winners Trophy",
"social_links": []
}
项目元数据示例(链下):
json
{
"name": "TON Smart Challenge #2 Winners Trophy",
"description": "TON Smart Challenge #2 Winners Trophy 1 place out of 181",
"image": "https://ton.org/_next/static/media/duck.d936efd9.png",
"content_url": "https://ton.org/_next/static/media/dimond_1_VP9.29bcaf8e.webm",
"attributes": []
}
链下元数据例如发布在网络上。
有用的链接
- 参考 NFT 实现
- Getgems NFT 合约
- Toncli NFT 脚手架项目 by Disintar
- TON NFT 部署器
- FunC 课程 - NFT 标准(英文/俄文)
- TON NFT 浏览器
规范
NFT 集合和每个 NFT 项目都是独立的智能合约。
例如:如果你发布一个包含 10,000 个项目的集合,那么你将部署 10,001 个智能合约。
NFT 项目智能合约
必须实现:
内部消息处理程序
1. transfer
请求
入站消息的 TL-B 方案:
transfer#5fcc3d14 query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress custom_payload:(Maybe ^Cell) forward_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) = InternalMsgBody;
query_id
- 任意请求编号。
new_owner
- NFT 项目的新所有者地址。
response_destination
- 发送确认成功转移和剩余消息币的响应地址。
custom_payload
- 可选的自定义数据。
forward_amount
- 要发送给新所有者的 nanotons 数量。
forward_payload
- 应发送给新所有者的可选自定义数据。
应拒绝的情况:
- 消息不是来自当前所有者。
- 没有足够的币(考虑到 NFT 自己的存储费指南)来处理操作并发送
forward_amount
。 - 处理请求后,合约必须至少发送
in_msg_value - forward_amount - max_tx_gas_price
到response_destination
地址。 如果合约不能保证这一点,则必须立即停止执行请求并抛出错误。max_tx_gas_price
是 NFT 栖息地工作链最大交易 gas 限制的 Toncoins 价格。对于基本链,可以从ConfigParam 21
的gas_limit
字段中获取。
否则应做:
- 将 NFT 的当前所有者更改为
new_owner
地址。 - 如果
forward_amount > 0
,则向new_owner
地址发送带有forward_amount
nanotons 的消息,并具有以下布局: TL-B 方案:ownership_assigned#05138d91 query_id:uint64 prev_owner:MsgAddress forward_payload:(Either Cell ^Cell) = InternalMsgBody;
query_id
应与请求的query_id
相等。forward_payload
应与请求的forward_payload
相等。prev_owner
是此 NFT 项目的前所有者地址。 如果forward_amount
等于零,则不应发送通知消息。 - 将入站消息币的所有多余部分发送到
response_destination
,布局如下: TL-B 方案:excesses#d53276db query_id:uint64 = InternalMsgBody;
query_id
应与请求的query_id
相等。
forward_payload
格式
如果你想在 forward_payload
中发送一个简单的评论,那么 forward_payload
必须以 0x00000000
(等于零的 32 位无符号整数)开头,评论包含在 forward_payload
的剩余部分中。
如果评论不以字节 0xff
开头,则评论是文本评论;可以按原样显示给钱包的最终用户(过滤掉无效和控制字符并检查其是否为有效的 UTF-8 字符串后)。 例如,用户可以在此文本字段中指示简单转账的目的(“用于咖啡”)。
另一方面,如果评论以字节 0xff
开头,则剩余部分是“二进制评论”,不应作为文本显示给最终用户(仅在必要时作为十六进制转储显示)。 “二进制评论”的预期用途是,例如,包含商店支付中的购买标识符,由商店的软件自动生成和处理。
如果 forward_payload
包含与目标智能合约交互的二进制消息(例如,与 DEX),则没有前缀。
这些规则与从常规钱包简单发送 Toncoins 时的有效负载格式相同(智能合约指南:内部消息,3)。
2 get_static_data
请求
入站消息的 TL-B 方案:
get_static_data#2fcb26a2 query_id:uint64 = InternalMsgBody;
query_id
- 任意请求编号。
应做:
- 发送回消息,布局如下,发送模式为
64
(返回消息金额减去 gas 费用): TL-B 方案:report_static_data#8b771735 query_id:uint64 index:uint256 collection:MsgAddress = InternalMsgBody;
query_id
应与请求的query_id
相等。index
- 此 NFT 在集合中的数字索引,通常是部署的序列号。collection
- 此 NFT 所属集合的智能合约地址。
Get 方法
get_nft_data()
返回(int init?, int index, slice collection_address, slice owner_address, cell individual_content)
init?
- 如果不为零,则此 NFT 已完全初始化并准备好交互。index
- 此 NFT 在集合中的数字索引。对于无集合的 NFT - 任意但恒定的值。collection_address
- 集合的智能合约地址。对于无集合的 NFT,此参数应为 addr_none;owner_address
- 当前所有者的地址。individual_content
- 如果 NFT 有集合 - 任何格式的单个 NFT 内容; 如果 NFT 没有集合 - 符合标准 TEP-64 的 NFT 内容。
NFT 集合智能合约
假设集合的智能合约部署此集合的 NFT 项目的智能合约。
必须实现:
Get 方法
get_collection_data()
返回(int next_item_index, cell collection_content, slice owner_address)
next_item_index
- 集合中当前部署的 NFT 项目的数量。通常,集合应按顺序发行 NFT(见理由(2))。next_item_index
的值为-1
表示非顺序集合,此类集合应提供其自己的索引生成/项目枚举方式。collection_content
- 符合标准 TEP-64 的集合内容。owner_address
- 集合所有者地址,如果没有所有者则为零地址。get_nft_address_by_index(int index)
返回slice address
获取此集合的 NFT 项目的序列号并返回此 NFT 项目智能合约的地址(MsgAddress)。get_nft_content(int index, cell individual_content)
返回cell full_content
获取此集合的 NFT 项目的序列号和此 NFT 项目的单个内容,并返回符合标准 TEP-64 的 NFT 项目的完整内容。 例如,如果 NFT 项目在其内容中存储一个元数据 URI,则集合智能合约可以存储一个域(例如 "https://site.org/"),而 NFT 项目智能合约在其内容中只存储链接的单个部分(例如 "kind-cobra")。 在此示例中,get_nft_content
方法将它们连接起来并返回 "https://site.org/kind-cobra"。
缺点
由于 TON 是一个异步区块链,因此无法在链上获取 NFT 的当前所有者。当包含 NFT 所有者信息的消息被传递时,该信息可能已经变得不相关,因此我们无法保证当前所有者没有改变。
理由和替代方案
- “一个 NFT - 一个智能合约”简化了费用计算,并允许提供 gas 消耗保证。
- 具有顺序 NFT 索引的 NFT 集合提供了关联和搜索链接 NFT 的简单方法。
- 将 NFT 内容分为单个和公共(集合)部分,可以去重存储以及进行廉价的大规模更新。
为什么不是一个包含 token_id -> owner_address 字典的单个智能合约?
- 不可预测的 gas 消耗 在 TON 中,字典操作的 gas 消耗取决于确切的键集合。 此外,TON 是一个异步区块链。这意味着如果你向智能合约发送消息,那么你不知道在你的消息到达智能合约之前会有多少其他用户的消息到达。 因此,你不知道在你的消息到达智能合约时字典的大小。 这对简单的钱包 -> NFT 智能合约交互来说是可以的,但对智能合约链来说不可接受,例如钱包 -> NFT 智能合约 -> 拍卖 -> NFT 智能合约。 如果我们无法预测 gas 消耗,那么可能会发生这样的情况:所有者在 NFT 智能合约上发生了变化,但拍卖操作没有足够的 Toncoins。 使用没有字典的智能合约可以使 gas 消耗确定。
- 不可扩展(成为瓶颈) TON 的扩展基于分片的概念,即在负载下自动将网络分割为分片链。 流行 NFT 的单个大智能合约与此概念相矛盾。在这种情况下,许多交易将引用一个单一的智能合约。 TON 架构提供了分片智能合约(见白皮书),但目前尚未实现。
为什么没有“批准”?
TON 是一个异步区块链,因此一些同步区块链的方法不适用。
你不能发送消息“是否有批准?”,因为在响应消息到达你时,响应可能已经变得不相关。
如果同步区块链可以在一个交易中检查 alowance
并且一切正常则执行 transferFrom
,那么在异步区块链中你总是需要随机发送 transferFrom
消息,并在发生错误时捕获响应消息并执行回滚操作。
这是一个复杂且不合适的方法。
幸运的是,讨论中出现的所有情况都可以通过常规转移并通知新所有者来实现。在某些情况下,这将需要额外的智能合约。
当你想将 NFT 放在多个市场同时出售时,可以通过创建拍卖智能合约来解决,这些合约首先接受付款,然后将 NFT 发送到其中一个拍卖智能合约。
为什么没有强制性的作者版税?
在开发这个想法的过程中,我们得出结论,只有在一种情况下才可以保证从所有销售中向作者支付版税:
所有转移必须通过公开的长期拍卖进行,并且禁止其他类型的转移。
如果你想将 NFT 转移到另一个钱包,则需要启动拍卖并赢得拍卖。
这种方案的另一种变体是使所有转移都收费。
通过禁止免费转移代币,我们在许多情况下使代币变得不方便 - 用户只是更新了钱包,用户想要捐赠 NFT,用户想要将 NFT 发送到某个智能合约。
考虑到糟糕的可用性以及 NFT 是一个通用概念,并且并非所有 NFT 都是为了销售 - 这种方法被拒绝。
先前的工作
未解决的问题
- 所有者索引尚未实现,是否应在未来标准中实现?
- 没有执行“安全转移”的标准方法,在合约执行失败的情况下将撤销所有权转移。
未来可能性
无
标准扩展
基本 NFT 标准的功能可以扩展:
TL-B 方案
nothing$0 {X:Type} = Maybe X;
just$1 {X:Type} value:X = Maybe X;
left$0 {X:Type} {Y:Type} value:X = Either X Y;
right$1 {X:Type} {Y:Type} value:Y = Either X Y;
var_uint$_ {n:#} len:(#< n) value:(uint (len * 8))
= VarUInteger n;
addr_none$00 = MsgAddressExt;
addr_extern$01 len:(### 9) external_address:(bits len)
= MsgAddressExt;
anycast_info$_ depth:(#<= 30) { depth >= 1 }
rewrite_pfx:(bits depth) = Anycast;
addr_std$10 anycast:(Maybe Anycast)
workchain_id:int8 address:bits256 = MsgAddressInt;
addr_var$11 anycast:(Maybe Anycast) addr_len:(### 9)
workchain_id:int32 address:(bits addr_len) = MsgAddressInt;
_ _:MsgAddressInt = MsgAddress;
_ _:MsgAddressExt = MsgAddress;
transfer query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress custom_payload:(Maybe ^Cell) forward_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) = InternalMsgBody;
ownership_assigned query_id:uint64 prev_owner:MsgAddress forward_payload:(Either Cell ^Cell) = InternalMsgBody;
excesses query_id:uint64 = InternalMsgBody;
get_static_data query_id:uint64 = InternalMsgBody;
report_static_data query_id:uint64 index:uint256 collection:MsgAddress = InternalMsgBody;
标签通过 tlbc 计算如下(请求标志等于 0x7fffffff,响应标志等于 0x80000000):
转移消息
- 消息定义:
transfer query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress custom_payload:Maybe ^Cell forward_amount:VarUInteger 16 forward_payload:Either Cell ^Cell = InternalMsgBody
- CRC32 计算:
crc32('transfer query_id:uint64 new_owner:MsgAddress response_destination:MsgAddress custom_payload:Maybe ^Cell forward_amount:VarUInteger 16 forward_payload:Either Cell ^Cell = InternalMsgBody')
- 结果:
0x5fcc3d14
- 应用请求标志:
0x5fcc3d14 & 0x7fffffff
- 结果:
0x5fcc3d14
- 消息定义:
所有权分配消息
- 消息定义:
ownership_assigned query_id:uint64 prev_owner:MsgAddress forward_payload:Either Cell ^Cell = InternalMsgBody
- CRC32 计算:
crc32('ownership_assigned query_id:uint64 prev_owner:MsgAddress forward_payload:Either Cell ^Cell = InternalMsgBody')
- 结果:
0x85138d91
- 应用请求标志:
0x85138d91 & 0x7fffffff
- 结果:
0x05138d91
- 消息定义:
剩余消息
- 消息定义:
excesses query_id:uint64 = InternalMsgBody
- CRC32 计算:
crc32('excesses query_id:uint64 = InternalMsgBody')
- 结果:
0x553276db
- 应用响应标志:
0x553276db | 0x80000000
- 结果:
0xd53276db
- 消息定义:
获取静态数据消息
- 消息定义:
get_static_data query_id:uint64 = InternalMsgBody
- CRC32 计算:
crc32('get_static_data query_id:uint64 = InternalMsgBody')
- 结果:
0x2fcb26a2
- 应用请求标志:
0x2fcb26a2 & 0x7fffffff
- 结果:
0x2fcb26a2
- 消息定义:
报告静态数据消息
- 消息定义:
report_static_data query_id:uint64 index:uint256 collection:MsgAddress = InternalMsgBody
- CRC32 计算:
crc32('report_static_data query_id:uint64 index:uint256 collection:MsgAddress = InternalMsgBody')
- 结果:
0x0b771735
- 应用响应标志:
0x0b771735 | 0x80000000
- 结果:
0x8b771735
- 消息定义:
致谢
我们感谢 Tonwhales 开发者在当前标准草案上的合作 🤝
更新日志
- 2022年2月1日
- 2022年2月2日
- 2022年2月4日
- 2022年2月8日
- 2022年2月11日
- 2022年7月30日
- 2022年8月31日 - 添加了
forward_payload
格式。