Move on Sui 中级演进

  • Noah
  • 更新于 2024-03-20 03:18
  • 阅读 990

PTB(ProgrammableTransactionBlock)在Sui网络上,可编程事务块(PTB)允许用户指定一系列操作(事务)作为单个事务发送到网络这些操作按顺序执行,并且是原子性的-如果其中任何一个操作失败,整个PTB都会失败,并且所有的更改都会自动回滚PTB可以调用

PTB (Programmable Transaction Block)

  • 在Sui网络上,可编程事务块(PTB)允许用户指定一系列操作(事务)作为单个事务发送到网络
  • 这些操作按顺序执行,并且是原子性的 - 如果其中任何一个操作失败,整个PTB都会失败,并且所有的更改都会自动回滚
  • PTB 可以调用 Move模块 中编写的任何 publicpublic entryentry (private) 函数
  • 后续会有更加详细的介绍

Programmable函数(Function)

  • public function

    • 公共函数可以被事务调用, 也可以被其他 Move 代码(模块)调用
        public fun fun_name(){}
  • private function

    • 私有函数只能在同一模块中被调用
        fun fun_name(){}
  • public(friend) function

    • public(friend) 函数只能被同一包(package)中的模块(module)调用
    • 要用到 public(friend)函数, 需要使用 use 引入
    • 在定义 public(friend)函数的 module 中, 需要显式声明 friend [address]::[module_name]
    module 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

      entry fun clip_ticket(ticket: Ticket) {
          let Ticket {
                id,
                expiration_time: _,
            } = ticket;
            object::delete(id);
      }

Struct abilities (结构体能力)

  • key

    • 在 Sui 中, key 能力表示一个结构体是一个 Object 类型, 并且要求结构体的第一个字段是 id: UID (链上的唯一地址), Sui 的字节码验证器会保证 UID 不会重复

      struct AdminCap has key {
          id: UID,
          num_frens: u64,
      }
  • copy

    • copy 能力允许一个结构体被“复制”,从而创建一个具有完全相同额字段值的结构体实例

      struct 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 能力允许一个结构体成为其他结构体的一部分

          public 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 能力允许在函数结束时隐式销毁结构体,而无需进行“析构”操作

      struct 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 中显示的传递

      struct 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: 不可变对象与共享对象几乎相同。任何用户都可以将它们作为其交易的一部分。然而,共享对象可以作为可变引用包含,因此可以被任何人修改。不可变对象在被“冻结”后永远不会改变

    struct 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 ,将会出错!!!
    • 不可变对象可以随时通过不可变引用(&) 包含

      public 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

        struct 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

        struct 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对象成本比操作 OwnedImmutable 对象要高。
  • Immutable对象: 是指一旦创建就不能被修改的对象。对于不需要改变的共享状态,优先使用Immutable对象,因为它们比Shared对象更高效。Immutable对象不需要处理修改冲突的问题,因此它们在性能和成本方面都比Shared对象更有优势
  • 最佳实践
    • 如果数据永远不会改变,合约的所有共享状态都应该是不可变的对象
    • 可更新的共享状态的共享对象 (效率最低)
    • owned object 用于其他一切事物

System Objects

