Skip to main content

可编程交易块

在Sui上,交易不仅仅是资产流动的基本记录。Sui上的交易由一系列命令组成,这些命令在输入上执行以定义交易的结果。这些命令组被称为可编程交易块(PTBs),它们定义了Sui上的所有用户交易。PTBs允许用户在单个交易中调用多个Move函数,管理其对象和代币,而无需发布新的Move包。PTBs专为自动化和交易构建器而设计,是生成交易的轻量而灵活的方式。然而,不支持更复杂的编程模式,如循环,在这些情况下,你必须发布新的Move包。

正如前面提到的,每个PTB由独立的交易命令(有时简称为交易或命令)组成。每个交易命令按顺序执行,你可以在任何后续交易命令中使用一个交易命令的结果。所有块中所有交易命令的效果,特别是对象修改或转移,在交易结束时以原子方式应用。如果一个交易命令失败,则整个块失败,不会应用来自命令的任何效果。

一个PTB可以在单个执行中执行多达1,024个独特的操作,而在传统区块链上的交易需要执行1,024次以实现相同的结果。该结构还促进了更便宜的气费。单独执行交易的成本总是高于将这些相同的交易集中在PTB中的成本。

本主题的其余部分介绍了交易命令的执行语义。它假设对Sui对象模型和Move语言有一定的了解。有关这些主题的更多信息,请参见以下文档:

交易类型

与执行语义相关的PTB的两个部分。其他交易信息,如交易发件人或气体限制,可能会被引用,但超出了范围。PTB的结构如下:

{
inputs: [Input],
commands: [Command],
}

深入了解两个主要组成部分:

  • inputs 值是一个参数向量,[Input]。这些参数可以是对象或纯值,你可以在命令中使用它们。这些对象要么由发送方拥有,要么是共享/不可变对象。纯值表示简单的Move值,如 u64String 值,你可以纯粹地通过它们的字节构建。由于历史原因,在Rust实现中,Input 被称为 CallArg
  • commands 值是一个命令向量,[Command]。可能的命令包括:
    • TransferObjects 将多个(一个或多个)对象发送到指定地址。
    • SplitCoins 从单个代币中拆分多个(一个或多个)代币。可以是任何 sui::coin::Coin<_> 对象。
    • MergeCoins 将多个(一个或多个)代币合并为一个代币。可以合并任何 sui::coin::Coin<_> 对象,只要它们都是相同类型的。
    • MakeMoveVec 创建一个Move值的向量(可能为空)。主要用于构造将用作 MoveCall 参数的Move值的向量。
    • MoveCall 调用已发布包中的 entrypublic Move函数。
    • Publish 创建一个新包并调用该包中每个模块的 init 函数。
    • Upgrade 升级现有包。升级由该包的 sui::package::UpgradeCap 限制。

输入和结果

输入和结果是你可以在交易命令中使用的两种类型的值。输入是提供给PTB的值,而结果是由PTB命令产生的值。输入可以是对象或简单的Move值,而结果可以是任意的Move值(包括对象)。

可以将输入和结果看作填充值数组。对于输入,有一个单一的数组,但对于结果,对于每个单独的交易命令都有一个数组,从而创建了一个结果值的二维数组。你可以通过借用(可变或不可变)、复制(如果类型允许)或移动(从数组中取出值而不重新索引)来访问这些值。

输入

PTB的输入参数可广泛分为对象或纯值两种。这些参数的直接实现通常由交易构建器或SDK所隐藏。本节描述了在指定输入列表 [Input] 时Sui网络需要的信息或数据。每个 Input 都是一个对象,Input::Object(ObjectArg),其中包含指定要使用的对象的必要元数据,或者是纯值,Input::Pure(PureArg),其中包含值的字节。

对于对象输入,所需的元数据取决于对象的所有权类型。ObjectArg 枚举的数据如下:

如果对象由地址拥有(或它是不可变的),则使用 ObjectArg::ImmOrOwnedObject(ObjectID, SequenceNumber, ObjectDigest)。三元组分别指定对象的ID、其序列号(也称为版本)和对象数据的摘要。

