跳到主要内容

修改示例代码

实验分析

在上一节课结尾的实验结果如下:

操作arg0 = 自己地址arg0 = 0x0
counter完成计数加一报错
操作先count再delete先delete再count
结果依次完成执行count时报错,显示Counter不存在
分析delete 之后 Object 已经不存在,无法再被调用

Object 所有权

Sui 是以 Object 为中心的数据模型,Object 根据所有权可以分为四种类型。

  • Account Owner 被单一账户地址所有,只有所有者才可以调用
  • Shared State 被共享所有,任何地址都可以调用
  • Immutable (Frozen) State 不可变类型,可以用于记录不会再发生改变的配置参数,发布后的智能合约package也属于这种类型
  • Object Owner 被其他Object所有类型,用于构造更复杂的数据结构,后续用到了再学

Account Owner 被账户地址所有

使用命令行查看mint出来的Object的属性

sui client object 0xb0e24862cf183e276cb1c1a9c92d718a67ee759aaba00d62638d22646820cc7b

可以看到,有很明显的owner属性,标明了所有权的账户地址,在被调用时,会跟发起请求的账户地址进行比对,只有一致时才会继续调用函数,否则会报错。

╭───────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ objectId │ 0xb0e24862cf183e276cb1c1a9c92d718a67ee759aaba00d62638d22646820cc7b │
│ version │ 83977972 │
│ digest │ 91cHe3zzchUmxiKA1b33GzCCTaU48rcMySfk6bpY6AwK │
│ objType │ 0xf97e49265ee7c5983ba9b10e23747b948f0b51161ebb81c5c4e76fd2aa31db0f::counter::Counter │
│ owner │ ╭──────────────┬──────────────────────────────────────────────────────────────────────╮ │
│ │ │ AddressOwner │ 0x8b8c71fb95ec259a279eb8e61d52d00eb103fcd524b8fe7ff4c405c484c8a25b │ │
│ │ ╰──────────────┴──────────────────────────────────────────────────────────────────────╯ │
│ prevTx │ HZVhnXWWntcycxPfsK2Sv1ZHrdou3vnbQLgWd6X7u164 │
│ storageRebate │ 1360400 │
│ content │ ╭───────────────────┬───────────────────────────────────────────────────────────────────────────────────────────╮ │
│ │ │ dataType │ moveObject │ │
│ │ │ type │ 0xf97e49265ee7c5983ba9b10e23747b948f0b51161ebb81c5c4e76fd2aa31db0f::counter::Counter │ │
│ │ │ hasPublicTransfer │ true │ │
│ │ │ fields │ ╭───────┬───────────────────────────────────────────────────────────────────────────────╮ │ │
│ │ │ │ │ id │ ╭────┬──────────────────────────────────────────────────────────────────────╮ │ │ │
│ │ │ │ │ │ │ id │ 0xb0e24862cf183e276cb1c1a9c92d718a67ee759aaba00d62638d22646820cc7b │ │ │ │
│ │ │ │ │ │ ╰────┴──────────────────────────────────────────────────────────────────────╯ │ │ │
│ │ │ │ │ times │ 0 │ │ │
│ │ │ │ ╰───────┴───────────────────────────────────────────────────────────────────────────────╯ │ │
│ │ ╰───────────────────┴───────────────────────────────────────────────────────────────────────────────────────────╯ │
╰───────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

Shared State 共享所有权

在业务中也有很多需要被任何人都可以调用的数据,比如 Market 需要允许不同用户寄存资产进行交易。 如果我们把上一小节代码中的 Counter 变为一个任何人都可以调用的计数器,只需要将 mint函数改为:

public fun mint(ctx: &mut TxContext) {
let counter = new(ctx);
transfer::public_share_object(counter);
}

public_share_object 函数是将生成的 counter Object 变为共享所有权的状态,这样生成的计数器 Counter可以被任何账户地址调用。

Fast Path & Consensus 执行效率

不同的Object所有权会对程序的执行效率有影响。 如果一个Object数据正在被多个人读写,就需要排序、锁定。Shared State 被共享所有的数据需要做这样的处理。 如果Object数据只属于个人账户,就只会被个人操作,不需要做排序和锁定,可以更快确认状态,也叫 Fast Path. 至于被其他Object所有的Object, 按照最上级Object的类型去处理。 Sui Move 智能合约在执行时,会根据函数输入参数的类型,先做分类,选择不同的执行效率。在设计合约时,可以结合需求选择更优方案。

作业一

改写counter项目代码,生成共享所有权的Counter, 然后换用不同账户地址去调用计数。

Event

就像服务器会输出记录执行日志一样,Sui 区块链也会把智能合约执行结果输出成 Event, 方便检索。不过需要在合约里自己定义。