One-time Witness Object (见证对象)

  • 当部署一个模块时,任何定义的init函数将被自动调用。这个init函数可以接收一个见证对象,这是一个特殊的系统对象,仅在模块第一次部署时创建一次

    module 0x123::my_module {
        struct MY_MODULE has drop {}
    
        fun init(witness: MY_MODULE) {
            // 使用见证对象执行操作。
        }
    }
  • 为了在init函数中接收见证对象,需要声明一个与模块名称相同(全大写,保留下划线)的结构体,并且这个结构体必须具有drop能力。然后,在定义init函数时,可以将该类型的见证对象作为第一个参数

  • 目前有两种用途

    • 声明发布者对象(Publisher Object):发布者对象是证明部署者已经部署了该对象的证明

      fun init(witness: MY_MODULE, ctx: &mut TxContext) {
          assert!(types::is_one_time_witness(&witness), ENotOneTimeWitness);
          let publisher_object = package::claim(witness, ctx);
          // 使用或存储发布者对象...
      }
      
    • 证明正在初始化流程中调用其他模块的函数:当需要作为项目初始化的一部分与多个不同模块进行一系列操作时,这通常很有用

      module 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, 下面是函数原型:

    struct 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 对象

    1. 获取Publisher对象引用:这个对象证明了调用者有权为特定模块(MyObject所在的模块)创建Display对象。Publisher对象是在模块部署时通过特定的初始化流程创建的
    2. 调用display::new函数:创建一个新的Display对象。这个过程需要Publisher对象的引用和 transaction 上下文(ctx
    3. 设置显示规则:通过调用display::add_multiple函数,你可以为MyObject的特定字段添加格式化规则。这个函数接受两个 vector,一个是要显示的字段列表,另一个是对应的格式化字符串
    4. 更新版本并广播:调用display::update_version函数来确认对Display对象所做的更改,并触发一个事件,这个事件被Sui网络节点捕捉到后,可以让这些节点识别到新的或更新的Display对象
  • 一旦Display对象被创建并设置好,每当通过节点API获取对象时,它的显示属性也会根据指定的格式计算出来,并与对象的其他字段一起返回

  • 代码:

    module 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,允许用户获取链上记录的当前时间。这个功能在智能合约编程中非常有用,尤其是在需要对事件进行时间戳记录或者基于时间生成伪随机数的场景中。

    1. 获取当前时间

      • Clock对象允许通过clock::timestamp_ms函数获取当前的时间戳。
      • 返回的时间戳以毫秒为单位,即1秒等于1000毫秒。
    2. 时间戳的常见用途包括但不限于:

      • 记录保持:用时间戳记录事件发生的具体时刻。例如,可以创建一个结构体TimeEvent,包含时间戳字段timestamp_ms,并在特定操作时发出事件,将时间戳记录下来。

        struct TimeEvent has copy, drop {
            timestamp_ms: u64,
        }
        
        public entry fun log_event(clock: &Clock) {
            event::emit(TimeEvent { timestamp_ms: clock::timestamp_ms(clock) });
        }
        
    3. 生成伪随机数:利用时间戳作为种子来生成伪随机数。这种方法虽然方便,但技术上容易受到验证者操纵,因为验证者可以在很小的误差范围内设置时间戳。

      entry fun flip_coin(clock: &Clock): u64 {
          let timestamp_ms = clock::timestamp_ms(clock);
          // 0是正面,1是反面
          timestamp_ms % 2
      }
      
    4. 安全事项

      • 当使用时间戳来生成伪随机数时,需要注意这种方法的安全性。在一些场景下,由于验证者对时间戳有一定的控制能力,可能会影响到伪随机数的公正性
    5. 使用Clock对象的条件

      • 要访问Clock对象,需要在合约函数中将其作为参数传入

TxContext

  • 提供了关于当前事务的上下文信息
  • 需要小心安全问题
  • 主要用途
    1. 创建新对象的ID
      • 使用object::new(ctx)来创建一个新的对象ID。这个ID是唯一的,确保了对象在全局的唯一性
    2. 获取发送者地址
      • 通过tx_context::sender(ctx)获取当前事务的发送者地址。这可以用来确定是谁发起了当前的事务
  • TxContext的其他功能
    • 事务哈希digest函数返回当前事务的哈希值,可以用于日志记录或跟踪
    • 当前纪元和时间戳epochepoch_timestamp_ms函数分别返回当前的纪元号和对应的时间戳,有助于实现基于时间的逻辑
    • 生成新对象地址fresh_object_address使用与object::new相同的底层函数来生成新对象的地址,这对于创建新对象时指定特定的地址可能很有用

Struct data access

  • 具有存储能力的对象可以转移到使用 transfer::public_transfer 定义的同一模块之外
  • 没有存储能力的对象只能在定义它的同一模块中使用 transfer::transfer 进行传输

Summary

  1. func: public / private / public entry/ private entry
  2. PTB
  3. Struct abilities: key, store, copy, drop
  4. object: object wrapping / 不可变对象 /系统对象 / 可转让性

    加入组织, 一起交流/学习!

  • 原创
  • 学分: 4
  • 分类: Sui
  • 标签:
点赞 1
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Noah
Noah
0x1d79...cb1b
江湖只有他的大名,没有他的介绍。