PTB (Programmable Transaction Block)
- 在Sui网络上,可编程事务块(PTB)允许用户指定一系列操作(事务)作为单个事务发送到网络
- 这些操作按顺序执行,并且是原子性的 - 如果其中任何一个操作失败,整个PTB都会失败,并且所有的更改都会自动回滚
- PTB 可以调用 Move模块 中编写的任何
public
、public entry
和entry
(private) 函数 - 后续会有更加详细的介绍
Programmable函数(Function)
-
public
function- 公共函数可以被事务调用, 也可以被其他 Move 代码(模块)调用
kotlinpublic fun fun_name(){}
-
private
function- 私有函数只能在同一模块中被调用
kotlinfun fun_name(){}
-
public(friend)
functionpublic(friend)
函数只能被同一包(package)中的模块(module)调用- 要用到
public(friend)
函数, 需要使用 use 引入 - 在定义
public(friend)
函数的 module 中, 需要显式声明friend [address]::[module_name]
kotlinmodule 0x123::my_other_module { use 0x123::my_module; public fun do_it(x: u64): bool { my_module::friend_only_equal(x) } } module 0x123::my_module { friend 0x123::my_other_module; public(friend) fun friend_only_equal(x: u64): bool { x == 1000 } }
-
Entry
function-
entry 主要是为了控制和管理交易的入口点
-
public entry
function: 与public
function 实际没有区别 -
entry
function(private): 可以直接从事务中调用而不能在其他 module 中调用(自身所在的module 还是可以调用的, 但是不推荐) -
我们希望用户必须明确地在事务中调用此功能,而不希望其他模块代表用户 clip_ticket
kotlinentry fun clip_ticket(ticket: Ticket) { let Ticket { id, expiration_time: _, } = ticket; object::delete(id); }
-
Struct abilities (结构体能力)
-
key
-
在 Sui 中,
key
能力表示一个结构体是一个Object 类型
, 并且要求结构体的第一个字段是id: UID
(链上的唯一地址), Sui 的字节码验证器会保证 UID 不会重复objectivecstruct AdminCap has key { id: UID, num_frens: u64, }
-
-
copy
-
copy
能力允许一个结构体被"复制",从而创建一个具有完全相同额字段值的结构体实例kotlinstruct CopyableStruct has copy { value: u64, } fun copy(original: CopyableStruct) { let copy = original; original.value = 1; copy.value = 2; // We now have two CopyableStructs with two different values. }
-
-
store
-
store
能力允许一个结构体成为其他结构体的一部分swiftpublic struct NestedStruct has store { value: u64, doubleNested: DoubleNestedStruct, } public struct DoubleNestedStruct has store { value: u64, } public struct Container has key { id: UID, nested: NestedStruct, }
-
-
drop
-
drop
能力允许在函数结束时隐式销毁结构体,而无需进行"析构"操作kotlinstruct DroppableStruct has drop { value: u64, } fun copy() { let droppable = DroppableStruct { value: 1 }; // At the end of this function, droppable would be destroyed. // We don't need to explicitly destruct: // let DroppableStruct { value: _ } = droppable; }
-
-
Note: 只有当结构体的所有字段具有相同的能力时, 结构体才具体相关的能力
Object wrapping (对象封装)
-
当我们常见一个对象, 对象内部又包含一个对象, 那么我们在新建实例的时候, 如果初始化呢?
-
例如在
Box
对象中包含了一个Thing
对象:-
方法 1: 在当前 module 中创建一个新的函数 - create,它创建一个
Thing
对象并将其返回,而不像mint函数那样立即将其传递给发送者 -
方法 2: 首先 mint
Thing
对象, mint 之后在当前 Transaction 无法检索到 mint 的Thing
对象, 需要在后续的 Transaction 中显示的传递kotlinstruct Box has key { id: UID, thing: Thing, } struct Thing has key, store { id: UID, } public fun wrap(thing: Thing, ctx: &mut TxContext) { let box = Box { id: object::new(ctx), thing }; transfer::transfer(box, tx_context::sender(ctx)); }
-
不可变对象(Immutable object)
-
上次提到的
Object
都是owned object
: 是私有对象,只有拥有它们的用户才能读取和修改(所有权) -
之前还提到过共享对象
shared object
: 可以被任何用户读取和修改 -
不可变对象
Immutable object
: 不可变对象与共享对象几乎相同。任何用户都可以将它们作为其交易的一部分。然而,共享对象可以作为可变引用包含,因此可以被任何人修改。不可变对象在被"冻结"后永远不会改变kotlinstruct ColorObject has key { id: UID, red: u8, green: u8, blue: u8, } public entry fun freeze_owned_object(object: ColorObject) { transfer::freeze_object(object) } public entry fun create_immutable(red: u8, green: u8, blue: u8, ctx: &mut TxContext) { let color_object = ColorObject { id: object::new(ctx), red, green, blue, }; transfer::freeze_object(color_object); }
-
create_immutable
创建一个对象并立即将其冻结,使其成为不可变的 -
transfer
的其他 freeze 函数:freeze_owned_object
: 提供一个现有的owned object
并使其成为不可变对象
-
Note: 如果在
shared object
上调用transfer::freeze_object
,将会出错!!! -
不可变对象可以随时通过不可变引用(&) 包含
kotlinpublic fun read_immutable(color: &ColorObject): (u8, u8, u8) { (color.red, color.green, color.blue) }
-
Transfer Policy
-
Public transfer:
transfer::public_transfer
-
具有
store
能力的Object
可以通过transfer::public_transfer
在定义这个 Object 的module 之外进行 transfer- 复习: 既然是一个 Object, 那一定有 key
-
-
Private transfer:
transfer::transfer
- 没有
store
能力的对象只能在定义它的模块内部进行传输
- 没有
Wrapping object 与 Non-Object struct
-
Wrapping object
- 使用对象封装可以创建复杂的对象层次结构,其中每个对象都是独立的,并具有其唯一的标识符
- 这种方式很强大, 但不总是必要
-
Non-Object struct
-
只具有 store 能力 的
struct
就是Non-Object struct
-
这种方法通常在不打算将嵌套的结构类型转换为对象时才有用。这可以将一个长的对象结构分解为更小的组
-
long object
yamlstruct LongObject has key { id: UID, field_1: u64, field_2: u64, field_3: u64, field_4: u64, field_5: u64, field_6: u64, field_7: u64, field_8: u64, }
-
big object
yamlstruct BigObject has key { id: UID, field_group_1: FieldGroup1, field_group_2: FieldGroup2, field_group_3: FieldGroup3, } struct FieldGroup1 has store { field_1: u64, field_2: u64, field_3: u64, } struct FieldGroup2 has store { field_4: u64, field_5: u64, field_6: u64, } struct FieldGroup3 has store { field_7: u64, field_8: u64, }
-
-
Objects & 并行执行 & 共识
-
Owned对象: 只有一个所有者可以控制和操作的对象。这种类型的对象对于大多数应用场景都是首选,因为它们易于管理,且与特定用户或合约直接关联。
-
Shared对象: 用于表示多个用户之间共享的状态。这类对象可以被多个方同时访问和修改,因此在需要跨多用户共享数据时使用。然而,由于Shared对象的修改需要全网共识来处理潜在的冲突,这使得操作
Shared对象
的成本 比操作Owned
或Immutable
对象要高。 -
Immutable对象: 是指一旦创建就不能被修改的对象。对于不需要改变的共享状态,优先使用Immutable对象,因为它们比Shared对象更高效。Immutable对象不需要处理修改冲突的问题,因此它们在性能和成本方面都比Shared对象更有优势
-
最佳实践
- 如果数据永远不会改变,合约的所有共享状态都应该是
不可变的对象
- 可更新的共享状态的
共享对象
(效率最低) owned object
用于其他一切事物
- 如果数据永远不会改变,合约的所有共享状态都应该是
System Objects
One-time Witness Object (见证对象)
-
当部署一个模块时,任何定义的**
init
函数将被自动调用。这个init
**函数可以接收一个见证对象,这是一个特殊的系统对象,仅在模块第一次部署时创建一次kotlinmodule 0x123::my_module { struct MY_MODULE has drop {} fun init(witness: MY_MODULE) { // 使用见证对象执行操作。 } }
-
为了在**
init
函数中接收见证对象,需要声明一个与模块名称相同(全大写,保留下划线)的结构体,并且这个结构体必须具有drop
能力。然后,在定义init
**函数时,可以将该类型的见证对象作为第一个参数 -
目前有两种用途
-
声明发布者对象(Publisher Object) :发布者对象是证明部署者已经部署了该对象的证明
kotlinfun init(witness: MY_MODULE, ctx: &mut TxContext) { assert!(types::is_one_time_witness(&witness), ENotOneTimeWitness); let publisher_object = package::claim(witness, ctx); // 使用或存储发布者对象... }
-
证明正在初始化流程中调用其他模块的函数:当需要作为项目初始化的一部分与多个不同模块进行一系列操作时,这通常很有用
kotlinmodule 0x123::module_b { // 在 module_b 的 init fun 中传入 A 的 witness, 作用是保证初始化顺序可控 fun init(module_a_witness: MODULE_A, ctx: &mut TxContext) { assert!(types::is_one_time_witness(&module_a_witness), ENotOneTimeWitness); } }
-
Publisher Object (发布者对象)
-
用途
- 创建显示对象(Display Objects) : 下面会提及
- 在Sui的Kiosk(NFT标准)中设置转移策略:这将在NFT中介绍
Display Object
-
Display<T>
: 指示如何为特定类型的 object 显示字段的 object, 下面是函数原型:ruststruct Display<phantom T: key> has key, store { id: UID, /// Contains fields for display. Currently supported /// fields are: name, link, image and description. fields: VecMap<String, String>, /// Version that can only be updated manually by the Publisher. version: u16 }
-
意味着开发者可以为存储在区块链上的对象定义如何在用户界面(比如
web UI
)中显示它们的字段 -
如果创建 display 对象
- 获取Publisher对象引用 :这个对象证明了调用者有权为特定模块(**
MyObject
**所在的模块)创建Display对象。Publisher对象是在模块部署时通过特定的初始化流程创建的 - 调用
display::new
函数 :创建一个新的Display对象。这个过程需要**Publisher
对象的引用和 transaction 上下文(ctx
**) - 设置显示规则 :通过调用**
display::add_multiple
函数,你可以为MyObject
**的特定字段添加格式化规则。这个函数接受两个 vector,一个是要显示的字段列表,另一个是对应的格式化字符串 - 更新版本并广播 :调用**
display::update_version
**函数来确认对Display对象所做的更改,并触发一个事件,这个事件被Sui网络节点捕捉到后,可以让这些节点识别到新的或更新的Display对象
- 获取Publisher对象引用 :这个对象证明了调用者有权为特定模块(**
-
一旦Display对象被创建并设置好,每当通过节点API获取对象时,它的显示属性也会根据指定的格式计算出来,并与对象的其他字段一起返回
-
代码:
cssmodule 0x123::my_module { struct MyObject has key { id: UID, num_value: u64, string_value: String, } public fun create_display_object(publisher: &Publisher, ctx: &mut TxContext) { let display_object = display::new<MyObject>(&publisher, ctx); display::add_multiple( &mut display, vector[ utf8(b"num_value"), utf8(b"string_value"), ], vector[ utf8(b"Value: {num_value}"), utf8(b"Description: {string_value}"), ], ); display::update_version(&mut display); } }
Clock
-
允许用户获取链上记录的当前时
-
Sui区块链提供了一个特殊的系统对象,名为
Clock
,允许用户获取链上记录的当前时间。这个功能在智能合约编程中非常有用,尤其是在需要对事件进行时间戳记录或者基于时间生成伪随机数的场景中。-
获取当前时间:
Clock
对象允许通过clock::timestamp_ms
函数获取当前的时间戳。- 返回的时间戳以毫秒为单位,即1秒等于1000毫秒。
-
时间戳的常见用途包括但不限于:
-
记录保持 :用时间戳记录事件发生的具体时刻。例如,可以创建一个结构体
TimeEvent
,包含时间戳字段timestamp_ms
,并在特定操作时发出事件,将时间戳记录下来。kotlinrustCopy code struct TimeEvent has copy, drop { timestamp_ms: u64, } public entry fun log_event(clock: &Clock) { event::emit(TimeEvent { timestamp_ms: clock::timestamp_ms(clock) }); }
-
-
生成伪随机数:利用时间戳作为种子来生成伪随机数。这种方法虽然方便,但技术上容易受到验证者操纵,因为验证者可以在很小的误差范围内设置时间戳。
kotlinrustCopy code entry fun flip_coin(clock: &Clock): u64 { let timestamp_ms = clock::timestamp_ms(clock); // 0是正面,1是反面 timestamp_ms % 2 }
-
安全事项:
- 当使用时间戳来生成伪随机数时,需要注意这种方法的安全性。在一些场景下,由于验证者对时间戳有一定的控制能力,可能会影响到伪随机数的公正性
-
使用
Clock
对象的条件:- 要访问**
Clock
**对象,需要在合约函数中将其作为参数传入
- 要访问**
-
TxContext
-
提供了关于当前事务的上下文信息
-
需要小心安全问题
-
主要用途
-
创建新对象的ID:
- 使用
object::new(ctx)
来创建一个新的对象ID。这个ID是唯一的,确保了对象在全局的唯一性
- 使用
-
获取发送者地址:
- 通过
tx_context::sender(ctx)
获取当前事务的发送者地址。这可以用来确定是谁发起了当前的事务
- 通过
-
-
TxContext
的其他功能- 事务哈希 :
digest
函数返回当前事务的哈希值,可以用于日志记录或跟踪 - 当前纪元和时间戳 :
epoch
和epoch_timestamp_ms
函数分别返回当前的纪元号和对应的时间戳,有助于实现基于时间的逻辑 - 生成新对象地址 :
fresh_object_address
使用与object::new
相同的底层函数来生成新对象的地址,这对于创建新对象时指定特定的地址可能很有用
- 事务哈希 :
Struct data access
- 具有存储能力的对象可以转移到使用
transfer::public_transfer
定义的同一模块之外 - 没有存储能力的对象只能在定义它的同一模块中使用
transfer::transfer
进行传输
Summary
- func:
public / private / public entry/ private entry
- PTB
- Struct abilities:
key, store, copy, drop
- object: object wrapping / 不可变对象 /系统对象 / 可转让性