可编程交易块
在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值,如u64
或String
值,你可以纯粹地通过它们的字节构建。由于历史原因,在Rust实现中,Input
被称为CallArg
。commands
值是一个命令向量,[Command]
。可能的命令包括:TransferObjects
将多个(一个或多个)对象发送到指定地址。SplitCoins
从单个代币中拆分多个(一个或多个)代币。可以是任何sui::coin::Coin<_>
对象。MergeCoins
将多个(一个或多个)代币合并为一个代币。可以合并任何sui::coin::Coin<_>
对象,只要它们都是相同类型的。MakeMoveVec
创建一个Move值的向量(可能为空)。主要用于构造将用作MoveCall
参数的Move值的向量。MoveCall
调用已发布包中的entry
或public
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
是对纯输入有效类型的递归检查。
有趣的是,字节在类型在命令中指定之前不会被验证,例如在 MoveCall
或 MakeMoveVec
中。这意味着给定的纯输入可以用于实例化多种类型的Move值。有关更多详细信息,请参见 Arguments section。
结果
每个事务命令产生一个(可能为空的)值数组。值的类型可以是任意的 Move 类型,因此与输入不同,这些值不仅限于对象或纯值。生成的结果数量及其类型对每个事务命令都是具体的。可以在每个命令的部分找到每个命令的具体信息,但总体上:
MoveCall
:结果的数量和类型由调用的 Move 函数确定。目前不支持返回引用的 Move 函数。SplitCoins
:从单个币中产生(一个或多个)币。每个币的类型是sui::coin::Coin<T>
,其中具体的币类型T
与被分割的币相匹配。Publish
:返回新发布包的升级能力sui::package::UpgradeCap
。Upgrade
:返回已升级包的升级收据sui::package::UpgradeReceipt
。TransferObjects
和MergeCoins
不产生任何结果(一个空的结果向量)。
参数结构和使用
每个命令都接受指定输入或结果的 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>
值,其中T
与CoinArg
的类型相同。 - 币参数
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。ModuleBytes
和 TransitiveDependencies
的工作方式与 Publish
命令相似。
有关 ModuleBytes
和 TransitiveDependencies
的详细信息,请参阅 Publish
命令。请注意,升级的模块不调用 init
函数。
Package: ObjectID
是要升级的包的对象 ID。该包必须存在并且是最新版本。
UpgradeTicket: sui::package::UpgradeTicket
是要升级的包的升级券,并且是从 sui::package::UpgradeCap
生成的。该券按值接受(移动)。
该命令生成一个类型为 sui::package::UpgradeReceipt
的 单一 结果,该结果提供了该升级的证明。有关升级的更多详细信息,请参见升级包。
执行结束
在执行结束时,将检查剩余的值并计算交易的效果。
对于输入,执行以下检查:
- 跳过任何剩余的不可变或只读输入对象,因为对它们没有进行修改。
- 将任何剩余的可变输入对象还给其原始所有者--如果它们是共享的,则保持共享,如果它们是拥有的,则保持拥有。
- 放弃任何剩余的纯输入值。请注意,纯输入值必须具有
copy
和drop
,因为对于这些值的所有允许的类型都具有copy
和drop
。 - 对于任何共享对象,还必须检查它是否仅已删除或重新共享。任何其他操作(包括封装、转移、冻结等)都会导致错误。
对于结果,执行以下检查:
- 放弃具有
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
保持相同的所有者,即共享。
- Gas coin 已经被移动。而
- 所有剩余的值都必须有
drop
。- Pure 输入具有
drop
,因为它们可以实例化的任何类型都有drop
。 - 发送者的地址具有
drop
,因为基本类型address
具有drop
。 - 所有其他的结果都已经被移动。
- Pure 输入具有
- 任何剩余的共享对象必须已经被删除或重新共享。
Marketplace
对象未被移动,因此所有者保持不变,仍然是共享的。
执行这些检查后,生成效果。
- 从 Gas coin 拆分出来的 coin,
new_coin
,不会出现在效果中,因为它在同一交易中被创建并删除。 - Gas coin 和具有
id1
的物品被转移到0x808
。- Gas coin 被突变以更新其余额。即使所有者发生了变化,最大预算的剩余 gas(
500_000
)也会返回给 gas coin。 - 具有
id1
的Item
是一个新创建的对象。
- Gas coin 被突变以更新其余额。即使所有者发生了变化,最大预算的剩余 gas(
- 具有
id2
的物品被转移到发送者的地址。- 具有
id2
的Item
是一个新创建的对象。
- 具有
Marketplace
对象被返回,保持共享,并发生了突变。- 对象保持共享状态,但其内容发生了突变。