如果对象是共享的,则使用 Object::SharedObject { id: ObjectID, initial_shared_version: SequenceNumber, mutable: bool }。与 ImmOrOwnedObject 不同,共享对象的版本和摘要由网络的共识协议确定。initial_shared_version 是对象首次共享时的版本,当协议尚未看到带有该对象的事务时,它由共识使用。虽然所有共享对象都可以被变更,但 mutable 标志指示该对象在此事务中是否可变。在 mutable 标志设置为 false 的情况下,对象是只读的,系统可以并行安排其他只读事务。

如果对象由另一个对象拥有,例如通过 TransferObjects 命令或 sui::transfer::transfer 函数将其发送到对象的ID,则使用 ObjectArg::Receiving(ObjectID, SequenceNumber, ObjectDigest)。对象数据与 ImmOrOwnedObject 案例相同。

对于纯输入,提供的唯一数据是BCS字节,这些字节被反序列化以构造Move值。并非所有Move值都可以从BCS字节构造出来。这意味着即使字节与给定Move类型的预期布局匹配,它们也不能被反序列化为该类型的值,除非该类型是 Pure 值允许的类型之一。以下类型允许与纯值一起使用:

  • 所有原始类型:
    • u8
    • u16
    • u32
    • u64
    • u128
    • u256
    • bool
    • address
  • 一个字符串,可以是ASCII字符串 (std::ascii::String) 或UTF8字符串 (std::string::String)。在任何情况下,都会验证字节是否是具有相应编码的有效字符串。
  • 一个对象ID sui::object::ID
  • 一个向量,vector<T>,其中 T 是对纯输入有效类型的递归检查。
  • 一个选项,std::option::Option<T>,其中 T 是对纯输入有效类型的递归检查。

有趣的是,字节在类型在命令中指定之前不会被验证,例如在 MoveCallMakeMoveVec 中。这意味着给定的纯输入可以用于实例化多种类型的Move值。有关更多详细信息,请参见 Arguments section

结果

每个事务命令产生一个(可能为空的)值数组。值的类型可以是任意的 Move 类型,因此与输入不同,这些值不仅限于对象或纯值。生成的结果数量及其类型对每个事务命令都是具体的。可以在每个命令的部分找到每个命令的具体信息,但总体上:

  • MoveCall:结果的数量和类型由调用的 Move 函数确定。目前不支持返回引用的 Move 函数。
  • SplitCoins:从单个币中产生(一个或多个)币。每个币的类型是 sui::coin::Coin<T>,其中具体的币类型 T 与被分割的币相匹配。
  • Publish:返回新发布包的升级能力 sui::package::UpgradeCap
  • Upgrade:返回已升级包的升级收据 sui::package::UpgradeReceipt
  • TransferObjectsMergeCoins 不产生任何结果(一个空的结果向量)。

参数结构和使用

每个命令都接受指定输入或结果的 Argument。使用方式(按引用或按值)是根据参数的类型和命令的预期参数推断的。首先,查看 Argument 枚举的结构。

  • Input(u16) 是一个输入参数,其中 u16 是输入向量中输入的索引。例如,给定输入向量 [Object1, Object2, Pure1, Object3]Object1 通过 Input(0) 访问,Pure1 通过 Input(2) 访问。

  • GasCoin 是表示用于支付燃料的 SUI 币的特殊输入参数。它与其他输入保持分开,因为燃料币总是出现在每个事务中并且具有特殊的限制(参见下文),不同于其他输入。此外,燃料币的分离使其使用明确,对于赞助交易而言,赞助商可能不希望发送方将燃料币用于除燃料外的任何其他用途。

    燃料币不能以值方式(按值)取出,除非使用 TransferObjects 命令。如果需要燃料币的拥有版本,可以先使用 SplitCoins 将其拆分为单个币。

    这个限制存在是为了使剩余的燃料轻松地在执行结束时退还给该币。换句话说,如果燃料币被封装或删除,那么就没有明显的位置可以将多余的燃料退还。有关更多详细信息,请参见 Execution section

  • NestedResult(u16, u16) 使用先前命令的值。第一个 u16 是命令向量中命令的索引,第二个 u16 是该命令的结果向量中结果的索引。例如,给定一个命令向量 [MoveCall1, MoveCall2, TransferObjects],其中 MoveCall2 具有结果向量 [Value1, Value2],则可以使用 NestedResult(1, 0) 访问 Value1,并使用 NestedResult(1, 1) 访问 Value2

  • Result(u16)NestedResult 的一种特殊形式,其中 Result(i) 粗略相当于 NestedResult(i, 0)。但是,与 NestedResult(i, 0) 不同,Result(i) 如果结果数组在索引 i 处为空或有多个值,则引发错误。Result 的最终意图是允许访问整个结果数组,但目前尚不支持。因此,在其当前状态下,NestedResult 可以在所有情况下代替 Result 使用。

