Sui.

帖子

分享您的知识。

article banner.
harry phan.
Apr 24, 2025
文章

使用 public_receive 进行跨模块儿童管理

这是 “Sui Move 中的亲子对象” 系列的第 3 部分.

有时,您的父类型和子类型是在不同的模块甚至不同的包中定义的. 例如,您可能有一个可以存储任何类型的 Parcel 对象的通用仓库对象. 仓库模块想要提取一个包裹子节点,但是包裹类型是在其他地方定义的. 在这种情况下,我们使用 transfer:: public_receive,它是 receive 的跨模块表兄弟.

###接收 vs public_receive

正如我们所见,transfer:: receive 只能在定义 T(或朋友)的模块中调用,因为它不需要 T: store. Move 字节码验证器实际上确保在任何要接收的调用中,T 类型来自当前模块. 这是对仅限钥匙的对象的安全限制.

transfer:: public_receive 是一个变体,它需要 T: key + store,但允许在 T 的模块之外接收. 换句话说,如果对象类型具有存储能力(这意味着它可以自由存在于全局存储中),则任何模块(给定父模块的 &mut UID)都可以使用 public_receive 接收它. 这非常适合父模块与子模块不同的情况.

为什么需要商店?因为存储标志着该对象可以安全地保存并在其定义模块之外传递. 仅限密钥的对象可能具有原始模块希望在传输/接收时强制执行的自定义不变量;通过排除public_receive中的那些不变量,Sui会强制开发人员在模块内处理它们(正如我们将在灵魂绑定对象中看到的那样). 如果一个对象有存储,则更宽松,而且 Sui 允许使用通用的传输/接收逻辑在外部对其进行管理.

###示例:分离父模块和子模块

让我们用一个简单的场景来说明一下:一个存储 Parcel 对象的仓库. 包裹类型在自己的模块中定义,仓库在另一个模块中定义. 我们将展示仓库如何使用 public_receive 接收子包裹.

module demo::parcel {  // Child module
    use sui::object::{Self, UID};
    use sui::tx_context::{Self, TxContext};

    /// A parcel object that can be stored in a Warehouse.
    /// It has both key and store, so it can be transferred across modules.
    struct Parcel has key, store { id: UID, contents: vector<u8> }

    public entry fun create_parcel(contents: vector<u8>, ctx: &mut TxContext): Parcel {
        Parcel { id: object::new(ctx), contents }
    }
}
module demo::warehouse {  // Parent module
    use sui::transfer::{Self, Receiving, public_receive};
    use demo::parcel::{Self, Parcel};
    use sui::object::{UID};
    use sui::tx_context::{Self, TxContext};

    struct Warehouse has key { id: UID, location: address }

    public entry fun create_warehouse(location: address, ctx: &mut TxContext): Warehouse {
        Warehouse { id: object::new(ctx), location }
    }

    /// Receive a Parcel that was sent to this Warehouse. 
    /// Returns the Parcel to the caller (transferred to caller's address).
    public entry fun withdraw_parcel(
        warehouse: &mut Warehouse, 
        parcel_ticket: Receiving<Parcel>, 
        ctx: &mut TxContext
    ): Parcel {
        // Using public_receive because Parcel is defined in another module and has store
        let parcel = public_receive<Parcel>(&mut warehouse.id, parcel_ticket) [oai_citation_attribution:27‡docs.sui.io](https://docs.sui.io/concepts/transfers/transfer-to-object#:~:text=%2F%2F%2F%20Given%20mutable%20,argument) [oai_citation_attribution:28‡github.com](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/packages/sui-framework/sources/transfer.move#:~:text=public%20fun%20public_receive,T%3E%29%3A%20T);
        // Transfer the parcel to the transaction sender (so the caller gets ownership)
        transfer::transfer(parcel, tx_context::sender(ctx));
        // We return nothing because we've transferred the Parcel out to the caller.
    }
}

让我们分解一下 redath_parcel 中发生了什么:

-我们打电话给 public_receive (&mut warehouse.id, parcel_ticket)由于 Parcel 具有存储能力,因此即使我们不���包裹模块中,也允许此调用. 在幕后,它会执行与接收相同的检查和提取,但允许跨模块进行检查,因为存储表明这样做是安全的. https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/packages/sui-framework/sources/transfer.move#:~:text=public%20fun%20public_receive,T%3E%29%3A%20T -然后,我们会立即将收到的包裹转到来电者的地址(tx_context:: sender(ctx)). 此步骤可确保包裹离开仓库并交给发起提款的用户. 我们也可以直接从函数中返回 Parcel,然后 Sui 会将其视为由调用者地址拥有的输出(因为它是输入函数的输出). 进行显式传输更为冗长,但可以清楚地说明发生了什么(并允许我们在释放对象之前进行任何检查).

**为什么要在包裹中加入店铺?**如果 Parcel 缺乏存储能力(即只有密钥),则 demo:: warehouse 中的 public_receive 调用将无法编译 — Sui 强制要求 T 为 public_receive 存储. 在这种情况下,我们将被迫在包裹模块本身中使用接收功能(或使用某种朋友关系)来取回包裹,这会使跨模块的设计变得复杂. 通过在 Parcel 中添加存储,我们实际上是在说 “这个对象可以自由移动和被外部模块接收”,这正是我们想要的通用容器模式.

**函数调用模式:**要在交易中使用它们,流程为:

