万字详解Aptos Object和Token V2.0 – OverMind

作者:TC_OverMind

Aptos 对象标准、Token V2 及 Sword of a Thousand Truths 任务。

AIP-10 Object 在 Token V2 和 Fungible Assets 的实现之前就已经引入, 但只有通过真实的使用案例, 我们才能更好的理解 AIP-10。

对象标准(Object Standard)具有高度的可组合性, 通过 Token V2, 我们可以看到 V2 类型的 NFT 由四个组件构成, 分别是:

  1. Token 对象 (Token Object) — Token 本身。
  2. 集合对象 (Collection Object) — Token 所属的集合
  3. 属性映射对象 (PropertyMap Object) — 保存关于 Token 的可定制属性,如游戏数据, 稀有度等。
  4. 版税对象 (Royalty Object) — 向创作者支付的版税百分比。

这四个组件构成了完整的 Token V2, 并被保存在 aptos-token-object 目录下的 aptos_token 文件中。此外,Aptos 还实现了一个可替换资产(Fungible Asset)模块, 并为可替换资产提供了元数据对象(Metadata Object),以覆盖最常见的使用场景, 不过开发者仍然能够通过对象标准(Object Standard)创建出不受限制的自定义资产。

在 Overmind 的一个中等难度的任务中,受 South Park 的 “Make love, not Warcraft” 一集启发, 深入探讨了 Token V2,并探索了属性映射(property map)、能力(capabilities)和集合(collections)的使用。

对象标准(Object Standard)

对象(Objects)、账户(Accounts)和事件(Events)

对象和账户在 Aptos Move 的非常重要。它们是仅有的能够发出事件(emit Event)的两种资源。我们可以在事件模块中轻易验证这点,因为只有 object 和 account两个模块被声明为友元模块(friend module) 。这意味着只有这两个模块能够调用在事件模块(event module)中标记为 public(friend) 的函数。

复习一下:友元(friend)是一个 move 函数可见性声明,位于公开(public)和私有(private)之间。友元模块可以访问标记为 public(friend) 的函数。

如果一个地址下面存储了 Account 这个资源, 我们就认为它是一个账户(account)。

struct Account has key, store {
authentication_key: vector<u8>,
sequence_number: u64,
guid_creation_num: u64,
coin_register_events: EventHandle<CoinRegisterEvent>,
key_rotation_events: EventHandle<KeyRotationEvent>,
rotation_capability_offer: CapabilityOffer<RotationCapability>,
signer_capability_offer: CapabilityOffer<SignerCapability>,
}

同样的,如果一个地址下面存储了 ObjectCore 这个资源, 我们就认为它是一个对象(object).

struct ObjectCore has key {
guid_creation_num: u64,
owner: address,
allow_ungated_transfer: bool,
transfer_events: event::EventHandle<TransferEvent>,
}

我们可以通过 Account 的 GUID 来创建对象, 并记录创建对象的数量。因为对象(object)有自己的地址,并且可以生成的自己的签名者(signer),所以对象(Object)可以使用同样的方法生成子对象。对象(object)还有几个用来与账户交互的函数,如对象创建和用账户地址及 GUID 检索对象。不过目前 Aptos 已经不推荐使用 UUID 的方式来创建对象,后续会改为用 transaction_context 的 AUID(aptos unique identifier)。

当然两者还有一些差异,账户更注重用户(user)和钱包(wallet),而对象更注重数据和转移。快速了解一个模块中什么是重要的一个好方法是查看它发出(emit)的事件。这些会被永久记录在链上,供所有人索引。账户和对象模块都能通过 new_event_handle 函数来为用户定义的事件(user defined events)和原生事件(natively defined events)创建 EventHandle,账户中定义的原生事件是 KeyRotationEvent 和 CoinRegistrationEvent,而对象只包含 TransferEvent 结构 。

密钥旋转(Key rotation)允许钱包采用不同的公钥和私钥,仍然能够正确恢复;我们通过 coin模块的 register 函数来使账户(account)能够发送和接收它已经注册的币,类似于 Solidity 中的批准(approve)函数。

而 `TransferEvent` 只包含三个类型为 address 的字段, tofrom 和 object。这表示对于代币(token)、能力(capability),或与对象相关的特殊功能如版税(royalty)的转移 — 所有这些都以灵活和可组合的方式完成。

