Skip to main content

转账至对象

你可以将对象转账至对象 ID,就像将对象转账至地址一样,使用相同的函数。这是因为 Sui 不区分地址的 32 字节 ID 和对象的 32 字节 ID(它们被保证不会重叠)。转账至对象操作利用了这一特性,允许你将对象 ID 作为转账操作的地址输入。

由于 ID 结构相同,你可以在转账对象时将对象 ID 用作地址字段。实际上,关于地址拥有的对象的所有功能对于被其他对象拥有的对象都是相同的,只需将地址替换为对象 ID。

当你将对象转账至另一个对象时,基本上是建立了一种父子身份验证关系。你转账给另一个对象的对象可以被(可能是传递性的)父对象的所有者接收。定义父(接收)对象类型的模块还定义了接收子对象的访问控制。

通过在事务执行期间提供对父对象的 UID 的可变访问权限,可以动态强制执行对已发送子对象的访问的限制。由于这一点,你可以将对象转账给拥有的对象、动态字段对象、封装对象和共享对象。

转账至对象操作的好处之一是在链上拥有一个稳定的 ID,例如用于钱包或账户。无论你将对象转账到何处,都不会影响其 ID。当你转账对象时,所有该对象的子对象都会随之移动,并且对象的地址在你将其转账、封装或将其保持为动态字段时保持不变。

转账至对象

与常规对象转账一样,你必须确保要转账的对象 ID 存在。此外,确保要转账的对象不是不可变的。无法访问转账到不可变对象的对象。

请注意要转账的对象的类型以及正在转账的对象。被转账的对象(父对象)始终可以:

  • 定义可动态检查以访问已发送对象的断言。
  • 不支持访问已发送给它的对象。该包的未来版本可能会支持此功能,但由包的作者决定是否包含它。

如果正在转账的对象仅具有 key 能力,则:

  • 定义正在转账的对象的模块必须为其实现一个自定义接收函数,类似于自定义转账函数。与自定义转账函数一样,自定义接收函数可能会强制执行任意限制,你应该注意这一点,或者可能不存在这样的函数。
  • 发送后,除非父对象(被发送至的对象)的模块已定义接收对象的函数 并且 子对象(你要发送的对象)的模块已定义接收对象的函数,并且 两个 函数定义的限制都得到满足,否则你不能访问或使用该对象。
// 0xADD is an address
// 0x0B is an object ID
// b and c are objects

// Transfers the object `b` to the address 0xADD
sui::transfer::public_transfer(b, @0xADD);

// Transfers the object `c` to the object with object ID 0x0B
sui::transfer::public_transfer(c, @0x0B);

将对象转账至对象 ID 会产生与将对象转账至地址相同的结果 - 对象的所有者是提供的 32 字节地址或对象 ID。此外,由于对象转账的结果没有区别,你可以在 32 字节 ID 上使用现有的 RPC 方法,如 getOwnedObjects。如果该 ID 表示一个地址,那么该方法将返回该地址拥有的对象。如果 ID 是一个对象 ID,那么该方法将返回对象 ID 拥有的对象(已转移的对象)。

// Get the objects owned by the address 0xADD. Returns `b`.
{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getOwnedObjects",
"params": [
"0xADD"
]
}

// Get the objects owned by the object with object ID 0x0B. Returns `c`
{
"jsonrpc": "2.0",
"id": 1,
"method": "suix_getOwnedObjects",
"params": [
"0x0B"
]
}

接收对象

在对象 c 已被发送到另一个对象 p 后,p 必须接收 c 才能对其进行任何操作。为接收对象 c,在可编程事务块(PTB)中使用了 Receiving(o: ObjectRef) 参数类型,该类型接受一个包含待接收对象的 ObjectIDVersionDigest 的对象引用(就像 PTB 的所有权对象参数一样)。但是,Receiving PTB 参数不会在事务内作为拥有值或可变引用传递。

