One Time Witness, Publisher, Object Display...
国王将从勇士当中挑选有能力的晋升为骑士,整个过程可以分成如下几点:
思考如何通过一次性见证(One Time Witness),发布者(Publisher)以及对象显示(Object Display)来实现?
注意: 本篇内容仅针对初学者,目的是将这三者尽可能串联起来,形成一个不那么枯燥又容易理解的例子,所以设计过程及最终呈现可能存在优化空间。
一次性见证(One Time Witness, OTW)是一种特殊类型的实例,该类型的定义需要具备如下条件:
OTW 只在模块初始化器中创建,并保证是唯一的,可以用types::is_one_time_witness(&witness)
来判断传入的 witness 是不是 OTW。
借助 OTW,对创建国王的函数进行限制,以此来保证其唯一性。
发布者(Publisher)对象用于代表发布者的权限,它本身并不代表任何特定的用例,主要通过package::from_module<T>
和package::from_package<T>
来检查传入的类型为 T (泛型或指定一个类型)的参数与 publisher 是否处在同一个模块或包中。
为了保证模块当中发布者的唯一性,需要用到上面提到的 OTW,通过package::claim_and_keep(otw, ctx)
创建一个 publisher 并将其所有权转移给发布者;如果不着急转移所有权,可以通过let publisher = package::claim(otw, ctx)
来获得 publisher,接下去可以借助这个对象,来做一些其它的事情(比如定义对象显示等),但是在最后,不要忘记将它的所有权手动通过 transfer 移交给发布者。
借助发布者的权限,比肩神明,唯有此方能指导国王能力提升。
拥有 Publisher 对象的构建者可以通过sui::display
模块来自定义对象的显示属性,以供生态系统在链下处理数据,所有属性都可以通过{property}
语法访问同时作为字符串插入其中,例如:

{
"name": "{name}",
"link": "https://sui-heroes.io/hero/{id}",
"img_url": "ipfs://{img_url}",
"description": "A true Hero of the Sui ecosystem!",
"project_url": "https://sui-heroes.io",
"creator": "Unknown Sui Fan"
}
当MyObject
和Display<MyObject>
匹配时,可以通过web
进行查看自定义的属性显示,具体规范请点击 sui_getObject,不要忘记将其中的 showDisplay 填为 true。
国王:
module king_knight::king { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::types; use sui::package::{Self, Publisher}; const ENOTWITNESS: u64 = 0; const ENOTPACKAGE: u64 = 1; struct King has key { id: UID, ability: u64, } public fun create_king<T: drop>(witness: T, ctx: &mut TxContext) { assert!(types::is_one_time_witness(&witness), ENOTWITNESS); transfer::transfer(King { id: object::new(ctx), ability: 66, }, tx_context::sender(ctx)); } entry fun rise(publisher: &Publisher, king: &mut King) { assert!(package::from_package<King>(publisher), ENOTPACKAGE); king.ability = king.ability + 1; } public fun get_ability(king: &King): u64 { king.ability } }
勇士:
module king_knight::warrior { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use std::string::String; struct Warrior has key { id: UID, name: String, ability: u64, } entry fun create_warrior(name: String, ability: u64, ctx: &mut TxContext) { transfer::transfer(Warrior { id: object::new(ctx), name, ability, }, tx_context::sender(ctx)); } entry fun rise(warrior: &mut Warrior) { warrior.ability = warrior.ability + 1; } public fun get_ability(warrior: &Warrior): u64 { warrior.ability } public fun destroy(warrior: Warrior): (String, u64) { let Warrior{id, name, ability} = warrior; object::delete(id); (name, ability) } }
骑士:
sui::display
当中的函数进行操作,具体请点击这里,当然,最后别忘记调用 update_version 来触发事件,使网络中各个完整节点监听此事件并获取该类型的新显示模板。module king_knight::knight { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use std::string::{Self, String}; use sui::package; use sui::display; struct KNIGHT has drop {} struct Knight has key { id: UID, name: String, ability: u64, } fun init(otw: KNIGHT, ctx: &mut TxContext) { let keys = vector[ string::utf8(b"name is"), string::utf8(b"ability is"), ]; let values = vector[ string::utf8(b"{name}"), string::utf8(b"{ability}"), ]; let publisher = package::claim(otw, ctx); let display = display::new_with_fields<Knight>(&publisher, keys, values, ctx); display::update_version(&mut display); transfer::public_transfer(publisher, tx_context::sender(ctx)); transfer::public_transfer(display, tx_context::sender(ctx)); } public fun create_knight(name: String, ability: u64, ctx: &mut TxContext) { let knight = Knight { id: object::new(ctx), name, ability, }; transfer::freeze_object(knight); } }
交互:
module king_knight::interact { use sui::tx_context::TxContext; use king_knight::warrior::{Self, Warrior}; use king_knight::king::{Self, King}; use king_knight::knight::create_knight; const ENOTENOUGHABILITY: u64 = 0; struct INTERACT has drop {} fun init(otw: INTERACT, ctx: &mut TxContext) { king::create_king(otw, ctx); } entry fun rise(warrior: Warrior, king: &King, ctx: &mut TxContext) { assert!(warrior::get_ability(&warrior) >= king::get_ability(king), ENOTENOUGHABILITY); let (name, ability) = warrior::destroy(warrior); create_knight(name, ability, ctx); } }
sui client publish --gas-budget 100000000
根据发布成功的信息,export 几个值,方便后续调用。