struct TransferEvent has drop, store {
object: address,
from: address,
to: address,
}

这里还有一个地方需要注意下,Object结构体只包含了一个地址类型的字段,用于指向对象所的地址,并且结构体本身是泛型(generic)的。

struct Object<phantom T> has copy, drop, store {
inner: address,
}

这使得我们可以很方便的对同一地址下的不同资源以对象的形式进行转换。可以参考下 object 模块下的 convert函数。泛型 T 只会用来校验该类型的资源是否存在于当前地址下。

public fun convert<X: key, Y: key>(object: Object<X>): Object<Y> {
address_to_object<Y>(object.inner)
}
public fun address_to_object<T: key>(object: address): Object<T> {
assert!(exists<ObjectCore>(object), error::not_found(EOBJECT_DOES_NOT_EXIST));
assert!(exists_at<T>(object), error::not_found(ERESOURCE_DOES_NOT_EXIST));
Object<T> { inner: object }
}

复习一下:账户和对象是 Move 的核心。账户面向钱包和用户,对象专注于数字资产及其转移。

对象标准详解

通过查看 Aptos 的参考文档,第一感觉是对象有大量的结构体(struct)和函数(function)。不过,我们可以很容易地对它们进行分组,因为许多操作都是相似的, 或是对底层函数的封装。

首先,看一下 ObjectCore 这个资源。它有四个字段:

  1. guid_creation_num 确保所有对象在创建时都是唯一的,并且可以被跟踪和识别。
  2. owner用来指定所有权,可以是用户或另一个对
  3. allow_ungated_transfer用来控制对象的转移。如果代币的转移是无限制的(ungated),它可以像在空投中那样自由分发。如果不是,它只能通过 TransferRef或 LinearTransferRef 来明确授权,这对于在投注、投资或甚至防欺诈策略的使用案例中锁定(冻结)代币是有用的
  4. transfer_events当所有权发生转变时发出 TransferEvent事件

TransferRef 是许多 “refs” 中的一个。进一步,我们看到还有两个的对象结构(它们不如 ObjectCore 重要),和其它一些 refs (本质上是各种许可):

  • ConstructorRef — 只在创建对象时生成,可以用来配置对象, 生成其他的 “ref”。通过代码我们看到它只有 drop`这个能力(capability), 没有 copy和 store, 所以它不能被存储和拷贝, 只能一次性的使用. 就像 Solidity 合约中的构造函数。
  • DeleteRef — 用于删除对象。
  • ExtendRef— 允许在对象的地址下存储额外的事件(event)或资源(resource)。
  • TransferRef — 用于对象转移。
  • LinearTransferRef— 对象只能从当前所有者转移一次。
  • DeriveRef— 允许从当前对象创建派生对象。

在所有的 refs 中,ConstructorRef 是最重要的。我们看下 generate_x_ref 系列函数,所有的都需要 ConstructorRef 作为输入,并直接从 ConstructorRef本身生成相应的 ref。ConstructorRef也可以通过 (address|object)_from_constructor_ref 来获得对象地址或对象本身,也可以通过 generate_signer 来为对象生成一个签名者(signer),方式类似于资源账户(resource account)的 `signer_capability`。

第二重要的 ref 是 DeleteRef。想象一下,如果有人可以随意地删除代币或 NFT, 那会有很多资产丢失。函数 can_generate_delete_ref`用来检查是否应该允许创建 DeleteRef,它需要 `ConstructorRef` 作为参数。

对象的核心操作是转移(transfer)。为此,对象模块提供了一系列转移(transfer)相关的函数,方便在不同的场景下调用。让我们直接看最核心的一个 transfer_raw,大部分转移相关的函数都是包装了 transfer_raw 函数。此外,还有一些函数专门用于无限制(ungated)的转移。

我们看下右边的 transfer_raw 函数,在第 421 行,它检查对象及其所有后代(也就是嵌套对象)是否也是无限制的(ungated)。这种对于后代的检查最多只允许 8 层嵌套。接下来的几行会检查发送者和接收者是否相同,然后发出一个事件,并将新所有者设置为接收者地址。

关于如何对无限制转移(ungated)的对象进行冻结,我们将在第二部分深入探讨,该部分探讨了 token V2 标准。可以使用 LinearTransferRef 覆盖并执行有门槛的转移,但这超出了这个任务的范围。

