修改示例代码
实验分析
在上一节课结尾的实验结果如下:
操作 | 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 功能的示例代码。
以下是一些修改的关键要点。
- 在 module 内引入 event 模块。
use sui::event::emit;
- 定义 Event 数据结构。
public struct CountEvent has copy, drop {
id: ID,
times: u64,
}
这是定义了copy
, drop
能力的数据结构。
- 在原有函数内添加 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 记录。
作业二
在 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;
}