执行

对于 PTB 的执行,输入向量由输入对象或纯值字节填充。然后按顺序执行事务命令,并将结果存储在结果向量中。最后,事务的效果会被以原子方式应用。以下各节更详细地描述了执行的每个方面。

执行的开始

在执行开始时,PTB 运行时将已加载的输入对象加载到输入数组中。对象已由网络验证,检查规则如存在性和有效拥有权等。纯值字节也加载到数组中,但直到使用时才进行验证。

在这个阶段最重要的一点是对燃料币的影响。在执行开始时,从燃料币中提取了最大燃料预算(以 SUI 为单位)。无论硬币是否更改所有者,未使用的燃料都将在执行结束时退还到燃料币。

执行事务命令

然后按顺序执行每个事务命令。首先,查看与所有命令共享的参数规则。

参数

可以通过引用或按值使用每个参数。使用方式基于参数的类型和命令的类型签名。

  • 如果签名期望 &mut T,则运行时检查参数是否具有类型 T,然后对其进行可变借用。
  • 如果签名期望 &T,则运行时检查参数是否具有类型 T,然后对其进行不可变借用。
  • 如果签名期望 T,则运行时检查参数是否具有类型 T,如果 T: copy 则进行复制,否则进行移动。Sui 中没有 copy,因为所有对象都没有 copy 能力的唯一 ID 字段 sui::object::UID

如果参数在移动后以任何形式使用,则事务失败。没有办法在移动后将参数恢复到其位置(其输入或结果索引)。

如果参数被复制但没有 drop 能力,则推断最后一次使用是移动。因此,如果参数具有 copy 但没有 drop,则最后一次使用 必须 是按值。否则,事务将失败,因为没有 drop 的值没有被使用。

借用参数还有其他规则,以确保按引用唯一安全地使用参数。如果参数是:

  • 可变借用,不能有未完成的借用。与未完成的可变借用重复的可变借用可能导致悬空引用(指向无效内存的引用)。
  • 不可变借用,不能有未完成的可变借用。允许重复的不可变借用。
  • 移动,不能有未完成的借用。移动已借用的值将使这些未完成的借用悬空,使其不安全。
  • 复制,可以有未完成的借用,可变或不可变。虽然在某些情况下可能导致一些意外的结果,但没有安全问题。

对象输入的类型与其对象 T 的类型一样。但是,对于 ObjectArg::Receiving 输入,对象类型 T 被包装为 sui::transfer::Receiving<T>。这是因为该对象不是由发送方拥有,而是由另一个对象拥有。为了证明具有该父对象的所有权,你调用 sui::transfer::receive 函数以移除包装。

GasCoin 对以值方式使用(移动)有特殊限制。你只能使用 TransferObjects 命令按值使用它。

共享对象对以值方式使用也有限制。这些限制存在是为了确保在事务结束时共享对象仍然是共享的或已删除。共享对象不能取消共享(拥有者更改)并且不能被封装。共享对象:

  • 标记为非 mutable(以只读方式使用)不能以值方式使用。
  • 不能被转移或冻结。然而,这些检查仅在事务结束时进行,而不是动态进行。例如,如果传递了一个共享对象,TransferObjects 将成功,但在执行结束时事务失败。
  • 可以被封装,并且可以暂时成为动态字段,但在事务结束时必须重新共享或删除。