以上就是对象标准的全部内容!这个模块对所有权、可组合性、能力和转移管理都进行了改进。

二:关于 Token V2 及其可组合对象。

在第一部分中,我们讨论了对象标准(Object standard)及其可组合性、所有权(ownership)、转移(transfer)和能力(capability)。接下来,让我们看一下对象(object)在 NFT 中的具体实现 — — Token V2。

为了快速了解 `aptos_token.move` 中的内容,可以先看下参考文档。我们可以根据函数名的开头来对这些函数进行分类:

– “is / are” 开头的是返回布尔值的视图函数(view function),表示状态为真或假。
– “set / update / add” 开头的是是设置函数(setter function),它们会改变对象的内部值。

除了以上这两类函数, 还有几个关键的函数我们可以看下:

  • create_collection:首先会创建一个集合对象(Collection object)和一个版税对象(Royalty object), 接着再创建一个 `AptosCollection` 对象并将其存储在集合对象的地址下。`Collection` 这个结构体主要用来保存集合的基本属性, 如描述,名称等;而 `AptosCollection` 会保存一些额外的属性,如描述是否允许修改,各种和 Collection 有关的 `refs`, 如 `collection::MutatorRef`。
  • mint_internalmint和 mint_soulbound 两个在内部主要都是调用这个函数。该函数创建一个 `Token` 对象,然后根据对应集合对象(Collection) 创建所需的 `refs`, 如 `MutatorRef` 和 `BurnRef`, 这些 ‘refs’ 会用来创建 `AptosToken` 对象,同样的,新建的 `AptosToken` 也是存储在 `Token` 对象的地址下。最后,会用 `Token` 的 `constructor_ref` 创建一个 `PropertyMap` 对象, 用来保存 token 的各种自定义属性。和 `AptosCollection` 类似, `AptosToken` 是用来存储和 `Token` 有关的 ‘refs’, 如 `token::MutatorRef`
  • freeze / unfreeze_transfer:这两个函数会调用 object 模块的 `enable / disable_ungated_transfer` 函数。本质上,`freeze_transfer` 禁止 NFT 的自由转移,只允许有授权的转移(通过 `TransferRef` 来转移)。
  • burn:通过析构(deconstruct)来销毁对象,之后会调用 `property_map` 模块和 `token` 模块的 `burn` 函数来销毁相应的对象。

如果你之前开发过 Solidity 程序,可能会觉得有点熟悉。`aptos_token` 对象有自己的地址和签名权力,让人想起了 ERC721 的 token 合约。NFT 的铸造和销毁都可以通过调用 aptos_token 模块的相应方法实现,而不需要首先确定用户的地址。在实现上,aptos_token 模块创建了四个对象,并存放在不同的地址。

在这篇文章中,我们不会讨论版税对象(royalty object),因为我们在 《Longswords and Dragons》 任务中并没有使用到。

构成 aptos_token 的四个主要对象

简单来说,如果一下地址下面有 ObjectCore这个资源,我们就认为它是一个对象(Object)。通常每个对象都会有一个主对象, 如 `Token` 就是主对象。`Token` 对象同时还包含了 `PropertyMap` 和 `Royalty` 两个对象,他们都存储在同一个地址上,相对于 `Token` 来说,它们属于次要部分,我们称它们为资源, 在代码中可以用 `Object<PropertyMap>` 或 `Object<Royalty>` 引用它们。对于 `Collection` 对象也是如此。在创建过程中,它创建自己的 `Royalty` 对象,并包含内部的 Supply 结构体,但我们认为它们都是 `Collection` 对象的资源。

Collection 对象

`Collections` 有三种类型:

– 固定供应(Fixed Supply)集合: 内部会用到 `FixedSupply` 这个结构体, 用来记录这个集合所允许的最大数量(max supply)以及当前的流通量(current supply)。

– 无限供应(Unlimited Supply)集合: 内部使用 `UnlimitedSupply` 结构体, 记录当前的铸造量和流通量(current supply = total minted — total burned),但没有最大上限。

– 非记录(Untracked)的集合: 对供应量和流通量都不记录。我们可以使用 `create_untracked_collection` 创建这类集合。注意,这个函数上面有一个 TODO:“Hide this until we bring back meaningful way to enforce burns”。

