From 17d9a44e6ba67983faca0547723d1ef96d767cd0 Mon Sep 17 00:00:00 2001 From: Randall-Mysten <109545725+randall-Mysten@users.noreply.github.com> Date: Thu, 23 Mar 2023 15:37:01 -0700 Subject: [PATCH] Updates for style, voice, tone, code changes (#9741) ## Description Describe the changes or additions included in this PR. ## Test Plan How did you test the new or updated feature? --- If your changes are not user-facing and not a breaking change, you can skip the following section. Otherwise, please indicate what changed, and then add to the Release Notes section as highlighted during the release process. ### Type of Change (Check all that apply) - [ ] user-visible impact - [ ] breaking change for a client SDKs - [ ] breaking change for FNs (FN binary must upgrade) - [ ] breaking change for validators or node operators (must upgrade binaries) - [ ] breaking change for on-chain data layout - [ ] necessitate either a data wipe or data migration ### Release notes --------- Co-authored-by: ronny-mysten <118224482+ronny-mysten@users.noreply.github.com> --- .../ch2-using-objects.md | 149 +++++++++++------- .../ch3-immutable-objects.md | 118 +++++++++----- .../ch4-object-wrapping.md | 91 +++++------ .../ch5-dynamic-fields.md | 36 ++--- .../ch6-collections.md | 36 ++--- doc/src/build/sui-object-display.md | 18 +-- doc/src/learn/transactions.md | 4 + 7 files changed, 267 insertions(+), 185 deletions(-) diff --git a/doc/src/build/programming-with-objects/ch2-using-objects.md b/doc/src/build/programming-with-objects/ch2-using-objects.md index c257e84579639..20dae351355cf 100644 --- a/doc/src/build/programming-with-objects/ch2-using-objects.md +++ b/doc/src/build/programming-with-objects/ch2-using-objects.md @@ -2,12 +2,13 @@ title: Chapter 2 - Using Objects --- -In [Chapter 1](./ch1-object-basics.md) we covered how to define, create and take ownership of a Sui object in Move. In this chapter we will look at how to use objects that you own in Move calls. +The [Object Basics](./ch1-object-basics.md) chapter covered how to define, create and take ownership of a Sui object in Sui Move. This chapter describes how to use objects that you own in Sui Move calls. -Sui authentication mechanisms ensure only you can use objects owned by you in Move calls. (We will cover non-owned objects in future chapters.) To use an object in Move calls, pass them as parameters to an [entry function](../move/index.md#entry-functions). Similar to Rust, there are a few ways to pass parameters: +Sui authentication mechanisms ensure only you can use objects owned by you in Sui Move calls. To use an object in Sui Move calls, pass them as parameters to an [entry function](../move/index.md#entry-functions). Similar to Rust, there are a few ways to pass parameters, as described in the following sections. ### Pass objects by reference -There are two ways to pass objects by reference: read-only references (`&T`) and mutable references (`&mut T`). Read-only references allow you to read data from the object, while mutable references allow you to mutate the data in the object. Let's try to add a function that would allow us to update one of `ColorObject`'s values with another `ColorObject`'s value. This will exercise using both read-only references and mutable references. + +There are two ways to pass objects by reference: read-only references (`&T`) and mutable references (`&mut T`). Read-only references allow you to read data from the object, while mutable references allow you to mutate the data in the object. To add a function that allows you to update one of the values of `ColorObject` with another value of `ColorObject`. This exercises both using read-only references and mutable references. The `ColorObject` we defined in the previous chapter looks like: ```rust @@ -18,7 +19,9 @@ struct ColorObject has key { blue: u8, } ``` -Now let's add this function: + +Now, add this function: + ```rust /// Copies the values of `from_object` into `into_object`. public entry fun copy_into(from_object: &ColorObject, into_object: &mut ColorObject) { @@ -27,15 +30,19 @@ public entry fun copy_into(from_object: &ColorObject, into_object: &mut ColorObj into_object.blue = from_object.blue; } ``` -> :bulb: We declared this function with the `entry` modifier to be callable as an entry function from transactions. -In the above function signature, `from_object` can be a read-only reference because we only need to read its fields; conversely, `into_object` must be a mutable reference since we need to mutate it. In order for a transaction to make a call to the `copy_into` function, **the sender of the transaction must be the owner of both of `from_object` and `into_object`**. +Declare this function with the `entry` modifier so that it is callable as an entry function from transactions. + +In the preceding function signature, `from_object` can be a read-only reference because you only need to read its fields. Conversely, `into_object` must be a mutable reference since you need to mutate it. For a transaction to make a call to the `copy_into` function, the sender of the transaction must be the owner of both `from_object` and `into_object`. + +Although `from_object` is a read-only reference in this transaction, it is still a mutable object in Sui storage--another transaction could be sent to mutate the object at the same time. To prevent this, Sui must lock any mutable object used as a transaction input, even when it's passed as a read-only reference. In addition, only an object's owner can send a transaction that locks the object. + +The following section describes how to write a unit test to learn how to interact with multiple objects of the same type in tests. + +The previous chapter introduced the `take_from_sender` function, which takes an object of type `T` from the global storage created by previous transactions. However, if there are multiple objects of the same type, `take_from_sender` is no longer able to determine which one to return. To solve this problem, use two new, test-only functions. The first is `tx_context::last_created_object_id(ctx)`, which returns the ID of the most recently created object. The second is `test_scenario::take_from_sender_by_id`, which returns an object of type `T` with a specific object ID. -> :bulb: Although `from_object` is a read-only reference in this transaction, it is still a mutable object in Sui storage--another transaction could be sent to mutate the object at the same time! To prevent this, Sui must lock any mutable object used as a transaction input, even when it's passed as a read-only reference. In addition, only an object's owner can send a transaction that locks the object. +Create the test `test_copy_into` as shown in this example: -Let's write a unit test to see how we could interact with multiple objects of the same type in tests. -In the previous chapter, we introduced the `take_from_sender` API, which takes an object of type `T` from the global storage created by previous transactions. However, what if there are multiple objects of the same type? `take_from_sender` will no longer be able to tell which one to return. To solve this problem, we need to use two new, test-only APIs. The first is `tx_context::last_created_object_id(ctx)`, which returns the ID of the most recently created object. The second is `test_scenario::take_from_sender_by_id`, which returns an object of type `T` with a specific object ID. -Now let's take a look at the test (`test_copy_into`): ```rust let owner = @0x1; let scenario_val = test_scenario::begin(owner); @@ -50,7 +57,9 @@ let (id1, id2) = { (id1, id2) }; ``` -The above code created two objects. Note that right after each call, we make a call to `tx_context::last_created_object_id` to get the ID of the object just created. At the end we have `id1` and `id2` capturing the IDs of the two objects. Next we retrieve both of them and test the `copy_into` function: + +The preceding code creates two objects. Note that right after each call, it makes a call to `tx_context::last_created_object_id` to get the ID of the object the call created. At the end, `id1` and `id2` capture the IDs of the two objects. Next, retrieve both of them and test the `copy_into` function: + ```rust test_scenario::next_tx(scenario, owner); { @@ -65,7 +74,9 @@ test_scenario::next_tx(scenario, owner); test_scenario::return_to_sender(scenario, obj2); }; ``` -We used `take_from_sender_by_id` to take both objects using different IDs. We then used `copy_into` to update `obj1`'s value using `obj2`'s. We can verify that the mutation works: + +This uses `take_from_sender_by_id` to take both objects using different IDs. Use `copy_into` to update the value for `obj1` using the value for `obj2`. You can verify that the mutation works: + ```rust test_scenario::next_tx(scenario, owner); { @@ -78,31 +89,38 @@ test_scenario::end(scenario_val); ``` ### Pass objects by value -Objects can also be passed by value into an entry function. By doing so, the object is moved out of Sui storage. It is then up to the Move code to decide where this object should go. -> :books: Since every [Sui object struct type](./ch1-object-basics.md#define-sui-object) must include `UID` as its first field, and the [UID struct](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object.move) does not have the `drop` ability, the Sui object struct type [cannot](https://github.com/move-language/move/blob/main/language/documentation/book/src/abilities.md#drop) have the `drop` ability either. Hence, any Sui object cannot be arbitrarily dropped and must be either consumed (e.g., transferred to another owner) or deleted by [unpacking](https://move-book.com/advanced-topics/struct.html#destructing-structures), as described below. +You can also pass objects by value into an entry function. By doing so, the object is moved out of Sui storage. It is then up to the Sui Move code to decide where this object should go. + +Since every [Sui object struct type](./ch1-object-basics.md#define-sui-object) must include `UID` as its first field, and the [UID struct](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object.move) does not have the `drop` ability, the Sui object struct type cannot have the [drop](https://github.com/move-language/move/blob/main/language/documentation/book/src/abilities.md#drop) ability either. Hence, any Sui object cannot be arbitrarily dropped and must be either consumed (for example, transferred to another owner) or deleted by [unpacking](https://move-book.com/advanced-topics/struct.html#destructing-structures), as described in the following sections. + +There are two ways to handle a pass-by-value Sui object in Move: + * delete the object + * transfer the object -There are two ways we can deal with a pass-by-value Sui object in Move: -- delete the object -- transfer the object +#### Delete the object -#### Option 1. Delete the object -If the intention is to actually delete the object, we can unpack the object. This can be done only in the module that defined the struct type, due to Move's [privileged struct operations rules](https://github.com/move-language/move/blob/main/language/documentation/book/src/structs-and-resources.md#privileged-struct-operations). Upon unpacking, if any field is also of struct type, recursive unpacking and deletion will be required. +If the intention is to actually delete the object, unpack it. You can do this only in the module that defined the struct type, due to Move's [privileged struct operations rules](https://github.com/move-language/move/blob/main/language/documentation/book/src/structs-and-resources.md#privileged-struct-operations). If any field is also of struct type, you must use recursive unpacking and deletion when you unpack the object. + +However, the `id` field of a Sui object requires special handling. You must call the following API in the [object](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object.move) module to signal Sui that we intend to delete this object: -However, the `id` field of a Sui object requires special handling. We must call the following API in the [object](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object.move) module to signal Sui that we intend to delete this object: ```rust public fun delete(id: UID) { ... } ``` -Let's define a function in the `ColorObject` module that allows us to delete the object: + +Define a function in the `ColorObject` module that allows us to delete the object: + ```rust public entry fun delete(object: ColorObject) { let ColorObject { id, red: _, green: _, blue: _ } = object; object::delete(id); } ``` -As we can see, the object is unpacked, generating individual fields. The u8 values are primitive types and can all be dropped. However the `id` (which has type `UID`) cannot be dropped and must be explicitly deleted through the `object::delete` API. At the end of this call, the object will no longer be stored on-chain. -We can add a unit test for it, as well: +The object unpacks and generates individual fields. You can drop all of the u8 values, which are primitive types. However, you can't drop the `id`, which has type `UID`, and must explicitly delete it using the `object::delete` API. At the end of this call, the object is no longer stored on-chain. + +To add a unit test for it: + ```rust let owner = @0x1; // Create a ColorObject and transfer it to @owner. @@ -125,18 +143,23 @@ test_scenario::next_tx(scenario, &owner); }; test_scenario::end(scenario_val); ``` -The first part is the same as what we have seen in [Chapter 1](./ch1-object-basics.md#writing-unit-tests), which creates a new `ColorObject` and puts it in the owner's account. The second transaction is what we are testing: retrieve the object from the storage and then delete it. Since the object is deleted, there is no need (in fact, it is impossible) to return it to the storage. The last part of the test checks that the object is indeed no longer in the global storage and hence cannot be retrieved from there. + +The first part of the example repeats the example used in [Object Basics](./ch1-object-basics.md#writing-unit-tests), and creates a new `ColorObject` and puts it in the owner's account. The second transaction gets tested. It retrieves the object from the storage and then delete it. Since the object is deleted, there is no need (in fact, it is impossible) to return it to the storage. The last part of the test checks that the object is indeed no longer in the global storage and hence cannot be retrieved from there. #### Option 2. Transfer the object -The owner of the object may want to transfer it to another address. To support this, the `ColorObject` module will need to define a `transfer` API: + +The owner of the object might want to transfer it to another address. To support this, the `ColorObject` module needs to define a `transfer` function: + ```rust public entry fun transfer(object: ColorObject, recipient: address) { transfer::transfer(object, recipient) } ``` ->:bulb: One cannot call `transfer::transfer` directly as it is not an `entry` function. -Let's add a test for transferring too. First of all, we create an object in `owner`'s account and then transfer it to a different account `recipient`: +You cannot call `transfer::transfer` directly as it is not an `entry` function. + +Add a test for transferring too. First, create an object in the account of the `owner`, and then transfer it to a different account `recipient`: + ```rust let owner = @0x1; // Create a ColorObject and transfer it to @owner. @@ -155,7 +178,9 @@ test_scenario::next_tx(scenario, owner); transfer::transfer(object, recipient, ctx); }; ``` -Note that in the second transaction, the sender of the transaction should still be `owner`, because only the `owner` can transfer the object that it owns. After the transfer, we can verify that `owner` no longer owns the object, while `recipient` now owns it: + +Note that in the second transaction, the sender of the transaction should still be `owner`, because only the `owner` can transfer the object that it owns. After the transfer, you can verify that `owner` no longer owns the object, and `recipient` now owns it: + ```rust // Check that owner no longer owns the object. test_scenario::next_tx(scenario, owner); @@ -171,47 +196,63 @@ test_scenario::end(scenario_val); ``` ### On-chain interactions -Now it's time to try this out on-chain. Assuming you have already followed the instructions in [Chapter 1](./ch1-object-basics.md#on-chain-interactions), you should already have the package published and a new object created. -Now we can try to transfer it to another address. First let's see what other addresses you own: -``` -$ sui client addresses -``` -Since the default current address is the first address, let's pick the second address in the list as the recipient. In my case, I have `0x1416f3d5af469905b0580b9af843ec82d02efd30`. Let's save it for convenience: -``` -$ export RECIPIENT=0x1416f3d5af469905b0580b9af843ec82d02efd30 + +Next, try this out on-chain. Assuming you followed the instructions in [Object Basics](./ch1-object-basics.md#on-chain-interactions), you should have a published package and a new object created. + +To transfer it to another address, first check the addresses available: + +```shell +sui client addresses ``` -Now let's transfer the object to this address: + +Choose an address other than the active address. If you have only one address, create another address using the [Sui Client CLI](../cli-client.md#create-a-new-account-address). + +For this example, the recipient address is: `0x44840a79dd5cf1f5efeff1379f5eece04c72db13512a2e31e8750f5176285446`. Save it as a variable for convenience: + +```shell +export RECIPIENT=0x44840a79dd5cf1f5efeff1379f5eece04c72db13512a2e31e8750f5176285446 ``` + +Now, transfer the object to the address: + +```shell $ sui client call --gas-budget 1000 --package $PACKAGE --module "color_object" --function "transfer" --args \"$OBJECT\" \"$RECIPIENT\" ``` + Now let's see what objects the `RECIPIENT` owns: -``` + +```shell $ sui client objects $RECIPIENT ``` -We should be able to see that one of the objects in the list is the new `ColorObject`! This means the transfer was successful. -Let's also try to delete this object: -``` +You should see the `ColorObject` listed. This means the transfer succeeded. + +To delete this object: + +```shell $ sui client call --gas-budget 1000 --package $PACKAGE --module "color_object" --function "delete" --args \"$OBJECT\" ``` -Oops. It will error out and complain that the address is unable to lock the object, which is a valid error because we have already transferred the object away from the original owner. -In order to operate on this object, we need to switch our client address to `$RECIPIENT`: -``` +The command returns an error indicating that the address is unable to lock the object. This is a valid error because the address used for the command, the active address, no longer owns the object. + +To operate on this object, use the recipient address, `$RECIPIENT`: + +```shell $ sui client switch --address $RECIPIENT ``` -And try the deletion again: -``` + +And try the to delete the object again: + +```shell $ sui client call --gas-budget 1000 --package $PACKAGE --module "color_object" --function "delete" --args \"$OBJECT\" ``` -In the output, you will see in the `Transaction Effects` section a list of deleted objects. -This shows that the object was successfully deleted. If we run this again: -``` + +In the `Transaction Effects` section of the output, you see a list of deleted objects. + +This shows that the object was successfully deleted. If you run the command again: + +```shell $ sui client objects $RECIPIENT ``` -We will see that this object is no longer there in the address. -In this chapter you learned: -- to pass objects by reference -- to pass objects by value (deleting or transferring them) -- to transfer and interact with objects on-chain +You see that the object is no longer listed for the address. \ No newline at end of file diff --git a/doc/src/build/programming-with-objects/ch3-immutable-objects.md b/doc/src/build/programming-with-objects/ch3-immutable-objects.md index cc27fc2d063a9..0c7195afc8830 100644 --- a/doc/src/build/programming-with-objects/ch3-immutable-objects.md +++ b/doc/src/build/programming-with-objects/ch3-immutable-objects.md @@ -2,57 +2,67 @@ title: Chapter 3 - Immutable Objects --- -In chapters 1 and 2, we learned how to create and use objects owned by an address. In this chapter, we will demonstrate how to create and use immutable objects. +Chapters 1 and 2 describe how to create and use objects owned by an address. This chapter describes how to create and use immutable objects. -Objects in Sui can have different types of [ownership](../objects.md#object-ownership), with two broad categories: immutable objects and mutable objects. An immutable object is an object that can **never** be mutated, transferred or deleted. Because of this immutability, the object is not owned by anyone, and hence it can be used by anyone. +Objects in Sui can have different types of [ownership](../objects.md#object-ownership), with two broad categories: immutable objects and mutable objects. An immutable object is an object that can't be mutated, transferred or deleted. Immutable objects have no owner, so anyone can use them. ### Create immutable object -Regardless of whether an object was just created or already owned by an address, to turn this object into an immutable object, we need to call the following API in the [transfer module](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/transfer.move): +To convert an object into an immutable object, call the following function in the [transfer module](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/transfer.move): + ```rust public native fun freeze_object(obj: T); ``` -After this call, the specified object will become permanently immutable. This is a non-reversible operation; hence, freeze an object only when you are certain that it will never need to be mutated. -Let's add an entry function to the [color_object module](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/objects_tutorial/sources/color_object.move) to turn an existing (owned) `ColorObject` into an immutable object: +This call makes the specified object immutable. This is a non-reversible operation. You should freeze an object only when you are certain that you don't need to mutate it. + +Add an entry function to the [color_object module](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/objects_tutorial/sources/color_object.move) to turn an existing (owned) `ColorObject` into an immutable object: + ```rust public entry fun freeze_object(object: ColorObject) { transfer::freeze_object(object) } ``` -In the above function, one must already own a `ColorObject` to be able to pass it in. At the end of this call, this object is *frozen* and can never be mutated. It is also no longer owned by anyone. -> :bulb: Note the `transfer::freeze_object` API requires passing the object by value. Had we allowed passing the object by a mutable reference, we would then still be able to mutate the object after the `freeze_object` call; this contradicts the fact that it should have become immutable. -Alternatively, you can also provide an API that creates an immutable object at birth: +In the preceding function, you must already own a `ColorObject` to pass it in. At the end of this call, this object is *frozen* and can never be mutated. It is also no longer owned by anyone. + +Note that the `transfer::freeze_object` function requires you to pass the object by value. If the object allowed passing the object by a mutable reference, you could still mutate the object after the `freeze_object` call. This contradicts the fact that it should have become immutable. + +Alternatively, you can also provide an API that creates an immutable object at creation: + ```rust public entry fun create_immutable(red: u8, green: u8, blue: u8, ctx: &mut TxContext) { let color_object = new(red, green, blue, ctx); transfer::freeze_object(color_object) } ``` -In this function, a fresh new `ColorObject` is created and immediately turned into an immutable object before being owned by anyone. + +This function creates a new `ColorObject` and immediately makes it immutable before it has an owner. ### Use immutable object -Once an object becomes immutable, the rules of who could use this object in Move calls change: -1. An immutable object can be passed only as a read-only, immutable reference to Move entry functions as `&T`. + +Once an object becomes immutable, the rules of who can use this object in Sui Move calls change: +1. An immutable object can be passed only as a read-only, immutable reference to Sui Move entry functions as `&T`. 2. Anyone can use immutable objects. -Recall that we defined a function that copies the value of one object to another: +In a preceding section, you defined a function that copies the value of one object to another: + ```rust public entry fun copy_into(from_object: &ColorObject, into_object: &mut ColorObject); ``` + In this function, anyone can pass an immutable object as the first argument `from_object`, but not the second argument. -Since immutable objects can never be mutated, there will never be a data race even when multiple transactions are using the same immutable object at the same time. Hence, the existence of immutable objects does not pose any requirement on consensus. +Since immutable objects can never be mutated, there's no data race, even when multiple transactions are using the same immutable object at the same time. Hence, the existence of immutable objects does not pose any requirement on consensus. ### Test immutable object -Let's take a look at how we interact with immutable objects in unit tests. -Previously, we used the `test_scenario::take_from_sender` API to take an object from the global storage that's owned by the sender of the transaction in a unit test. And `take_from_sender` returns an object by value, which allows you to mutate, delete or transfer it. +Previously, you used the `test_scenario::take_from_sender` function to take an object from the global storage that's owned by the sender of the transaction in a unit test. And `take_from_sender` returns an object by value, which allows you to mutate, delete, or transfer it. + +To take an immutable object, use a new function: `test_scenario::take_immutable`. This is required because you can access immutable objects only through read-only references. The `test_scenario` runtime keeps track of the usage of this immutable object. If the compiler does not return the object via `test_scenario::return_immutable` before the start of the next transaction, the test stops. -To take an immutable object, we will need to use a new API: `test_scenario::take_immutable`. This is required because immutable objects can be accessed only through read-only references. The `test_scenario` runtime will keep track of the usage of this immutable object. If the object is not returned via `test_scenario::return_immutable` before the start of the next transaction, the test will abort. +To see it work in action, (`ColorObjectTests::test_immutable`): -Let's see it work in action (`ColorObjectTests::test_immutable`): ```rust let sender1 = @0x1; let scenario_val = test_scenario::begin(sender1); @@ -67,8 +77,11 @@ test_scenario::next_tx(scenario, sender1); assert!(!test_scenario::has_most_recent_for_sender(scenario), 0); }; ``` -In this test, we submit a transaction as `sender1`, which would create an immutable object. -As we can see above, `can_take_owned` will no longer return `true`, because the object is no longer owned. To take this object, we need to: + +This test submits a transaction as `sender1`, which tries to create an immutable object. + +The `can_take_owned` function no longer returns `true`, because the object is no longer owned. To take this object: + ```rust // Any sender can work. let sender2 = @0x2; @@ -80,9 +93,11 @@ test_scenario::next_tx(scenario, sender2); test_scenario::return_immutable(object); }; ``` - To show that this object is indeed not owned by anyone, we start the next transaction with `sender2`. As explained earlier, we used `take_immutable`, and it succeeded! This means that any sender will be able to take an immutable object. In the end, to return the object, we also need to call a new API: `return_immutable`. -In order to examine if this object is indeed immutable, let's introduce a function that would mutate a `ColorObject` (we will use this function when describing [on-chain interactions](#on-chain-interactions)): +To show that this object is indeed not owned by anyone, start the next transaction with `sender2`. Note that it used `take_immutable` and succeeded. This means that any sender can take an immutable object. To return the object, call a new function: `return_immutable`. + +To examine whether this object is immutable, add a function that tries to mutate a `ColorObject`: + ```rust public entry fun update( object: &mut ColorObject, @@ -93,48 +108,67 @@ public entry fun update( object.blue = blue; } ``` -To summarize, we introduced two new API functions to interact with immutable objects in unit tests: + +This section introduced two new functions to interact with immutable objects in unit tests: - `test_scenario::take_immutable` to take an immutable object wrapper from global storage. - `test_scenario::return_immutable` to return the wrapper back to the global storage. ### On-chain interactions -First of all, take a look at the current list of objects you own: -``` + +First, view the objects you own: + +```shell $ export ADDR=`sui client active-address` $ sui client objects $ADDR ``` -Let's publish the `ColorObject` code on-chain using the Sui CLI client: -``` -$ sui client publish $ROOT/sui_programmability/examples/objects_tutorial --gas-budget 10000 -``` -Set the package object ID to the `$PACKAGE` environment variable as we did in previous chapters. +Publish the `ColorObject` code on-chain using the Sui Client CLI: -Then create a new `ColorObject`: +```shell +sui client publish $ROOT/sui_programmability/examples/objects_tutorial --gas-budget 10000 ``` -$ sui client call --gas-budget 1000 --package $PACKAGE --module "color_object" --function "create" --args 0 255 0 -``` -Set the newly created object ID to `$OBJECT`. If we look at the list of objects in the current active address: + +Set the package object ID to the `$PACKAGE` environment variable as described in previous chapters. Then create a new `ColorObject`: + +```shell +sui client call --gas-budget 1000 --package $PACKAGE --module "color_object" --function "create" --args 0 255 0 ``` + +Set the newly created object ID to `$OBJECT`. To view the objects in the current active address: + +```shell $ sui client objects $ADDR ``` -There should be one more, with ID `$OBJECT`. Let's turn it into an immutable object: -``` + +You should see an object with the ID you used for `$OBJECT`. To turn it into an immutable object: + +```shell $ sui client call --gas-budget 1000 --package $PACKAGE --module "color_object" --function "freeze_object" --args \"$OBJECT\" ``` -Now let's look at the list of objects we own again: -``` + +View the list of objects again: + +```shell $ sui client objects $ADDR ``` -`$OBJECT` is no longer there. It's no longer owned by anyone. You can see that it's now immutable by querying the object information: -``` + +`$OBJECT` is no longer listed. It's no longer owned by anyone. You can see that it's now immutable by querying the object information: + +```shell $ sui client object $OBJECT +``` + +The response includes: + +``` Owner: Immutable -... ``` -If we try to mutate it: + +If you try to mutate it: + ``` $ sui client call --gas-budget 1000 --package $PACKAGE --module "color_object" --function "update" --args \"$OBJECT\" 0 0 0 ``` -It will complain that an immutable object cannot be passed to a mutable argument. + +The response indicates that you can't pass an immutable object to a mutable argument. \ No newline at end of file diff --git a/doc/src/build/programming-with-objects/ch4-object-wrapping.md b/doc/src/build/programming-with-objects/ch4-object-wrapping.md index 60a955c3a92cf..eacd5a3c566ad 100644 --- a/doc/src/build/programming-with-objects/ch4-object-wrapping.md +++ b/doc/src/build/programming-with-objects/ch4-object-wrapping.md @@ -2,7 +2,7 @@ title: Chapter 4 - Object Wrapping --- -In many programming languages, we organize data structures in layers by nesting complex data structures in another data structure. In Move, you may do the same by putting a field of `struct` type in another, like the following: +In many programming languages, you organize data structures in layers by nesting complex data structures in another data structure. In Sui Move, you can organize data structures by putting a field of `struct` type in another, like the following: ```rust struct Foo has key { @@ -15,10 +15,11 @@ struct Bar has store { } ``` -> :book: For a struct type to be capable of being embedded in a Sui object struct (which will have `key` ability), the embedded struct type must have the `store` ability. +To embed a struct type in a Sui object struct (with a `key` ability), the struct type must have the `store` ability. -In the above example, `Bar` is a normal Move struct, but it is not a Sui object, since it doesn't have the `key` ability. This is common usage when we just need to organize data with good encapsulation. -In some cases, however, we want to put a Sui object struct type as a field in another Sui object struct type. In the above example, we may change `Bar` into: +In the preceding example, `Bar` is a normal struct, but it is not a Sui object since it doesn't have the `key` ability. This is common usage to organize data with good encapsulation. + +To put a Sui object struct type as a field in another Sui object struct type, change `Bar` into: ```rust struct Bar has key, store { @@ -27,25 +28,25 @@ struct Bar has key, store { } ``` -Now `Bar` is also a Sui object type. When we put a Sui object of type `Bar` into a Sui object of type `Foo`, the Sui object of type `Bar` is said to be **wrapped** by `Foo` (which we call the **wrapper** object or the **wrapping** object). +Now `Bar` is also a Sui object type. If you put a Sui object of type `Bar` into a Sui object of type `Foo`, the object type `Foo` wraps the object type `Bar`. The object type `Foo` is the wrapper or wrapping object. -> :bulb: In Move code, it is also possible to put a Sui object as a field of a non-Sui object struct type. For example, in the above code sample, we can define `Foo` to not have `key` but `Bar` to have `key, store`. However, this case can happen only temporarily in the middle of a Move execution and cannot be persisted on-chain. This is because a non-Sui object cannot flow across the Move-Sui boundary, and one must unpack the non-Sui object at some point and deal with the Sui object fields in it. +In Sui Move code, you can put a Sui object as a field of a non-Sui object struct type. For example, the preceding code sample defined `Foo` to not have `key`, but `Bar` to have `key, store`. This case can happen only temporarily in the middle of a Sui Move execution, and cannot persist on-chain. This is because a non-Sui object cannot flow across the Move-Sui boundary, and one must unpack the non-Sui object at some point and handle the Sui object fields in it. -There are some interesting consequences of wrapping a Sui object into another. When an object is wrapped, this object no longer exists independently on-chain. We will no longer be able to look up this object by its ID. This object becomes part of the data of the object that wraps it. Most importantly, _we can no longer pass the wrapped object as an argument in any way in Move calls_. The only access point is through the wrapping object. +There are some interesting consequences of wrapping a Sui object into another. When an object is wrapped, the object no longer exists independently on-chain. You can no longer look up the object by its ID. The object becomes part of the data of the object that wraps it. Most importantly, you can no longer pass the wrapped object as an argument in any way in Sui Move calls. The only access point is through the wrapping object. -> :bulb: The fact that you can no longer use a wrapped Sui object means that it's impossible to create circular wrapping behavior, where A wraps B, B wraps C, and C also wraps A. +It is not possible to create circular wrapping behavior, where A wraps B, B wraps C, and C also wraps A. -At some point, you can then take out the wrapped object and transfer it to an address. This process is called **unwrapping**. When an object is **unwrapped**, it will become an independent object again, and can be accessed directly on-chain. There is also an important property about wrapping and unwrapping: _the object's ID stays the same across wrapping and unwrapping_! +At some point, you can then take out the wrapped object and transfer it to an address. This is called **unwrapping**. When an object is **unwrapped**, it becomes an independent object again, and can be accessed directly on-chain. There is also an important property about wrapping and unwrapping: the object's ID stays the same across wrapping and unwrapping. -There are a few common ways to wrap a Sui object into another Sui object, and their use cases are typically different. In the following, we will walk through three different ways to wrap a Sui object and their typical use cases. +There are a few common ways to wrap a Sui object into another Sui object, and their use cases are typically different. This section describes three different ways to wrap a Sui object with typical use cases. ### Direct wrapping -When we put a Sui object type directly as a field in another Sui object type (just like how we put `Bar` as field `bar` in `Foo`), it is called _direct wrapping_. The most important property achieved through direct wrapping is the following: _The wrapped object cannot be unwrapped unless we destroy the wrapping object._ In the example above, in order to make `bar` a standalone object again, one has to delete (and hence [unpack](https://move-book.com/advanced-topics/struct.html#destructing-structures)) the `Foo` object. Because of this property, direct wrapping is the best way to implement _object locking_: lock an object with constrained access, and one can unlock it only through specific contract calls. +If you put a Sui object type directly as a field in another Sui object type (as in the preceding example), it is called _direct wrapping_. The most important property achieved through direct wrapping is that the wrapped object cannot be unwrapped unless the wrapping object is destroyed. In the preceding example, to make `Bar` a standalone object again, delete (and hence [unpack](https://move-book.com/advanced-topics/struct.html#destructing-structures)) the `Foo` object. Direct wrapping is the best way to implement object locking, which is to lock an object with constrained access. You can unlock it only through specific contract calls. -Let's walk through an example implementation of a trusted swap to demonstrate how to use direct wrapping. Let's say there is an NFT-style `Object` type that has `scarcity` and `style`. `scarcity` determines how rare the object is (presumably the more scarce the higher its market value); `style` determines the object content/type or how it's rendered. Let's say you own some of these objects and want to trade your objects with others. But to make sure it's a fair trade, you are willing to trade an object only with another one that has identical `scarcity` but different `style` (so that you can collect more styles). +The following example implementation of a trusted swap demonstrates how to use direct wrapping. Assume there is an NFT-style `Object` type that has `scarcity` and `style`. In this example, `scarcity` determines how rare the object is (presumably the more scarce the higher its market value), and `style` determines the object content/type or how it's rendered. If you own some of these objects and want to trade your objects with others, you want to make sure it's a fair trade. You are willing to trade an object only with another one that has identical `scarcity`, but want a different `style` (so that you can collect more styles). -First of all, let's define such an object type: +First, define such an object type: ```rust struct Object has key, store { @@ -55,7 +56,7 @@ struct Object has key, store { } ``` -In a real application, we probably would make sure that there is a limited supply of the objects and there is a mechanism to mint them to a list of owners. For simplicity and demonstration purposes, here we will just make it straightforward to create: +In a real application, you might make sure that there is a limited supply of the objects, and there is a mechanism to mint them to a list of owners. For simplicity and demonstration purposes, this example simplifies creation: ```rust public entry fun create_object(scarcity: u8, style: u8, ctx: &mut TxContext) { @@ -68,7 +69,7 @@ public entry fun create_object(scarcity: u8, style: u8, ctx: &mut TxContext) { } ``` -Anyone can call `create_object` to create a new object with specified `scarcity` and `style`. The created object will be sent to the signer of the transaction. We will likely also want to be able to transfer the object to others: +Anyone can call `create_object` to create a new object with specified `scarcity` and `style`. The created object is sent to the signer of the transaction. To enable transferring the object to others: ```rust public entry fun transfer_object(object: Object, recipient: address) { @@ -76,13 +77,13 @@ public entry fun transfer_object(object: Object, recipient: address) { } ``` -Now let's look at how we could enable a swap/trade between your object and others' objects. A straightforward idea is this: define a function that takes two objects from two addresses and swaps their ownership. But this doesn't work in Sui! Recall from [chapter 2](ch2-using-objects.md) that only object owners can send a transaction to mutate the object. So one person cannot send a transaction that would swap their own object with someone else's object. +You can also enable a swap/trade between your object and others' objects. For example, define a function that takes two objects from two addresses and swaps their ownership. But this doesn't work in Sui! Recall from [Using Objects](ch2-using-objects.md) that only object owners can send a transaction to mutate the object. So one person cannot send a transaction that would swap their own object with someone else's object. -In the future, we will likely introduce multi-sig transactions so that two people can sign the same transaction for this type of use case. However, you may not always be able to find someone to swap with right away. A multi-sig transaction won't work in this scenario. Even if you can, you may not want to carry the burden of finding a swap target. +Sui supports multi-signature (multi-sig) transactions so that two people can sign the same transaction for this type of use case. But a multi-sig transaction doesn't work in this scenario. -Another common solution is to "send" your object to a pool (e.g. a marketplace in the case of NFT, or a liquidity pool in the case of tokens), and perform the swap in the pool (either right away, or later when there is demand). In future chapters, we will explore the concept of shared objects that can be mutated by anyone and show that how it enables anyone to operate in a shared object pool. In this chapter, we will focus on how to achieve the same effect using owned objects. Transactions using only owned objects are faster and cheaper (in terms of gas) than using shared objects, since they do not require consensus in Sui. +Another common solution is to send your object to a pool - such as an NFT marketplace or a staking pool - and perform the swap in the pool (either right away, or later when there is demand). Other chapters explore the concept of shared objects that can be mutated by anyone, and show that how it enables anyone to operate in a shared object pool. This chapter focuses on how to achieve the same effect using owned objects. Transactions using only owned objects are faster and less expensive (in terms of gas) than using shared objects, since they do not require consensus in Sui. -To be able to perform a swap of objects, both objects must be owned by the same address. We can imagine that a third party builds infrastructure to provide swap services. Anyone who wants to swap their object can send their objects to the third party, and the third party will help perform the swap and send the objects back. But we don't fully trust the third party and don't want to give them full custody of our objects. To achieve this, we can use direct wrapping. We define a wrapper object type as following: +To swap objects, the same address must own both objects. Anyone who wants to swap their object can send their objects to the third party, such as a site that offers swapping services, and the third party helps perform the swap and send the objects to the appropriate owner. To ensure that you retain custody of your objects (such as coins and NFTs) and not give full custody to the third party, use direct wrapping. To define a wrapper object type: ```rust struct ObjectWrapper has key { @@ -93,7 +94,7 @@ struct ObjectWrapper has key { } ``` -`ObjectWrapper` defines a Sui object type, wraps the object that we want to swap as `to_swap`, and tracks the original owner of the object in `original_owner`. To make this more interesting and realistic, we can also expect that we may need to pay the third party some fee for this swap. Below we define an interface to request a swap by someone who owns an `Object`: +`ObjectWrapper` defines a Sui object type, wraps the object to swap as `to_swap`, and tracks the original owner of the object in `original_owner`. You might need to also pay the third party some fee for this swap. To define an interface to request a swap by someone who owns an `Object`: ```rust public entry fun request_swap(object: Object, fee: Coin, service_address: address, ctx: &mut TxContext) { @@ -108,26 +109,28 @@ public entry fun request_swap(object: Object, fee: Coin, service_address: a } ``` -In the above entry function, to request swapping an `object`, one must pass the object by value so that it's fully consumed and wrapped into `ObjectWrapper`. A fee (in the type of `Coin`) is also provided. The function also checks that the fee is sufficient. Note that we turn `Coin` into `Balance` when putting it into the `wrapper` object. This is because `Coin` is a Sui object type and used only to pass around as Sui objects (e.g. as entry function arguments or objects sent to addresses). For coin balances that need to be embedded in another Sui object struct, we use `Balance` instead because it's not a Sui object type and hence is much cheaper to use. -The wrapper object is then sent to the service operator, whose address is also specified in the call as `service_address`. +In the preceding entry function, you must pass the object by value so that it's fully consumed and wrapped into `ObjectWrapper`to request swapping an `object`. The example also provides a fee (in the type of `Coin`). The function also checks that the fee is sufficient. The example turns `Coin` into `Balance` when it's put into the `wrapper` object. This is because `Coin` is a Sui object type and used only to pass around as Sui objects (such as entry function arguments or objects sent to addresses). For coin balances that need to be embedded in another Sui object struct, use `Balance` instead because it's not a Sui object type and is much less expensive to use. + +The wrapper object is then sent to the service operator, with the address specified in the call as `service_address`. -Although the service operator (`service_address`) now owns the `ObjectWrapper`, which contains the object to be swapped, the service operator still cannot access or steal the underlying wrapped `Object`. This is because the `transfer_object` function we defined requires the caller to pass an `Object` into it; but the service operator cannot access the wrapped `Object`, and passing `ObjectWrapper` to the `transfer_object` function would be invalid. Recall that an object can be read or modified only by the module in which it is defined; because this module defines only a wrapping / packing function (`request_swap`), and not an unwrapping / unpacking function, the service operator has no way to unpack the `ObjectWrapper` to retrieve the wrapped `Object`. Furthermore, `ObjectWrapper` itself lacks any defined transfer method, so the service operator cannot transfer the wrapped object to someone else either. +Although the service operator (`service_address`) now owns the `ObjectWrapper`, which contains the object to be swapped, the service operator still cannot access or steal the underlying wrapped `Object`. This is because the `transfer_object` function we defined requires the caller to pass an `Object` into it; but the service operator cannot access the wrapped `Object`, and passing `ObjectWrapper` to the `transfer_object` function would be invalid. Recall that an object can be read or modified only by the module in which it is defined; because this module defines only a wrapping / packing function (`request_swap`), and not an unwrapping / unpacking function, the service operator has no way to unpack the `ObjectWrapper` to retrieve the wrapped `Object`. Also, `ObjectWrapper` itself lacks any defined transfer method, so the service operator cannot transfer the wrapped object to someone else either. -Finally, let's define the function that the service operator can call in order to perform a swap between two objects sent from two addresses. The function interface will resemble: +The function interface for the function that the service operator can call to perform a swap between two objects sent from two addresses resembles: ```rust public entry fun execute_swap(wrapper1: ObjectWrapper, wrapper2: ObjectWrapper, ctx: &mut TxContext); ``` -Where `wrapper1` and `wrapper2` are two wrapped objects that were sent from different object owners to the service operator. (Hence, the service operator owns both.) Both wrapped objects are passed by value because they will eventually need to be [unpacked](https://move-book.com/advanced-topics/struct.html#destructing-structures). -We first check that the swap is indeed legit: +Where `wrapper1` and `wrapper2` are two wrapped objects that were sent from different object owners to the service operator. Both wrapped objects are passed by value because they eventually need to be [unpacked](https://move-book.com/advanced-topics/struct.html#destructing-structures). + +First, check that the swap is legitimate: ```rust assert!(wrapper1.to_swap.scarcity == wrapper2.to_swap.scarcity, 0); assert!(wrapper1.to_swap.style != wrapper2.to_swap.style, 0); ``` -It checks that the two objects have identical scarcity, but have different style, perfect pair for a swap. Next we unpack the two objects to obtain the inner fields. By doing so, we unwrap the objects: +It checks that the two objects have identical scarcity, but have different styles. This meets your criteria for a swap. Unpacking the two objects to obtain the inner fields, also unwraps the objects: ```rust let ObjectWrapper { @@ -145,14 +148,14 @@ let ObjectWrapper { } = wrapper2; ``` -We now have all the things we need for the actual swap: +To perform the actual swap: ```rust transfer::transfer(object1, original_owner2); transfer::transfer(object2, original_owner1); ``` -The above code does the swap: it sends `object1` to the original owner of `object2`, and sends `object1` to the original owner of `object2`. The service provider is also happy to take the fee: +The preceding code sends `object1` to the original owner of `object2`, and sends `object2` to the original owner of `object1`. The service provider also takes a fee: ```rust let service_address = tx_context::sender(ctx); @@ -160,26 +163,26 @@ balance::join(&mut fee1, fee2); transfer::transfer(coin::from_balance(fee1, ctx), service_address); ``` -`fee2` is merged into `fee1`, turned into a `Coin` and sent to the `service_address`. Finally, we signal Sui that we have deleted both wrapper objects: +The `fee2` is merged into `fee1`, turned into a `Coin` and sent to the `service_address`. Finally, delete both wrapped objects: ```rust object::delete(id1); object::delete(id2); ``` -At the end of this call, the two objects have been swapped (sent to the opposite owner) and the service provider takes the service fee. +After this call, the two objects are swapped and the service provider takes the service fee. Since the contract defined only one way to deal with `ObjectWrapper` - `execute_swap` - there is no other way the service operator can interact with `ObjectWrapper` despite its ownership. -The full source code can be found in [trusted_swap.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/objects_tutorial/sources/trusted_swap.move). +Find the full source code in [trusted_swap.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/objects_tutorial/sources/trusted_swap.move). -A more complex example of using direct wrapping can be found in [escrow.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/defi/sources/escrow.move). +To view a more complex example of how to use direct wrapping, see [escrow.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/defi/sources/escrow.move). ### Wrapping through `Option` -When Sui object type `Bar` is directly wrapped into `Foo`, there is not much flexibility: a `Foo` object must have a `Bar` object in it, and in order to take out the `Bar` object one must destroy the `Foo` object. However, there are cases where we want more flexibility: the wrapping type may or may not always have the wrapped object in it, and the wrapped object may be replaced with a different object at some point. +When Sui object type `Bar` is directly wrapped into `Foo`, there is not much flexibility: a `Foo` object must have a `Bar` object in it, and to take out the `Bar` object you must destroy the `Foo` object. However, for more flexibility, the wrapping type might not always have the wrapped object in it, and the wrapped object might be replaced with a different object at some point. -Let's demonstrate this use case by designing a simple game character: A warrior with a sword and shield. A warrior may or may not have a sword and shield, and they should be able to replace them anytime. To design this, we define a `SimpleWarrior` type as following: +To demonstrate this use case, design a simple game character: A warrior with a sword and shield. A warrior might have a sword and shield, or might not have either. The warrior should be able to add a sword and shield, and replace the current ones at any time. To design this, define a `SimpleWarrior` type: ```rust struct SimpleWarrior has key { @@ -203,7 +206,7 @@ struct Shield has key, store { } ``` -When we are creating a new warrior, we can set the `sword` and `shield` to `none` to indicate there is no equipment yet: +When you create a new warrior, set the `sword` and `shield` to `none` to indicate there is no equipment yet: ```rust public entry fun create_warrior(ctx: &mut TxContext) { @@ -216,7 +219,7 @@ public entry fun create_warrior(ctx: &mut TxContext) { } ``` -With this, we can then define functions to equip new swords or new shields: +You can then define functions to equip new swords or new shields: ```rust public entry fun equip_sword(warrior: &mut SimpleWarrior, sword: Sword, ctx: &mut TxContext) { @@ -228,19 +231,19 @@ public entry fun equip_sword(warrior: &mut SimpleWarrior, sword: Sword, ctx: &mu } ``` -In the above function, we are passing a `warrior` as mutable reference of `SimpleWarrior`, and a `sword` passed by value because we need to wrap it into the `warrior`. +The function in the preceding example passes a `warrior` as a mutable reference of `SimpleWarrior`, and passes a `sword` by value to wrap it into the `warrior`. -It is important to note that because `Sword` is a Sui object type without `drop` ability, if the warrior already has a sword equipped, that sword cannot just be dropped. If we make a call to `option::fill` without first checking and taking out the existing sword, a runtime error may occur. Hence in `equip_sword`, we first check if there is already a sword equipped, and if so, we take it out and send it back to the sender. This matches what you would expect when you equip a new sword--you get the old sword back, if there is one. +Note that because `Sword` is a Sui object type without `drop` ability, if the warrior already has a sword equipped, the warrior can't drop that sword. If you call `option::fill` without first checking and taking out the existing sword, an error occurs. In `equip_sword`, first check whether there is already a sword equipped. If so, remove it out and send it back to the sender. To a player, this returns an equipped sword to their inventory when they equip the new sword. -Full code can be found in [simple_warrior.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/objects_tutorial/sources/simple_warrior.move). +Find the source code in [simple_warrior.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/objects_tutorial/sources/simple_warrior.move). -You can also find a more complex example in [hero.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/games/sources/hero.move). +To view a more complex example, see [hero.move](https://github.com/MystenLabs/sui/blob/main/sui_programmability/examples/games/sources/hero.move). ### Wrapping through `vector` -The concept of wrapping objects in a vector field of another Sui object is very similar to wrapping through `Option`: an object may contain 0, 1 or many of the wrapped objects of the same type. +The concept of wrapping objects in a vector field of another Sui object is very similar to wrapping through `Option`: an object can contain 0, 1, or many of the wrapped objects of the same type. -We won't use a full example to demonstrate this use case, but wrapping through vector may resemble: +Wrapping through vector resembles: ```rust struct Pet has key, store { @@ -254,4 +257,4 @@ struct Farm has key { } ``` -In the above example, a vector of `Pet`s are wrapped in `Farm` and can be accessed only through the `Farm` object. +The preceding example wraps a vector of `Pet` in `Farm`, and can be accessed only through the `Farm` object. \ No newline at end of file diff --git a/doc/src/build/programming-with-objects/ch5-dynamic-fields.md b/doc/src/build/programming-with-objects/ch5-dynamic-fields.md index 06ad6bcaf8eff..01b30c55b1708 100644 --- a/doc/src/build/programming-with-objects/ch5-dynamic-fields.md +++ b/doc/src/build/programming-with-objects/ch5-dynamic-fields.md @@ -2,17 +2,17 @@ title: Chapter 5 - Dynamic Fields --- -In previous chapters, we walked through various ways to use object fields to store primitive data and other objects (wrapping), but there are a few limitations to this approach: +Previous chapters describe various ways to use object fields to store primitive data and other objects (wrapping), but there are a few limitations to this approach: 1. Object's have a finite set of fields keyed by identifiers that are fixed when its module is published (i.e. limited to the fields in the `struct` declaration). 2. An object can become very large if it wraps several other objects. Larger objects can lead to higher gas fees in transactions. In addition, there is an upper bound on object size. -3. As we will see in future chapters, there will be use cases where we need to store a collection of objects of heterogeneous types. Since the Move `vector` type must be instantiated with one single type `T`, it is not suitable for this. +3. Later chapters include use cases where you need to store a collection of objects of heterogeneous types. Since the Sui Move `vector` type must be instantiated with one single type `T`, it is not suitable for this. Fortunately, Sui provides *dynamic fields* with arbitrary names (not just identifiers), added and removed on-the-fly (not fixed at publish), which only affect gas when they are accessed, and can store heterogeneous values. This chapter introduces the libraries for interacting with this kind of field. ### Current Limitations -There are some aspects of dynamic fields that are not yet behaving as designed in this early release. We are actively working on these areas, but watch out for: -- Potential durability/consistency issues with dynamic field objects: When a validator goes down and comes back up while processing a transaction with dynamic fields, it might be unable to process further transactions involving those objects. + +There are some aspects of dynamic fields that are not yet behaving as designed in this early release. Watch for potential durability/consistency issues with dynamic field objects. For example, if a validator goes offline and then reconnects while processing a transaction with dynamic fields, it might be unable to process further transactions involving those objects. ### Fields vs Object Fields @@ -57,7 +57,7 @@ public fun add( These functions add a field with name `name` and value `value` to `object`. To see it in action, consider these code snippets: -First we define two object types for the parent and the child: +First, define two object types for the parent and the child: ```rust struct Parent has key { @@ -70,7 +70,7 @@ struct Child has key, store { } ``` -Now, we can define an API to add a `Child` object as a dynamic field of a `Parent` object: +Next, define an API to add a `Child` object as a dynamic field of a `Parent` object: ```rust use sui::dynamic_object_field as ofield; @@ -80,12 +80,12 @@ public entry fun add_child(parent: &mut Parent, child: Child) { } ``` -This function takes the `Child` object by value, and makes it a dynamic field of `parent` with name `b"child"` (a byte string of type `vector`). At the end of the `add_child` call, we have the following ownership relationship: +This function takes the `Child` object by value and makes it a dynamic field of `parent` with name `b"child"` (a byte string of type `vector`). This call results in the following ownership relationship: 1. Sender address (still) owns the `Parent` object. 2. The `Parent` object owns the `Child` object, and can refer to it by the name `b"child"`. -> :warning: It is an error to overwrite a field (attempt to add a field with the same Name type and value as one that is already defined), and a transaction that does this will abort. Fields can be modified in-place by borrowing them mutably and can be overwritten safely (e.g. to change its value type) by removing the old value first (see below for details). +It is an error to overwrite a field (attempt to add a field with the same Name type and value as one that is already defined), and a transaction that does this will fail. Fields can be modified in-place by borrowing them mutably and can be overwritten safely (such as to change its value type) by removing the old value first. ### Accessing Dynamic Fields @@ -109,9 +109,9 @@ public fun borrow_mut( Where `object` is the UID of the object the field is defined on and `name` is the field's name. -> :bulb: `sui::dynamic_object_field` has equivalent functions for object fields, but with the added constraint `Value: key + store`. +**Note:** `sui::dynamic_object_field` has equivalent functions for object fields, but with the added constraint `Value: key + store`. -Let's look at how to use these APIs with the `Parent` and `Child` types defined earlier: +To use these APIs with the `Parent` and `Child` types defined earlier: ```rust use sui::dynamic_object_field as ofield; @@ -132,15 +132,15 @@ The first function accepts a mutable reference to the `Child` object directly, a The second function accepts a mutable reference to the `Parent` object and accesses its dynamic field using `borrow_mut`, to pass to `mutate_child`. This can only be called on `Parent` objects that have a `b"child"` field defined. A `Child` object that has been added to a `Parent` *must* be accessed via its dynamic field, so it can only by mutated using `mutate_child_via_parent`, not `mutate_child`, even if its ID is known. -> :warning: A transaction that attempts to borrow a field that does not exist will abort. +**Important:** A transaction that attempts to borrow a field that does not exist will fail. -> :warning: The `Value` type passed to `borrow` and `borrow_mut` must match the type of the stored field, or the transaction will abort. +The `Value` type passed to `borrow` and `borrow_mut` must match the type of the stored field, or the transaction will abort. -> :warning: Dynamic object field values *must* be accessed through these APIs. A transaction that attempts to use those objects as inputs (by value or by reference), will be rejected for having invalid inputs. +Dynamic object field values *must* be accessed through these APIs. A transaction that attempts to use those objects as inputs (by value or by reference), will be rejected for having invalid inputs. ### Removing a Dynamic Field -Similar to "unwrapping" an object held in a regular field, a dynamic field can be removed, exposing its value: +Similar to unwrapping, an object held in a regular field, a dynamic field can be removed, exposing its value: ```rust module sui::dynamic_field { @@ -157,7 +157,7 @@ This function takes a mutable reference to the ID of the `object` the field is d > :bulb: `sui::dynamic_object_field` has an equivalent function for object fields. -The value that is returned can be interacted with just like any other value (because it is any other value). For example, removed dynamic object field values can then be `delete`-d or `transfer`-ed to an address (e.g. back to the sender): +The value that is returned can be interacted with just like any other value (because it is any other value). For example, removed dynamic object field values can then be `delete`-d or `transfer`-ed to an address (back to the sender): ```rust use sui::dynamic_object_field as ofield; @@ -183,10 +183,10 @@ public entry fun reclaim_child(parent: &mut Parent, ctx: &mut TxContext) { } ``` -> :warning: Like with borrowing a field, a transaction that attempts to remove a non-existent field, or a field with a different `Value` type will abort. +Similar to borrowing a field, a transaction that attempts to remove a non-existent field, or a field with a different `Value` type, fails. ### Deleting an Object with Dynamic Fields -It is possible to delete an object that has dynamic fields still defined on it. Because field values can only be accessed via the dynamic field's associated object and field name, deleting an object that has dynamic fields still defined on it renders them all inaccessible to future transactions. This is true regardless of whether the field's value has the `drop` ability. +It is possible to delete an object that has dynamic fields still defined on it. Because field values can be accessed only via the dynamic field's associated object and field name, deleting an object that has dynamic fields still defined on it renders them all inaccessible to future transactions. This is true regardless of whether the field's value has the `drop` ability. -> :warning: Deleting an object that has dynamic fields still defined on it is permitted, but it will render all its fields inaccessible. +Deleting an object that has dynamic fields still defined on it is permitted, but it will render all its fields inaccessible. diff --git a/doc/src/build/programming-with-objects/ch6-collections.md b/doc/src/build/programming-with-objects/ch6-collections.md index 85eb58fe47cab..25527fcf3e48f 100644 --- a/doc/src/build/programming-with-objects/ch6-collections.md +++ b/doc/src/build/programming-with-objects/ch6-collections.md @@ -2,11 +2,11 @@ title: Chapter 6 - Collections --- -The last chapter introduces a way to extend existing objects with *dynamic fields*, but noted that it's possible to delete an object that still has (potentially non-`drop`) dynamic fields. This may not be a concern when adding a small number of statically known additional fields to an object, but is particularly undesirable for *on-chain collection types* which could be holding unboundedly many key-value pairs as dynamic fields. +The previous chapter, [Dynamic Fields](ch5-dynamic-fields.md), introduced a way to extend existing objects with *dynamic fields*. Note that it's possible to delete an object that still has (potentially non-`drop`) dynamic fields. This might not be a concern when adding a small number of statically known additional fields to an object, but is particularly undesirable for *on-chain collection types* which could be holding unboundedly many key-value pairs as dynamic fields. -This chapter covers two such collections -- `Table` and `Bag` -- built using dynamic fields, but with additional support to count the number of entries they contain, and protect against accidental deletion when non-empty. +This chapter describes two such collections -- `Table` and `Bag` -- built using dynamic fields, but with additional support to count the number of entries they contain, and protect against accidental deletion when non-empty. -The types and function discussed below are built into the Sui framework in modules [`table`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/table.move) and [`bag`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/bag.move). As with dynamic fields, there is also an `object_` variant of both: `ObjectTable` in [`object_table`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object_table.move) and `ObjectBag` in [`object_bag`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object_bag.move). The relationship between `Table` and `ObjectTable`, and `Bag` and `ObjectBag` are the same as between a field and an object field: The former can hold any `store` type as a value, but objects stored as values will be hidden when viewed from external storage. The latter can only store objects as values, but keeps those objects visible at their ID in external storage. +The types and function discussed in this section are built into the Sui framework in modules [`table`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/table.move) and [`bag`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/bag.move). As with dynamic fields, there is also an `object_` variant of both: `ObjectTable` in [`object_table`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object_table.move) and `ObjectBag` in [`object_bag`](https://github.com/MystenLabs/sui/blob/main/crates/sui-framework/sources/object_bag.move). The relationship between `Table` and `ObjectTable`, and `Bag` and `ObjectBag` are the same as between a field and an object field: The former can hold any `store` type as a value, but objects stored as values are hidden when viewed from external storage. The latter can only store objects as values, but keeps those objects visible at their ID in external storage. ### Current Limitations @@ -31,7 +31,7 @@ public fun new( `Table` is a *homogeneous* map, meaning that all its keys have the same type as each other (`K`), and all its values have the same type as each other as well (`V`). It is created with `sui::table::new`, which requires access to a `&mut TxContext` because `Table`s are objects themselves, which can be transferred, shared, wrapped, or unwrapped, just like any other object. -> :bulb: See `sui::bag::ObjectTable` for the object-preserving version of `Table`. +See `sui::bag::ObjectTable` for the object-preserving version of `Table`. ### Bags @@ -47,11 +47,11 @@ public fun new(ctx: &mut TxContext): Bag; `Bag` is a *heterogeneous* map, so it can hold key-value pairs of arbitrary types (they don't need to match each other). Note that the `Bag` type does not have any type parameters for this reason. Like `Table`, `Bag` is also an object, so creating one with `sui::bag::new` requires supplying a `&mut TxContext` to generate an ID. -> :bulb: See `sui::bag::ObjectBag` for the object-preserving version of `Bag`. +See `sui::bag::ObjectBag` for the object-preserving version of `Bag`. --- -The following sections explain the collection APIs. `sui::table` will be used as the basis for code examples, with explanations where other modules differ. +The following sections explain the collection APIs. They use `sui::table` as the basis for code examples, with explanations where other modules differ. ### Interacting with Collections @@ -84,11 +84,11 @@ public fun remove( } ``` -These functions, add, read, write, and remove entries from the collection, respectively, and all accept keys by value. `Table` has type parameters for `K` and `V` so it is not possible to call these functions with different instantiations of `K` and `V` on the same instance of `Table`, however `Bag` does not these type parameters, and so does permit calls with different instantiations on the same instance. +These functions add, read, write, and remove entries from the collection, respectively, and all accept keys by value. `Table` has type parameters for `K` and `V` so it is not possible to call these functions with different instantiations of `K` and `V` on the same instance of `Table`, however `Bag` does not have these type parameters, and so does permit calls with different instantiations on the same instance. -> :warning: Like with dynamic fields, it is an error to attempt to overwrite an existing key, or access or remove a non-existent key. +**Note:** Like with dynamic fields, it is an error to attempt to overwrite an existing key, or access or remove a non-existent key. -> :warning: The extra flexibility of `Bag`'s heterogeneity means the type system will not statically prevent attempts to add a value with one type, and then borrow or remove it at another type. This pattern will fail at runtime with an abort, similar to the behavior for dynamic fields. +The extra flexibility of `Bag`'s heterogeneity means the type system doesn't statically prevent attempts to add a value with one type, and then borrow or remove it at another type. This pattern fails at runtime, similar to the behavior for dynamic fields. ### Querying Length @@ -108,7 +108,7 @@ public fun is_empty( } ``` -> :bulb: `Bag` has these APIs, but they are not generic on `K` and `V` because `Bag` does not have these type parameters. +`Bag` has these functions, but they are not generic on `K` and `V` because `Bag` does not have these type parameters. ### Querying for Containment @@ -138,7 +138,7 @@ public fun contains_with_type( } ``` -which tests whether `bag` contains a key-value pair with key `k: K` and some value of type `V`. +This example tests whether `bag` contains a key-value pair with key `k: K` and some value of type `V`. ### Clean-up @@ -154,7 +154,7 @@ public fun destroy_empty( } ``` -This function takes the collection by value. If it contains no entries, it will be deleted, otherwise the call will abort. `sui::table::Table` also has a convenience function, +This function takes the collection by value. If it contains no entries, it is deleted, otherwise the call fails. `sui::table::Table` also has a convenience function: ```rust module sui::table { @@ -166,17 +166,17 @@ public fun drop( } ``` -that can only be called for tables where the value type also has `drop`, which allows it to delete tables whether they are empty or not. +You can call the convenience function only for tables where the value type also has `drop` ability, which allows it to delete tables whether they are empty or not. -> :bulb: Note that `drop` will not be called implicitly on eligible tables, before they go out of scope. It must be called explicitly, but it is guaranteed to succeed at runtime. +Note that `drop` is not called implicitly on eligible tables before they go out of scope. It must be called explicitly, but it is guaranteed to succeed at runtime. -> :bulb: `Bag` and `ObjectBag` cannot support `drop` because they could be holding a variety of types, some of which may have `drop` and some which may not. +`Bag` and `ObjectBag` cannot support `drop` because they could be holding a variety of types, some of which may have `drop` and some which may not. -> :bulb: `ObjectTable` does not support `drop` because its values must be objects, which cannot be drop (because they must contain an `id: UID` field and `UID` does not have `drop`). +`ObjectTable` does not support `drop` because its values must be objects, which cannot be drop (because they must contain an `id: UID` field and `UID` does not have `drop`). ### :warning: Equality -Equality on collections is based on identity, i.e. an instance of a collection type is only considered equal to itself and not to all collections that hold the same entries: +Equality on collections is based on identity, for example, an instance of a collection type is only considered equal to itself and not to all collections that hold the same entries: ```rust let t1 = sui::table::new(ctx); @@ -186,4 +186,4 @@ assert!(&t1 == &t1, 0); assert!(&t1 != &t2, 1); ``` -This is unlikely to be the definition of equality that you want, don't use it! +This is unlikely to be the definition of equality that you want. diff --git a/doc/src/build/sui-object-display.md b/doc/src/build/sui-object-display.md index 4c3eccc1140ff..142e0cf03eb4c 100644 --- a/doc/src/build/sui-object-display.md +++ b/doc/src/build/sui-object-display.md @@ -2,13 +2,13 @@ title: Sui Object Display Standard --- -The Sui Object Display standard is a template engine that allows for off-chain management of display configurations for a type. With it, you can substitute data for an object into a template string. The standard doesn’t limit the fields you can set. You can use the `{property}` syntax to access all object properties, and then insert them as a part of the template string. +The Sui Object Display standard is a template engine that allows for on-chain management of off-chain representation (display) for a type. With it, you can substitute data for an object into a template string. The standard doesn’t limit the fields you can set. You can use the `{property}` syntax to access all object properties, and then insert them as a part of the template string. -You can use the `sui::display` module to define the display properties for a `Publisher` object you own. For more information about `Publisher` objects, see [Publisher](https://examples.sui.io/basics/publisher.html) topic in *Sui Move by Example*. +Use a `Publisher` object that you own to set `sui::display` for a type. For more information about `Publisher` objects, see [Publisher](https://examples.sui.io/basics/publisher.html) topic in *Sui Move by Example*. In Sui Move, `Display` represents an object that specifies a set of named templates for the type `T`. For example, for a type `0x2::capy::Capy` the display syntax is: `Display<0x2::capy::Capy>`. -Sui Full nodes process all objects of the type `T` by matching the `Display` definition, and return the processed result when you query an object. +Sui Full nodes process all objects of the type `T` by matching the `Display` definition, and return the processed result when you query an object with the `{ showDisplay: true }` setting in the query. ## Display properties @@ -134,7 +134,7 @@ module sui::display { public fun add_multiple( self: &mut Display, keys: vector, - values: vector ) { /* ... */ } /// Edit a single field @@ -168,7 +168,7 @@ module capy::utility { /// A capability which grants Capy Manager permission to add /// new genes and manage the Capy Market struct CapyManagerCap has key, store { -id: UID } + id: UID } } ``` @@ -184,8 +184,8 @@ module capy::capy_items { /// A wearable Capy item. For some items there can be an /// unlimited supply. And items with the same name are identical. struct CapyItem has key, store { -id: UID, - name: String + id: UID, + name: String } } ``` @@ -204,8 +204,8 @@ module capy::capy { /// of genes. Created dynamically + for images a dynamic SVG /// generation is used. struct Capy has key, store { -id: UID, - genes: vector + id: UID, + genes: vector } } ``` diff --git a/doc/src/learn/transactions.md b/doc/src/learn/transactions.md index 69e5da28c74e1..869b3a11fd8f3 100644 --- a/doc/src/learn/transactions.md +++ b/doc/src/learn/transactions.md @@ -99,6 +99,10 @@ flowchart LR classDef object-b fill:#ff43 ``` +## Limits on transactions, objects, and data + +Sui has some limits on transactions and data used in transactions, such as a maximum size and number of objects used. To view the full list of limits in source code, see [Transaction input limits](https://github.com/MystenLabs/sui/blob/main/crates/sui-protocol-config/src/lib.rs#L143). + ## Further reading * See the [Move tutorial](move/index.md) to develop Sui smart contracts.