纯值在使用之前不进行类型检查。在检查纯值是否具有类型 T 时,将检查 T 是否是纯值的有效类型(请参见前面的列表)。如果是,则验证字节。你可以使用具有多种类型的纯值,只要字节对每种类型都有效。例如,可以将字符串用作 ASCII 字符串 std::ascii::String 和 UTF8 字符串 std::string::String。但是,在你可变地借用纯值之后,类型就会变得固定,所有将来的使用都必须具有该类型。

TransferObjects

该命令的形式为 TransferObjects(ObjectArgs, AddressArg),其中 ObjectArgs 是对象的向量,AddressArg 是对象发送到的地址。

  • 每个参数 ObjectArgs: [Argument] 必须是一个对象,但对象的类型可以不同。
  • 地址参数 AddressArg: Argument 必须是一个地址,可以来自 Pure 输入或结果。
  • 所有参数,对象和地址,都按值接受。
  • 该命令不生成任何结果(一个空的结果向量)。
  • 虽然此命令的签名无法在 Move 中表达,但你可以粗略地将其视为具有签名 (vector<forall T: key + store. T>, address): (),其中 forall T: key + store. T 表示 vector 是对象的异构向量。

SplitCoins

该命令的形式为 SplitCoins(CoinArg, AmountArgs),其中 CoinArg 是要分割的币,AmountArgs 是一个要分割的数量的向量。

  • 在交易签名时,网络验证 AmountArgs 不为空。
  • 币参数 CoinArg: Argument 必须是类型为 sui::coin::Coin<T> 的币,其中 T 是要分割的币的类型。它可以是任何币类型,不仅限于 SUI 币。
  • 金额参数 AmountArgs: [Argument] 必须是 u64 值,可以来自 Pure 输入或结果。
  • 币参数 CoinArg 按可变引用接受。
  • 金额参数 AmountArgs 按值接受(复制)。
  • 该命令的结果是一个币的向量,sui::coin::Coin<T>。币类型 T 与参数的数量相匹配。
  • 在 Move 中,其签名类似于一个函数 <T: key + store>(coin: &mut sui::coin::Coin<T>, amounts: vector<u64>): vector<sui::coin::Coin<T>>,其中结果 vector 保证与 amounts 向量具有相同的长度。

MergeCoins

该命令的形式为 MergeCoins(CoinArg, ToMergeArgs),其中 CoinArg 是要将 ToMergeArgs 币合并到其中的目标币。换句话说,你将多个币(ToMergeArgs)合并到单个币(CoinArg)中。

  • 在交易签名时,网络验证 AmountArgs 不为空。
  • 币参数 CoinArg: Argument 必须是类型为 sui::coin::Coin<T> 的币,其中 T 是要合并的币的类型。它可以是任何币类型,不仅限于 SUI 币。
  • 币参数 ToMergeArgs: [Argument] 必须是 sui::coin::Coin<T> 值,其中 TCoinArg 的类型相同。
  • 币参数 CoinArg 按可变引用接受。
  • 合并参数 ToMergeArgs 按值接受(移动)。
  • 该命令不生成任何结果(一个空的结果向量)。
  • 在 Move 中,其签名类似于一个函数 <T: key + store>(coin: &mut sui::coin::Coin<T>, to_merge: vector<sui::coin::Coin<T>>): ()

MakeMoveVec

该命令的形式为 MakeMoveVec(VecTypeOption, Args),其中 VecTypeOption 是一个可选参数,指定正在构建的向量中元素的类型,Args 是要用作向量元素的参数的向量。

  • 在交易签名时,网络验证如果为空则必须为 Args 的空向量指定类型。
  • 类型 VecTypeOption: Option<TypeTag> 是一个可选参数,指定正在构建的向量中元素的类型。TypeTag 是向量中元素的 Move 类型,即生成的 vector<T> 中的 T
    • 对于对象向量,如果 T: key,则不必指定类型。
    • 如果类型不是对象类型或向量为空,则 必须 指定类型。
  • 参数 Args: [Argument] 是向量的元素。参数可以是任何类型,包括对象、纯值或先前命令的结果。
  • 参数 Args 按值接受。如果 T: copy 则复制,否则移动。
  • 该命令生成一个类型为 vector<T>单一 结果。然后无法使用 NestedResult 分别访问向量的元素。相反,整个向量必须用作另一条命令的参数。如果希望分别访问元素,可以使用 MoveCall 命令并在 Move 代码中执行。
  • 虽然此命令的签名无法在 Move 中表达,但你可以粗略地将其视为具有签名 (T...): vector<T>,其中 T... 表示类型为 T 的参数的可变数量。