是一份在原有的 counter 项目中添加 Event 功能的示例代码

以下是一些修改的关键要点。

  1. 在 module 内引入 event 模块。
use sui::event::emit;
  1. 定义 Event 数据结构。
public struct CountEvent has copy, drop {
id: ID,
times: u64,
}

这是定义了copy, drop能力的数据结构。

  1. 在原有函数内添加 emit event 功能。
public fun count(counter: &mut Counter) {
counter.times = counter.times + 1;

emit(
CountEvent {
id: object::id(counter),
times: counter.times,
}
);
}

添加了这些 Event 功能后,再执行,就可以在 explorer 上看到 Event 记录。

event

作业二

counter_event 示例代码中,为 mint, burn 也分别添加 Event 功能。

Ability 能力

截至目前,我们已经遇到了 Sui Move 上所有的 Struct 能力,一共 4 种。

  • key
  • store
  • copy
  • drop

key

就像传统的 K-V 数据库一样,需要有 key 才能在区块链数据库中存储和被检索。拥有 key 能力的 Object 可以在最顶级被存储,也可以被某个地址或账户持有。

这里的 key 其实是 UID, 在创建 Object 时生成的全局唯一的 0x.. 开头地址。 定义含有key能力的 Object 时,第一个属性必须是 id: UID.

use std::string::String;

public struct Object has key {
id: UID, // required
name: String,
}

/// Creates a new Object with a Unique ID
public fun new(name: String, ctx: &mut TxContext): Object {
Object {
id: object::new(ctx), // creates a new UID
name,
}
}

UID又是全局唯一,不可以被复制 copy 和丢弃 drop 的。所以,具有 key 能力的 Object 不能再具有 copy 或者 drop 能力。

参考资料

store

store 能力支持 Object 作为子 Object 被存储在其他 Object 中。

/// This type has the `store` ability.
public struct Storable has store {}

/// Config contains a `Storable` field which must have the `store` ability.
public struct Config has key, store {
id: UID,
stores: Storable,
}

/// MegaConfig contains a `Config` field which has the `store` ability.
public struct MegaConfig has key {
id: UID,
config: Config, // there it is!
}

参考资料中给出了更多具有store能力的基础数据结构。

copy

让 Struct 具有可以被复制的能力,通常会跟 drop 一起使用。

public struct Value has copy, drop {}

参考资料中给出了更多具有copy能力的基础数据结构。

drop

支持让数据在作用域结束后自动被丢弃销毁,回收存储资源。 作用域通俗点讲,就是包含该数据的最里层的 { ... } 括号对的范围。

module book::drop_ability {

/// This struct has the `drop` ability.
public struct IgnoreMe has drop {
a: u8,
b: u8,
}

/// This struct does not have the `drop` ability.
public struct NoDrop {}

#[test]
// Create an instance of the `IgnoreMe` struct and ignore it.
// Even though we constructed the instance, we don't need to unpack it.
fun test_ignore() {
let no_drop = NoDrop {};
let _ = IgnoreMe { a: 1, b: 2 }; // no need to unpack

// The value must be unpacked for the code to compile.
let NoDrop {} = no_drop; // OK
}
}

参考资料中给出了更多具有drop能力的基础数据结构。

作业三

定义一个Profile NFT,可以mint然后发送给任意地址。 Profile 的数据结构包含 用户名 handle 和积分 points

public struct Profile has key {
id: UID,
handle: String,
points: u64,
}

持有者每次调用 click 函数,可以增加积分。String的输入实现,参考key部分的示例代码

时间

很多应用也会需要在链上获得时间信息,Sui 上获得时间的方式有两种

  • epoch timestamp 记录当前epoch的开启时间,不够精确,可以从tx_context获得,每个epoch差不多24小时;
  • clock 可以获得更精确的时间,需要额外引入 sui::clock 模块。

单位都是毫秒ms.

epoch timestamp

示例代码中,修改了click函数,使得每个epoch只能执行一次click函数。assert!是做了断言约束,如果不满足条件,程序无法执行,全部状态返回。

public fun click(profile: &mut Profile, ctx: &TxContext) {
let this_epoch_time = ctx.epoch_timestamp_ms();
assert!(this_epoch_time > profile.last_time);
profile.last_time = this_epoch_time;
profile.points = profile.points + 1;
}

clock

示例代码中,限定了每次执行click函数的时间要大于 1 小时。

引入clock模块。

use sui::clock::Clock;

定义1小时的时间常量。

const ONE_HOUR_IN_MS: u64 = 60 * 60 * 1000;

更新后的click函数。

public fun click(profile: &mut Profile, clock: &Clock) {
let now = clock.timestamp_ms();
assert!(now > profile.last_time + ONE_HOUR_IN_MS);
profile.last_time = now;
profile.points = profile.points + 1;
}