export PACKAGE_ID=0x4f81d54db52fee57cae7f6d22e0746f729c59b3ad4a8f513857cbd57417d18e8
export PUBLISHER=0x44c23e62e75a1c765dc10e48212cf5f35d658d84813e1caf19feefd619437c62
export KING=0xaf25300b110c4a41e52dbdfa998a36de88b3c2cdf25757814ac2e7aa5571d3c3
其中,可以通过sui client object $KING
来查看国王的能力值是否为默认设定的 66。
我们用sui client call --package $PACKAGE_ID --module warrior --function create_warrior --args Nigdle 65 --gas-budget 100000000
来创建一个 warrior,姓名为 Nigdle,初始能力值设定为 65。
别忘记把它的对象地址也 export 一下export WARRIOR=0xb6c63d92e7042cc4ede8201ac515ce54aac62808b1e5a237830f093d8386481c
。
此时如果尝试晋升为骑士,会得到如下报错:

Error executing transaction: Failure {
error: "MoveAbort(MoveLocation { module: ModuleId { address: 4f81d54db52fee57cae7f6d22e0746f729c59b3ad4a8f513857cbd57417d18e8, name: Identifier(\"interact\") }, function: 1, instruction: 10, function_name: Some(\"rise\") }, 0) in command 0",
}
这是因为能力值不足,通过sui client call --package $PACKAGE_ID --module warrior --function rise --args $WARRIOR --gas-budget 100000000
提升至 66,至于是否提升成功,可以通过sui client object $WARRIOR
来查看。
这个时候再通过sui client call --package $PACKAGE_ID --module interact --function rise --args $WARRIOR $KING --gas-budget 100000000
尝试晋升为骑士,就成功了。
原来的 warrior 已经不复存在,sui client object $WARRIOR
也将报错说这个对象已经被删除。
复制下来新创建的骑士的对象地址,用类似的命令去查看里面存储的内容,发现它的名字是 Nigdle,能力值是 66。这是一个共享的不可变对象,谁都可见,同时谁都无法进行更改。
如果你有条件,可以通过 sui_getObject 进行实验并查看 Knight 在 web 上的显示是否如同预期的那样。
接下去,国王在神明(publisher)授意下提升自身能力:sui client call --package $PACKAGE_ID --module king --function rise --args $PUBLISHER $KING --gas-budget 100000000
这个时候,如果创建一个能力值为 66 的 warrior 就无法再晋升,而那一位名为 Nigdle 的骑士由于占尽先机,就与其形成了鲜明的对比,但 Nigdle 的上限也到此为止了。
注意: 本篇内容仅针对初学者,目的是将这三者尽可能串联起来,形成一个不那么枯燥又容易理解的例子,所以设计过程及最终呈现可能存在优化空间。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!