MoveCall

该命令的形式为 MoveCall(Package, Module, Function, TypeArgs, Args),其中 Package::Module::Function 组合在一起指定了正在调用的 Move 函数,TypeArgs 是要传递给该函数的类型参数的向量,Args 是 Move 函数的参数的向量。

  • Package: ObjectID 是包含正在调用的模块的对象 ID。
  • 模块 Module: String 是包含正在调用的函数的模块的名称。
  • 函数 Function: String 是正在调用的函数的名称。
  • 类型参数 TypeArgs: [TypeTag] 是要传递给正在调用的函数的类型参数。它们必须满足函数类型参数的约束。
  • 参数 Args: [Argument] 是正在调用的函数的参数。这些参数必须符合函数签名中指定的参数的规定。
  • 与其他命令不同,参数的使用和结果的数量是动态的,因为它们都取决于正在调用的 Move 函数的签名。

Publish

该命令的形式为 Publish(ModuleBytes, TransitiveDependencies),其中 ModuleBytes 是正在发布的模块的字节,TransitiveDependencies 是要链接到的包对象 ID 依赖项的向量。

在交易签名时,网络验证 ModuleBytes 不为空。模块字节 ModuleBytes: [[u8]] 包含每个 [u8] 元素是一个模块的模块字节。

传递依赖项 TransitiveDependencies: [ObjectID] 是新包依赖的包的对象 ID。虽然每个模块都指示用作依赖项的包,但必须提供传递的对象 ID 以选择这些包的版本。换句话说,这些对象 ID 用于选择模块中标记为依赖项的包的版本。

验证了包中的模块后,将按照模块字节向量 ModuleBytes 的相同顺序调用每个模块的 init 函数。

该命令生成一个类型为 sui::package::UpgradeCap单一 结果,该结果是新发布包的升级能力。

Upgrade

该命令的形式为 Upgrade(ModuleBytes, TransitiveDependencies, Package, UpgradeTicket),其中 Package 指示要升级的包的对象 ID。ModuleBytesTransitiveDependencies 的工作方式与 Publish 命令相似。

有关 ModuleBytesTransitiveDependencies 的详细信息,请参阅 Publish 命令。请注意,升级的模块不调用 init 函数。

Package: ObjectID 是要升级的包的对象 ID。该包必须存在并且是最新版本。

UpgradeTicket: sui::package::UpgradeTicket 是要升级的包的升级券,并且是从 sui::package::UpgradeCap 生成的。该券按值接受(移动)。

该命令生成一个类型为 sui::package::UpgradeReceipt单一 结果,该结果提供了该升级的证明。有关升级的更多详细信息,请参见升级包

执行结束

在执行结束时,将检查剩余的值并计算交易的效果。

对于输入,执行以下检查:

  • 跳过任何剩余的不可变或只读输入对象,因为对它们没有进行修改。
  • 将任何剩余的可变输入对象还给其原始所有者--如果它们是共享的,则保持共享,如果它们是拥有的,则保持拥有。
  • 放弃任何剩余的纯输入值。请注意,纯输入值必须具有 copydrop,因为对于这些值的所有允许的类型都具有 copydrop
  • 对于任何共享对象,还必须检查它是否仅已删除或重新共享。任何其他操作(包括封装、转移、冻结等)都会导致错误。

对于结果,执行以下检查:

  • 放弃具有 drop 能力的任何剩余结果。
  • 如果值具有 copy 但没有 drop,则其最后一次使用必须是按值使用。以这种方式,最后一次使用将被视为移动。
  • 否则,将出现错误,因为存在一个未使用的值没有 drop

