Skip to content

[book] transfer to object #106

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

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
Collaborator

Choose a reason for hiding this comment

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

I feel like the term "PTB" should be mentioned somewhere on this page.


- 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

It would be good to have an example and/or more detail on how you actually create a receiving object, either on this page or that one


## 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
[Sui Object Model](./../object/object-model.md) - transferring objects to other objects. If certain
[Sui Object Model](./../object/object-model.md) &mdash; transferring objects to other objects. If certain

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
Contributor

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.

```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
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think including this part in the example would be very helpful - I don't yet see how the end-to-end process of transferring works. e.g. if Alice wants to transfer an object to a PostOffice owned by Bob, does Bob have to execute a PTB that constructs a Receiving and hands it to the PostOffice's receive function?

[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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if it would be clearer if this function wasn't named receive? Again going back to the "implements" discussion above, people might think this name is special.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry to chime in an internal discussion, but I got sort of the same question. For an object to be able to receive other objects it HAS TO have a struct method called public_receive or just a struct method, doesn't matter how is called, that will handle the object and call to transfer::public_receive.

Also not sure where to mention this, but I feel like it would be really good to compare this to DOFs. My best guess right now is that dofs are for attaching specific objects to others in the module defining the outer object, while transfer to object will be for the developer allowing anyone who's able to obtain a mutable reference to the object to deposit anything on it. Very different purposes, but since both end up with an object wrapped into another I really feel it's worth clarifying.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm very confused about the non-public receive here. I don't understand the connection between whether or not something has store and whether the receive function is public. I also don't have a clear picture of which I should use under what conditions.

}
```
Comment on lines +101 to +105
Copy link
Contributor

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
Contributor

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
[Dynamic Fields](./../programmability/dynamic-fields.md), which we explore in the
[dynamic fields](./../programmability/dynamic-fields.md), which we explore in the


## 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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think the "including being transferred again" part was discussed above, so it probably shouldn't be mentioned in the summary.

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