  1. **存款(转移到对象):**致电转移:: public_transfer(parcel_obj,@warehouse_id)将包裹发送到仓库. 这会将包裹的所有者标记为仓库. (我们在这里使用 public_transfer 是因为它在 Parcel 模块之外,而且 Parcel 有存储. 在包裹模块内,普通的传输也可以. )
  2. **提款(收回):**稍后,致电撤回包裹(warehouse_obj,接收(包裹编号,...)). SDK 可以通过引用包裹的 ID 和最新版本来获取接收信息. 该函数将调用 public_receive 然后将包裹转移给你.

在拨打 redth_parcel 电话后,包裹的所有者回到了一个地址(你的),所以它又是一个普通的地址拥有的物品. 仓库不再拥有它.

**跨模块注意事项:**请注意,仓库模块需要了解包裹类型(我们使用 demo:: parcel:: Parcel). 这是因为我们将接收明确键入为接收. 如果你想要一个可以接收任何类型对象的真正通用容器,你必须使用泛型或其他方法(可能是带有类型擦除的动态字段). 但是对于大多数用例,你会知道你期望的是什么类型的孩子.

**为什么 public_receive 而不只是调用 receive?**如果我们尝试在仓库模块中转移:: 接收(&mut warehouse.id,parcel_ticket),则移动验证器会拒绝,因为在 demo:: warehouse 中没有定义包裹. Sui 提供了 public_receive 作为通过额外能力检查(需要存储)来实现此目的的好方法. 同样,Sui 也有传输与 public_transfer、freeze_object 与 public_freeze_object 等,遵循相同的模式:public_ 版本在定义模块之外使用,需要存储.

**别忘了家长的许可:**即使有 public_receive,你仍然需要那个 &mut warehouse.id. 我们之所以得到它,是因为撤回包裹在 Warehouse 的模块中并接受 &mut Warehouse. 因此,只有可以这样称呼的人(仓库的所有者)才能提取包裹. 如果仓库模块没有公开提供这样的函数,那么也没有人可以在外部对其子模块调用 public_receive. 因此,跨模块不会绕过父级的控制;它只允许父级的代码处理其未定义类型的子类型.

关于存储能力的说明:提供对象存储可以使其更加灵活,但限制稍微减少一点——任何具有父引用的模块都可以使用 public_receive 将其拉出. 如果你想限制对象的检索方式(例如,强制执行自定义逻辑或防止轻松提取),你可以故意将其设为仅限密钥. 我们将用灵魂束缚的物体来看这个例子. 在这种情况下,你可以实现自定义接收函数,而不是依赖 public_receive.

总结一下这部分:public_receive 是你管理其他模块中定义的子对象的好友,只要这些对象具有存储能力即可. 它允许您构建跨模块系统(例如我们的仓库/包裹),同时仍然尊重所有权和访问控制. 只要记得在子类型上添加存储,并在从模块外部将它们发送给父类型时使用 public_transfer 即可.

  • Sui
  • Architecture
5
分享
评论
.

Sui is a Layer 1 protocol blockchain designed as the first internet-scale programmable blockchain platform.

259帖子368答案
Sui.X.Peera.

赚取你的 1000 Sui 份额

获取声誉积分,并因帮助 Sui 社区成长而获得奖励。

奖励活动五月
      我们使用 cookie 确保您在我们的网站上获得最佳体验。
      更多信息