为了进一步解释,看一下在 Sui 框架中 sui::transfer 模块中定义的接收接口的核心部分:

module sui::transfer {
/// Represents the ability to receive an object of type `T`. Cannot be stored.
struct Receiving<phantom T: key> has drop { ... }

/// Given mutable (i.e., locked) access to the `parent` and a `Receiving`
/// object referencing an object owned by `parent` use the `Receiving` ticket
/// and return the corresponding object.
///
/// This function has custom rules that the Sui Move bytecode verifier enforces to ensure
/// that `T` is an object defined in the module where `receive` is invoked. Use
/// `public_receive` to receive an object with `store` outside of its defining module.
///
/// NB: &mut UID here allows the defining module of the parent type to
/// define custom access/permission policies around receiving objects sent
/// to objects of a type that it defines. You can see this more in the examples.
public native fun receive<T: key>(parent: &mut UID, object: Receiving<T>): T;

/// Given mutable (locked) access to the `parent` and a `Receiving` argument
/// referencing an object of type `T` owned by `parent` use the `object`
/// argument to receive and return the referenced owned object of type `T`.
/// The object `T` must have `store` to be received by this function, and
/// this can be called outside of the module that defines `T`.
public native fun public_receive<T: key + store>(parent: &mut UID, object: Receiving<T>): T;

...
}

每个在 PTB 中引用类型为 T 的已发送对象的 Receiving 参数都将导致正好一个具有 sui::transfer::Receiving<T> Move 类型的参数。随后,你可以使用此参数通过 sui::transfer::receive 函数接收类型为 T 的已发送对象。

在调用 sui::transfer::receive 函数时,你必须传递到父对象的 UID 的可变引用。但是,除非对象的定义模块将其暴露出来,否则无法获取对象的 UID 的可变引用。因此,接收子对象的父对象类型的定义模块定义了对发送到该对象的对象进行接收的访问控制策略和其他限制。有关此模式的演示,请参见授权示例。传入的 UID 实际上拥有由 Receiving 参数引用的对象的事实将动态检查和执行。这允许访问已发送到例如动态字段的对象,其中只能动态建立所有权链。

由于 sui::transfer::Receiving 仅具有 drop 能力,Receiving<T> 参数的存在表示在该事务期间接收由 PTB Receiving 参数中对象引用指定的类型为 T 的对象的能力,但不是义务。你可以在 PTB 中使用一些、没有或全部 Receiving 参数而无需问题。与 Receiving 参数相对应的任何对象都保持不变(特别是其对象引用保持不变)除非它被接收。

自定义接收规则

类似于自定义传输策略,Sui 允许为仅具有 key 的对象定义自定义接收规则。特别是,你只能在与调用 sui::transfer::receive 的模块中定义的对象上使用 sui::transfer::receive 函数,就像只能在使用 sui::transfer::transfer 的模块中定义的对象上使用该函数一样。

对于还具有 store 能力的对象,任何人都可以使用 sui::transfer::public_receive 函数接收它们,就像 sui::transfer::public_transfer 可以转移具有其上的 store 能力的任何对象一样。

这与父对象始终可以围绕接收规则定义自定义规则的事实结合在一起,意味着你必须考虑基于子对象的能力接收对象以及发送对象的能力的权限矩阵:

Child abilitiesParent can restrict accessChild can restrict access
keyYesYes
key + storeYesNo

就像自定义传输策略一样,你可以使用和组合这些限制来创建强大的表达式。例如,你可以使用自定义传输和接收规则一起实现soul-bound objects

使用 SDK

在创建交易时,与 Sui TypeScript SDK 中的其他对象参数几乎完全相同地与 Receiving 事务输入进行交互。例如,在接下来的Simple Account示例中,如果你想发送一笔交易,接收一个带有 ID 为 0xc0ffee 的硬币对象,该对象已发送到你的帐户地址 0xcafe,你可以使用 Sui TypeScript SDK 或 Sui Rust SDK 执行以下操作:

... // Setup Typescript SDK as normal.
const tx = new TransactionBlock();
tx.moveCall({
target: `${examplePackageId}::account::accept_payment`,
arguments: [tx.object("0xcafe"), tx.object("0xc0ffee")]
});
const result = await client.signAndExecuteTransactionBlock({
transactionBlock: tx,
});
...
... // setup Rust SDK client as normal
client
.transaction_builder()
.move_call(
sending_account,
example_package_id,
"account",
"accept_payment",
vec!["0x2::sui::SUI"],
vec![
SuiJsonValue::from_object_id("0xcafe"),
SuiJsonValue::from_object_id("0xc0ffee") // 0xcoffee is turned into the `Receiving<...>` argument of `accept_payment` by the SDK
])
...

此外,与具有 ObjectRef 构造函数的对象参数一样,你可以在其中提供显式的对象 ID、版本和摘要,还有一个 ReceivingRef 构造函数,它接受与接收参数相对应的相同参数。

示例

以下示例演示如何接收先前发送的对象。

从共享对象接收对象

通常,如果要允许从在模块中定义的共享对象接收已发送对象,请添加动态授权检查;否则,任何人都可以接收已发送对象。在此示例中,一个共享对象 (SharedObject) 持有一个计数器,任何人都可以增加,但只有地址 0xB0B 可以从共享对象接收对象。

由于 receive_object 函数是针对接收的对象通用的,它只能接收同时具有 keystore 能力的对象。receive_object 还必须使用 sui::transfer::public_receive 函数接收对象,而不是 sui::transfer::receive,因为你只能在当前模块中使用 receive 来定义对象。

module examples::shared_object_auth {
use sui::transfer::{Self, Receiving};
use sui::object;
use sui::tx_context::{Self, TxContext};
const EAccessDenied: u64 = 0;
const AuthorizedReceiverAddr: address = @0xB0B;

struct SharedObject has key {
id: object::UID,
counter: u64,
}

public fun create(ctx: &mut TxContext) {
let s = SharedObject {
id: object::new(ctx),
counter: 0,
};
transfer::share_object(s);
}

/// Anyone can increment the counter in the shared object.
public fun increment(obj: &mut SharedObject) {
obj.counter = obj.counter + 1;
}

/// Objects can only be received from the `SharedObject` by the
/// `AuthorizedReceiverAddr` otherwise the transaction aborts.
public fun receive_object<T: key + store>(obj: &mut SharedObject, sent: Receiving<T>, ctx: &TxContext): T {
assert!(tx_context::sender(ctx) == AuthorizedReceiverAddr, EAccessDenied);
transfer::public_receive(&mut obj.id, sent)
}
}

接收对象并将其添加为动态字段

此示例定义了一个基本的帐户类型模型,其中一个 Account 对象在不同的动态字段中保存其硬币余额。此 Account 还可转移到不同的地址或对象。

重要的是,与 Account 对象一起发送硬币的地址保持不变,无论 Account 对象是转移、封装(例如,封装在托管账户中)还是移动到动态字段中。特别是,对于给定的 Account 对象,在对象的生命周期内都有一个稳定的 ID,而不考虑任何所有权更改。

