Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[book] transfer to object #106

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
- [Ability: Store](./storage/store-ability.md)
- [UID and ID](./storage/uid-and-id.md)
- [Restricted and Public Transfer](./storage/transfer-restrictions.md)
- [Transfer to Object?]() <!-- (./storage/transfer-to-object.md) -->
- [Transfer to Object?](./storage/transfer-to-object.md)
- [Advanced Programmability](./programmability/README.md)
- [Transaction Context](./programmability/transaction-context.md)
- [Module Initializer](./programmability/module-initializer.md)
Expand Down
35 changes: 21 additions & 14 deletions book/src/concepts/what-is-a-transaction.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,27 @@ Transactions consist of:

## Inputs

Transaction inputs are the arguments for the transaction and are split between 2 types:
Transaction inputs are the arguments for the transaction and are split between 3 types:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically they are split into four types:

  • Pure arguments
  • Owned or Immutable Object arguments
  • Shared Object Arguments
  • Receiving Object Arguments

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, will expand!


- Pure arguments: These are mostly [primitive types](../move-basics/primitive-types.html) with some
extra additions. A pure argument can be:
- [`bool`](../move-basics/primitive-types.html#booleans).
- [unsigned integer](../move-basics/primitive-types.html#integer-types) (`u8`, `u16`, `u32`, `u64`, `u128`, `u256`).
- [`address`](../move-basics/address.html).
- [`std::string::String`](../move-basics/string.html), UTF8 strings.
- [`std::ascii::String`](../move-basics/string.html#ascii-strings), ASCII strings.
- [`vector<T>`](../move-basics/vector.html), where `T` is a pure type.
- [`std::option::Option<T>`](../move-basics/option.html), where `T` is a pure type.
- [`std::object::ID`](../storage/uid-and-id.html), typically points to an object. See also [What is an Object](../object/object-model.html).
extra additions. A pure argument can be:
- [`bool`](../move-basics/primitive-types.html#booleans).
- [unsigned integer](../move-basics/primitive-types.html#integer-types) (`u8`, `u16`, `u32`,
`u64`, `u128`, `u256`).
- [`address`](../move-basics/address.html).
- [`std::string::String`](../move-basics/string.html), UTF8 strings.
- [`std::ascii::String`](../move-basics/string.html#ascii-strings), ASCII strings.
- [`vector<T>`](../move-basics/vector.html), where `T` is a pure type.
- [`std::option::Option<T>`](../move-basics/option.html), where `T` is a pure type.
- [`std::object::ID`](../storage/uid-and-id.html), typically points to an object. See also
[What is an Object](../object/object-model.html).
- Object arguments: These are objects or references of objects that the transaction will access. An
object argument needs to be either a shared object, a frozen object, or an object that the
transaction sender owns, in order for the transaction to be successfull.
For more see [Object Model](../object/index.html).
object argument needs to be either a shared object, a frozen object, or an object that the
transaction sender owns, in order for the transaction to be successfull. For more see
[Object Model](../object/index.html).
- Receiving object argument: a special argument that is used to receive an object transferred to
another object. We cover this in more detail in the
[Transfer to Object](../storage/transfer-to-object.html) section.

## Commands

Expand Down Expand Up @@ -95,6 +101,7 @@ The result of the executed transaction consists of different parts:
status of the transaction, updates to objects and their new versions, the gas object used, the gas
cost of the transaction, and the events emitted by the transaction;
- Events - the custom [events](./../programmability/events.md) emitted by the transaction;
- Object Changes - the changes made to the objects, including the _change of ownership_;
- Object Changes - the changes made to the objects, including the
[_change of ownership_](../storage/storage-functions.md);
- Balance Changes - the changes made to the aggregate balances of the account involved in the
transaction;
2 changes: 1 addition & 1 deletion book/src/move-basics/abilities-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The abilities are separated by commas. Move supports 4 abilities: `copy`, `drop`

```move
/// This struct has the `copy` and `drop` abilities.
struct VeryAble has copy, drop {
public struct VeryAble has copy, drop {
// field: Type1,
// field2: Type2,
// ...
Expand Down
134 changes: 131 additions & 3 deletions book/src/storage/transfer-to-object.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,132 @@
# Transfer to Object?
# Transfer to Object

The `transfer::transfer` call takes the receiver `address` as the second argument, and while in most
of the cases it is an account address, it can also be an address of an object. In this case,
Previously, we have explained how [transfer](./storage-functions.md#transfer) works in relation to
accounts. However, there is an additional behaviour allowed by the
[Sui Object Model](./../object/object-model.md) - transferring objects to other objects. If certain
conditions are met (which we explain in this section), objects can be sent to and _received_ from
other objects.

<div class="warning">

Warning: recipient object must be implemented in a way that allows receiving. Attempt to send an
object to an object that does not implement receiving can result in asset loss.

</div>

## Background

Every object in Sui has its own `UID` which is represented by the
[address type](./../move-basics/address.md). This address can be used to query the object's data,
and to perform account queries, such as getting the list of owned objects. This property of the
Comment on lines +18 to +19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be a bit confusing to folks, because depending on context, an object's ID can be represented by a UID, ID, or address type. On this page the address representation is the most interesting/novel one, but in other circumstances (e.g. for dynamic fields) UID is more interesting, and in other circumstances (e.g. ID pointer) ID is more interesting.

Here seems like the wrong place to discuss this concept in detail, but it seems like something we should document somewhere.

system lays the foundation for the transfer to object feature.

> In this section, by _object address_ we mean the address value stored in the `UID` of the object.

## Transfer to Object

An object can be transferred to another object by calling the `transfer` function, or its public
version - `public_transfer`. The behaviour is identical to the transfer to account.

```move
// File: sui-framework/sources/transfer.move
module sui::transfer;

public fun transfer<T: key>(object: T, recipient: address) {}
public fun public_transfer<T: key + store>(object: T, recipient: address) {}
```

## Receiving Objects

While objects _owned by accounts_ can be used in the transaction directly (given that the sender of
the transaction is the owner), objects transferred to other objects first need to be _received_
through their owner. The owner object needs to implement a custom function that will call the
`receive` function from the `transfer` module.

Comment on lines +42 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so-so on the phrase "the owner object needs to implement..." because it sounds like interface implementation which is not a concept we have in Move. It also puts the focus on the custom receive function, and I'm not sure that's the focus for the object doing the receiving (the only thing that matters for the receiving object is that it exposes its &mut UID, but it's probably a good idea for that to be done in a limited way that means you can only use it for specific purposes). A custom receive function is more important for dealing with the object being transferred -- because if that object does not have store, it needs a custom function to call receive on it.

Copy link
Collaborator Author

@damirka damirka Dec 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also had this feeling, and I don't like it. The biggest blocker I faced on every attempt to write this page.

```move
// File: sui-framework/sources/transfer.move
module sui::transfer;

/// Special type which cannot be constructed in Move but can be passed as a
/// special input to a PTB.
public struct Receiving<phantom T: key> has drop {
id: ID,
version: u64,
}

/// Private receiving function for `key`-only objects.
public fun receive<T: key>(parent: &mut UID, to_receive: Receiving<T>): T { /* ... */ }

/// Public receiving function for `key + store` objects.
public fun public_receive<T: key + store>(parent: &mut UID, to_receive: Receiving<T>): {
/* ... */
}
```

The `Receiving` type is a special type that references an object-owned object. It cannot be
constructed in Move, and can only be passed as a special input to a
[Programmable Transaction Block (PTB)](../concepts/what-is-a-transaction.md). The `receive` (or
`public_receive`) function must be called in order to exchange the `Receiving` instance for the
actual object.

While this may seem overwhelming, we will demonstrate the feature in a simple example.

## Example

To demonstrate the feature, let's consider a simple `PostOffice` object that can receive any objects
transferred to it. Given that the transfer itself can be performed on the PTB level, or in a Move
function, we will focus on the receiving part.

```move
module book::post_office;

// While the `sui::transfer` module is implicitly imported, the `Receiving` type
// is not. We need to import it explicitly.
use sui::transfer::Receiving;

/// The `PostOffice` object that can receive any objects transferred to it via
/// a custom `receive` function.
public struct PostOffice has key {
id: UID,
}

/// Voila! The `receive` function can now be called on the `PostOffice` object
/// to receive any objects transferred to it.
public fun public_receive<T: key + store>(
po: &mut PostOffice,
to_receive: Receiving<T>
): T {
transfer::public_receive(&mut po.id, to_receive)
}

/// For objects without `store` (though, they would require special handling), we
/// can implement the non-public version of the `receive` function.
public fun receive<T: key>(po: &mut PostOffice, to_receive: Receiving<T>): T {
transfer::receive(&mut po.id, to_receive)
}
```
Comment on lines +101 to +105
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? I would expect it to fail because of custom transfer rules (the non-public_ receive is being called on type T from a module that does not define T).


In this example, we have defined a `PostOffice` object that can receive any objects transferred to
it. We omitted the creation of the `PostOffice` for brevity; additionally, an implementation like
this would require some authorization to prevent unauthorized claims. We talk about authorization
patterns in the [Advanced Programmability](./../programmability/) chapter.

## Limitations

Transfer to Object must be used with caution, as the recipient object must be implemented in a way
that allows receiving. Worth noting, that objects owned by other objects must be taken by value, and
cannot be borrowed as references, unlike objects owned by accounts.

Transfer to Object is not the only way to create a relationship between objects. There are more
flexible and feature-rich ways, such as the
[Dynamic Fields](./../programmability/dynamic-fields.md), which we explore in the
[Advanced Programmability](./../programmability/) chapter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not know if this is the place for this comment, however as dynamic fields are referenced here, here it is:
Something that comes up a lot:

Should I use dynamic fields or TTO?

One major difference is that one does not need to have ownership of the owner-object in order to transfer an object to it.
Dynamic fields on the other hand can be seen as having more "control" on what is added "below" the owner object.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Been thinking about it a lot and still don't have an answer. Technically, in the book, it is too early to mention dynamic fields (they come mid next chapter), yet dynamic fields are too far from transfer to object. Very good point, and very much worth discussing!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind it's not really an either/or of TTO vs dynamic fields. TTO is about transfer, dynamic fields are about ownership -- i.e. it's perfectly valid to have a protocol where you use TTO to transfer something to an object, and then when it is received, it is turned into a dynamic field on the object it was received on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amnn I agree that they're not replacing each other (though, in some situations they do), but more of a guide which one is suited for which purpose.


## Summary

- Objects can be transferred to other objects by calling the `transfer` function.
- The recipient object must be able to `receive` transferred objects, otherwise the objects may be
lost.
- The `Receiving` type is a special type that references an object-owned object. It is passed as a
special input to a transaction and cannot be constructed dynamically.
- Once received, the object can be used normally, including being transferred again, even to the
same parent object.
5 changes: 4 additions & 1 deletion packages/samples/sources/move-basics/assert-and-abort.move
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ if (!user_has_access) {
if (user_has_access) {
abort(1)
};

// abort may also come without a code
abort
// ANCHOR_END: abort
}

Expand Down Expand Up @@ -68,4 +71,4 @@ public fun update_value(user: &mut User, value: u64) {
user.value = value;
}
// ANCHOR_END: error_attribute
}

2 changes: 1 addition & 1 deletion packages/samples/sources/move-basics/struct.move
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#[allow(unused_variable, unused_field)]
module book::struct_syntax;

use std::string::{Self, String};
use std::string::String;

// ANCHOR: def
/// A struct representing an artist.
Expand Down
2 changes: 1 addition & 1 deletion packages/samples/sources/programmability/events.move
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ public fun purchase(coin: Coin<SUI>, ctx: &mut TxContext) {
});

// Omitting the rest of the implementation to keep the example simple.
abort 0
abort
}
// ANCHOR_END: emit
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ public fun purchase_phone(ctx: &mut TxContext): (Phone, Ticket) {
public fun pay_in_bonus_points(ticket: Ticket, payment: Coin<BONUS>) {
let Ticket { amount } = ticket;
assert!(payment.value() == amount);
abort 0 // omitting the rest of the function
abort // omitting the rest of the function
}

/// The customer may pay for the `Phone` with `USD`.
public fun pay_in_usd(ticket: Ticket, payment: Coin<USD>) {
let Ticket { amount } = ticket;
assert!(payment.value() == amount);
abort 0 // omitting the rest of the function
abort // omitting the rest of the function
}
// ANCHOR_END: phone_shop
}
Loading