揭秘 Aptos Object 和 Token V2 — 第二部分

Overmind
Overmind_xyz
Published in
8 min readJul 30, 2023

关于 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_internalmintmint_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_u8, from_bcs::to_bool

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

--

--

Overmind
Overmind_xyz

The first web3 solve-to-earn platform where developers compete on coding puzzles to earn prizes and on-chain credentials. Live on #Aptos.