在执行开始时,从 gas 币中扣除的任何剩余 SUI 都会返回到该币中,即使所有者已更改。换句话说,在执行开始时,会扣除最大可能的 gas,然后在执行结束时返回未使用的 gas(全部以 SUI 计)。由于只能使用 TransferObjects 按值获取 gas 币,因此它不会被封装或删除。

然后,总效果(其中包含创建、变异和删除的对象)传递出执行层,并由 Sui 网络应用。

示例

让我们通过一个 PTB 执行的示例来说明。尽管这个例子不能详尽地展示所有规则,但它确实显示了执行的一般流程。

假设你想要从市场购买两个价格为 100 MIST 的物品。你留下一个物品,然后将另一个物品和剩余的币发送到地址 0x808 的朋友。你可以在一个 PTB 中完成所有这些操作:

{
inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Object(SharedObject { /* Marketplace shared object */ id: market_id, ... }),
Pure(/* 100u64 BCS bytes */ ...),
]
commands: [
SplitCoins(GasCoin, [Input(2)]),
MoveCall("some_package", "some_marketplace", "buy_two", [], [Input(1), NestedResult(0, 0)]),
TransferObjects([GasCoin, NestedResult(1, 0)], Input(0)),
MoveCall("sui", "tx_context", "sender", [], []),
TransferObjects([NestedResult(1, 1)], NestedResult(3, 0)),
]
}

输入包括朋友的地址、市场物品和币值分割的数值。至于命令,首先要分割币,调用市场函数,发送 gas 币和一个对象,获取你的地址 (通过 sui::tx_context::sender),然后将剩余对象发送给自己。为简化起见,文档中使用包的名称引用它们,但请注意,在实践中它们是通过包的对象 ID 引用的。

为了进行演示,首先查看 gas 对象、输入和结果的内存位置。

Gas Coin: sui::coin::Coin<SUI> { id: gas_coin, balance: sui::balance::Balance<SUI> { value: 1_000_000u64 } }
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
some_package::some_marketplace::Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: []

到目前为止,已加载两个对象,gas 币的值为 1_000_000u64,市场对象的类型为 some_package::some_marketplace::Marketplace(为简便起见,这些名称和表示法在后文中被缩短)。纯参数尚未加载,并以 BCS 字节的形式存在。

请注意,虽然每个命令都会扣除 gas,但未详细展示执行的这一方面。

在命令之前:执行开始

在执行之前,从 gas 币中扣除 gas 预算。假设 gas 预算为 500_000,因此 gas 币现在的值为 500_000u64

Gas Coin: Coin<SUI> { id: gas_coin, ... value: 500_000u64 ... } // The maximum gas is deducted
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: []

现在你可以执行命令了。

Command 0: SplitCoins

第一个命令 SplitCoins(GasCoin, [Input(2)]) 通过可变引用访问 gas 币,并将 Input(2) 处的纯参数加载为 u64 类型的值 100u64。由于 u64 具有 copy 能力,不会移动 Input(2) 处的 Pure 输入。相反,将其字节复制出来。

对于结果,生成一个新的 coin 对象。

这给我们带来了更新后的内存位置:

Gas Coin: Coin<SUI> { id: gas_coin, ... value: 499_900u64 ... }
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[Coin<SUI> { id: new_coin, value: 100u64 ... }], // The result of SplitCoins
],

Command 1: MoveCall

现在执行命令 MoveCall("some_package", "some_marketplace", "buy_two", [], [Input(1), NestedResult(0, 0)])。调用函数 some_package::some_marketplace::buy_two,传递参数 Input(1)NestedResult(0, 0)。要确定它们是如何使用的,你需要查看函数的签名。对于此示例,假设签名为

entry fun buy_two(
marketplace: &mut Marketplace,
coin: Coin<Sui>,
ctx: &mut TxContext,
): (Item, Item)

其中 Item 是被出售的两个物品的类型。