对于集合,最主要的属性是创建者、名称、描述和 uri 。我们可以看到,有大部分函数都是对这几个字段的修改和查看。另外,集合需要使用其名称作为种子(seed)和创建者的地址来生成确定的地址,内部是通过调用 `object` 模块 `create_named_object` 来实现,它通过使用这两个输入的 sha-256 哈希来创建集合的对象地址。

除了可以在自定义模块触发的事件(如在 Longswords and Dragons 任务中所示的那示),Collection 对象所包含 Supply 结构体也会在 Token 的铸造和和销毁时触发事件。

Token 对象

`Token` 把 NFT 所需要的各个方面组合了的一起。它有自己的描述、名称和 uri,同时也存储了它所属的集合(collection)和索引(index) — — 它在集合中的唯一标识符。

要理解 Token 对象的创建过程,我们主要来看下 `create_common` 这个内联函数(inline function)。它首先获取 token 所属的集合的信息,使用集合对象中的总铸造数(total_minted)生成 token 索引,然后创建 `Token` 对象,如果需要,会同时创建 token 的版税资源。该函数需要 `ConstructorRef` 做为参数,其他函数在调用 `create_common` 之前,都会先调用 object 模块创建对象的函数,来获得一个 `ConstructorRef`。如果是调用 `create_named_token` 创建 Token, 种子(seed)是由其集合名称和自己的名称的哈希创建的。

注:token::create_from_account 目前被标记为已弃用,但仍在使用。可能会转向使用来自交易上下文的 uuid。

PropertyMap 对象

这里我们需要了解两个结构体 `PropertyMap` 和 `PropertyValue`。`PropertyMap` 本身是对 `SimpleMap` 的封装。属性的名称(String 类型)作为键(key); 属性的类型(type)和值(value)转换为 UTF-8 的字节后,一起构成 `PropertyValue`,作为 `SimpleMap` 的值(value)。为了更好的使用 `PropertyMap` ,我们需要分清每个部分的类型,所以总结一下:

  • keys — `String`
  • types — `u8`, 每个类型对有一个对应的值
  • values — `vector<u8>`,通过 BCS 进行序列化

这里需要注意下, `prepare_input` 函数需要的三个参数的类型分别是 `String`, `String`, `vector<u8>` ,这与构成 `PropertyMap` 的三个字段的类型不同,有点容易产生混淆。这里为了方便/可读性,值的类型用 `String` 传入。在 `prepare_input` 内部,会用内联函数 `to_internal_type`,把类型输入转换为 0–9 的数值(u8)。同样的,还有一个 `to_external_type` 函数,将这种内部表示形式转换回成可读性较好的字符串。

property_map 模块还有一些和 simple_map 模块类似的函数,如 `contains_key`, `length`, `add` 等。`init` 函数会把通过 `prepare_input` 生成的 `PropertyMap` 对象存储在 `Token` 所在的地址下; `burn` 用于在 `Token` 被销毁时,删除相应的 `PropertyMap` 对象。 剩下的就一些读函数,用于方便的读取某个类型的值, 如 `read_bool`, `read_u8` 等。

Move 序列化:bcs 和 from_bcs

作为一种强类型语言,Move 只能将整数转换为不同大小的整数(例如,u8 转换为 u16)。其余的类型可以通过 BCS 来将数据序列成字节数组(bytes):

  • bcs::to_bytes(variable): 将任何类型转换为 BCS 格式的字节数组(vector<u8>)。这也是 bcs 这个模块中唯一的函数。
  • from_bcs::to_x(variable):将 BCS 格式的数据转换为所需的 Move 类型。将 x 替换为类型, 如 from_bcs::to_u8from_bcs::to_bool

这使得我们可以用统一的方式,在 Move 类型和字节组(bytes, vector<u8>)之间转换,非常简洁。

OverMind是一个Move语言教育平台,您可以在这里学习Aptos Move智能合约,参加挑战赛赚取奖励并打造专属您的区块链简历。

以上内容均转载自互联网,不代表AptosNews立场,不是投资建议,投资有风险,入市需谨慎,如遇侵权请联系管理员删除。

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023年7月2日
下一篇 2023年7月5日

相关文章

发表回复

登录后才能评论
微信扫一扫
百度扫一扫

订阅AptosNews

订阅AptosNews,掌握Aptos一手资讯。


窗口将关闭与 25

本站没有投资建议,投资有风险,入市需谨慎。