module examples::account {
use sui::transfer::{Self, Receiving};
use sui::coin::{Self, Coin};
use sui::object;
use sui::dynamic_field as df;
use sui::tx_context::TxContext;

const EBalanceDONE: u64 = 1;

/// Account object that `Coin`s can be sent to. Balances of different types
/// are held as dynamic fields indexed by the `Coin` type's `type_name`.
struct Account has key {
id: object::UID,
}

/// Dynamic field key representing a balance of a particular coin type.
struct AccountBalance<phantom T> has copy, drop, store { }

/// This function will receive a coin sent to the `Account` object and then
/// join it to the balance for each coin type.
/// Dynamic fields are used to index the balances by their coin type.
public fun accept_payment<T>(account: &mut Account, sent: Receiving<Coin<T>>) {
// Receive the coin that was sent to the `account` object
// Since `Coin` is not defined in this module, and since it has the `store`
// ability we receive the coin object using the `transfer::public_receive` function.
let coin = transfer::public_receive(&mut account.id, sent);
let account_balance_type = AccountBalance<T>{};
let account_uid = &mut account.id;

// Check if a balance of that coin type already exists.
// If it does then merge the coin we just received into it,
// otherwise create new balance.
if (df::exists_(account_uid, account_balance_type)) {
let balance: &mut Coin<T> = df::borrow_mut(account_uid, account_balance_type);
coin::join(balance, coin);
} else {
df::add(account_uid, account_balance_type, coin);
}
}

/// Withdraw `amount` of coins of type `T` from `account`.
public fun withdraw<T>(account: &mut Account, amount: u64, ctx: &mut TxContext): Coin<T> {
let account_balance_type = AccountBalance<T>{};
let account_uid = &mut account.id;
// Make sure what we are withdrawing exists
assert!(df::exists_(account_uid, account_balance_type), EBalanceDONE);
let balance: &mut Coin<T> = df::borrow_mut(account_uid, account_balance_type);
coin::split(balance, amount, ctx)
}

/// Can transfer this account to a different address
/// (e.g., to an object or address).
public fun transfer_account(account: Account, to: address, _ctx: &mut TxContext) {
// Perform some authorization checks here and if they pass then transfer the account
// ...
transfer::transfer(account, to);
}
}

灵魂绑定对象

通过控制对象何时以及如何被接收,以及何时以及如何被转移的规则,我们可以定义一种“灵魂绑定”对象类型,该对象可以在交易中按值使用,但必须始终留在同一位置,或者返回到同一对象。

你可以使用以下模块实现其简化版本,其中 get_object 函数接收灵魂绑定对象并创建必须在事务中销毁的收据,以便成功执行事务。然而,为了销毁收据,接收到的对象必须使用 return_object 函数在事务中转移到其接收对象以执行。

module examples::soul_bound {
use sui::transfer::{Self, Receiving};
use sui::object::{Self, UID, ID};

/// This object has `key` only -- if this had `store` we would not be
/// able to ensure it is bound to whatever address we sent it to
struct SoulBound has key {
id: UID,
}

/// A non-store, non-drop, non-copy struct. When you receive a `SoulBound`
/// object, we'll also give you one of these. In order to successfully
/// execute the transaction you need to destroy this `ReturnReceipt` and
/// the only way to do that is to transfer it back to the same object you
/// received it from in the transaction using the `return_object` function.
struct ReturnReceipt {
// The object ID of the object that needs to be returned.
// This field is required to prevent swapping of soul bound objects if
// multiple are present in the same transaction.
object_id: ID,
// The address (object ID) it needs to be returned to.
return_to: address,
}

/// Tried to return the wrong object.
const EWrongObject: u64 = 0;

/// Takes the object UID that owns the `SoulBound` object and a `SoulBound`
/// receiving ticket. It then receives the `SoulBound` object and returns a
/// `ReturnReceipt` that must be destroyed in the transaction by calling `return_object`.
public fun get_object(parent: &mut UID, soul_bound_ticket: Receiving<SoulBound>): (SoulBound, ReturnReceipt) {
let soul_bound = transfer::receive(parent, soul_bound_ticket);
let return_receipt = ReturnReceipt {
return_to: object::uid_to_address(parent),
object_id: object::id(&soul_bound),
};
(soul_bound, return_receipt)
}

/// Given a `SoulBound` object and a return receipt returns it to the
/// object it was received from. Verifies that the `receipt`
/// is for the given `soul_bound` object before returning it.
public fun return_object(soul_bound: SoulBound, receipt: ReturnReceipt) {
let ReturnReceipt { return_to, object_id } = receipt;
assert!(object::id(&soul_bound) == object_id, EWrongObject);
sui::transfer::transfer(soul_bound, return_to);
}
}