由于 marketplace 参数的类型是 &mut Marketplace,因此通过可变引用使用 Input(1)。假设对 Marketplace 对象的值进行了一些修改。然而,coin 参数的类型是 Coin<Sui>,因此通过按值使用 NestedResult(0, 0)TxContext 输入由运行时自动提供。

这给出了更新后的内存位置,其中 _ 表示对象已被移动。

Gas Coin: Coin<SUI> { id: gas_coin, ... value: 499_900u64 ... }
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... }, // Any mutations are applied
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ], // The coin was moved
[Item { id: id1 }, Item { id: id2 }], // The results from the Move call
],

假设 buy_two 删除了其 Coin<SUI> 对象参数,并将 Balance<SUI> 转移到了 Marketplace 对象中。

Command 2: TransferObjects

TransferObjects([GasCoin, NestedResult(1, 0)], Input(0)) 将 gas coin 和第一个物品转移到 Input(0) 处的地址。所有输入都是按值传递的,而且对象没有 copy,因此它们被移动。虽然没有给出结果,但对象的所有权已经改变。这不能在内存位置中看到,而是通过交易效果来体现。

现在,你有了更新后的内存位置

Gas Coin: _ // The gas coin is moved
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ],
[ _ , Item { id: id2 }], // One item was moved
[], // No results from TransferObjects
],

Command 3: MoveCall

进行另一个 Move 调用,这次调用 sui::tx_context::sender,签名为

public fun sender(ctx: &TxContext): address

虽然你本可以将发送者的地址作为 Pure 输入传递,但这个例子演示了 PTB 的一些附加实用功能;尽管这个函数不是一个 entry 函数,你也可以调用 public 函数,因为你可以提供所有的参数。在这种情况下,唯一的参数 TxContext 是由运行时提供的。该函数的结果是发送者的地址。请注意,这个值不像 Pure 输入那样对待--它的类型被固定为 address,即使它有一个兼容的 BCS 表示,也不能反序列化为不同的类型。

现在,你的内存位置已经更新为

Gas Coin: _
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ],
[ _ , Item { id: id2 }],
[],
[/* senders address */ ...], // The result of the Move call
],

命令 4: TransferObjects

最后,将剩下的物品转移到自己手中。这与之前的 TransferObjects 命令类似。你正在通过值使用最后一个 Item 和发送者的地址。该物品被移动,因为 Item 没有 copy,而地址被复制,因为 address 具有 copy

最终的内存位置为

Gas Coin: _
Inputs: [
Pure(/* @0x808 BCS bytes */ ...),
Marketplace { id: market_id, ... },
Pure(/* 100u64 BCS bytes */ ...),
]
Results: [
[ _ ],
[ _ , _ ],
[],
[/* senders address */ ...],
[], // No results from TransferObjects
],

执行结束后:执行的最后

在执行结束时,运行时会检查剩余的值,这些值包括三个输入和发送者的地址。在给出效果之前,以下是执行前进行的总结检查:

  • 任何剩余的输入对象都标记为将被归还给它们的原始所有者。
    • Gas coin 已经被移动。而 Marketplace 保持相同的所有者,即共享。
  • 所有剩余的值都必须有 drop
    • Pure 输入具有 drop,因为它们可以实例化的任何类型都有 drop
    • 发送者的地址具有 drop,因为基本类型 address 具有 drop
    • 所有其他的结果都已经被移动。
  • 任何剩余的共享对象必须已经被删除或重新共享。
    • Marketplace 对象未被移动,因此所有者保持不变,仍然是共享的。

执行这些检查后,生成效果。

  • 从 Gas coin 拆分出来的 coin,new_coin,不会出现在效果中,因为它在同一交易中被创建并删除。
  • Gas coin 和具有 id1 的物品被转移到 0x808
    • Gas coin 被突变以更新其余额。即使所有者发生了变化,最大预算的剩余 gas(500_000)也会返回给 gas coin。
    • 具有 id1Item 是一个新创建的对象。
  • 具有 id2 的物品被转移到发送者的地址。
    • 具有 id2Item 是一个新创建的对象。
  • Marketplace 对象被返回,保持共享,并发生了突变。
    • 对象保持共享状态,但其内容发生了突变。