diff --git a/doc/move_code/objects_tutorial/sources/Ch1/ColorObject.move b/doc/move_code/objects_tutorial/sources/Ch1/ColorObject.move deleted file mode 100644 index ab1ab5c67e82b..0000000000000 --- a/doc/move_code/objects_tutorial/sources/Ch1/ColorObject.move +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2022, Mysten Labs, Inc. -// SPDX-License-Identifier: Apache-2.0 - -module Tutorial::ColorObject { - use Sui::ID::VersionedID; - use Sui::Transfer; - use Sui::TxContext::{Self, TxContext}; - - struct ColorObject has key { - id: VersionedID, - red: u8, - green: u8, - blue: u8, - } - - fun new(red: u8, green: u8, blue: u8, ctx: &mut TxContext): ColorObject { - ColorObject { - id: TxContext::new_id(ctx), - red, - green, - blue, - } - } - - public fun create(red: u8, green: u8, blue: u8, ctx: &mut TxContext) { - let color_object = new(red, green, blue, ctx); - Transfer::transfer(color_object, TxContext::sender(ctx)) - } -} diff --git a/doc/move_code/objects_tutorial/sources/Ch1_2/ColorObject.move b/doc/move_code/objects_tutorial/sources/Ch1_2/ColorObject.move new file mode 100644 index 0000000000000..30c0764abadd1 --- /dev/null +++ b/doc/move_code/objects_tutorial/sources/Ch1_2/ColorObject.move @@ -0,0 +1,134 @@ +// Copyright (c) 2022, Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module Tutorial::ColorObject { + use Sui::ID::{Self, VersionedID}; + use Sui::Transfer; + use Sui::TxContext::{Self, TxContext}; + + struct ColorObject has key { + id: VersionedID, + red: u8, + green: u8, + blue: u8, + } + + // == Functions covered in Chapter 1 == + + fun new(red: u8, green: u8, blue: u8, ctx: &mut TxContext): ColorObject { + ColorObject { + id: TxContext::new_id(ctx), + red, + green, + blue, + } + } + + public fun create(red: u8, green: u8, blue: u8, ctx: &mut TxContext) { + let color_object = new(red, green, blue, ctx); + Transfer::transfer(color_object, TxContext::sender(ctx)) + } + + public fun get_color(self: &ColorObject): (u8, u8, u8) { + (self.red, self.green, self.blue) + } + + // == Functions covered in Chapter 2 == + + public fun delete(object: ColorObject, _ctx: &mut TxContext) { + let ColorObject { id, red: _, green: _, blue: _ } = object; + ID::delete(id); + } + + public fun transfer(object: ColorObject, recipient: address, _ctx: &mut TxContext) { + Transfer::transfer(object, recipient) + } +} + +#[test_only] +module Tutorial::ColorObjectTests { + use Sui::TestScenario; + use Tutorial::ColorObject::{Self, ColorObject}; + + // == Tests covered in Chapter 1 == + + #[test] + fun test_create() { + let owner = @0x1; + // Create a ColorObject and transfer it to @owner. + let scenario = &mut TestScenario::begin(&owner); + { + let ctx = TestScenario::ctx(scenario); + ColorObject::create(255, 0, 255, ctx); + }; + // Check that @not_owner does not own the just-created ColorObject. + let not_owner = @0x2; + TestScenario::next_tx(scenario, ¬_owner); + { + assert!(!TestScenario::can_remove_object(scenario), 0); + }; + // Check that @owner indeed owns the just-created ColorObject. + // Also checks the value fields of the object. + TestScenario::next_tx(scenario, &owner); + { + let object = TestScenario::remove_object(scenario); + let (red, green, blue) = ColorObject::get_color(&object); + assert!(red == 255 && green == 0 && blue == 255, 0); + TestScenario::return_object(scenario, object); + }; + } + + // == Tests covered in Chapter 2 == + + #[test] + fun test_delete() { + let owner = @0x1; + // Create a ColorObject and transfer it to @owner. + let scenario = &mut TestScenario::begin(&owner); + { + let ctx = TestScenario::ctx(scenario); + ColorObject::create(255, 0, 255, ctx); + }; + // Delete the ColorObject we just created. + TestScenario::next_tx(scenario, &owner); + { + let object = TestScenario::remove_object(scenario); + let ctx = TestScenario::ctx(scenario); + ColorObject::delete(object, ctx); + }; + // Verify that the object was indeed deleted. + TestScenario::next_tx(scenario, &owner); + { + assert!(!TestScenario::can_remove_object(scenario), 0); + } + } + + #[test] + fun test_transfer() { + let owner = @0x1; + // Create a ColorObject and transfer it to @owner. + let scenario = &mut TestScenario::begin(&owner); + { + let ctx = TestScenario::ctx(scenario); + ColorObject::create(255, 0, 255, ctx); + }; + // Transfer the object to recipient. + let recipient = @0x2; + TestScenario::next_tx(scenario, &owner); + { + let object = TestScenario::remove_object(scenario); + let ctx = TestScenario::ctx(scenario); + ColorObject::transfer(object, recipient, ctx); + }; + // Check that owner no longer owns the object. + TestScenario::next_tx(scenario, &owner); + { + assert!(!TestScenario::can_remove_object(scenario), 0); + }; + // Check that recipient now owns the object. + TestScenario::next_tx(scenario, &recipient); + { + assert!(TestScenario::can_remove_object(scenario), 0); + }; + } +} \ No newline at end of file diff --git a/doc/src/build/programming-with-objects/ch1-object-basics.md b/doc/src/build/programming-with-objects/ch1-object-basics.md index 03c78e7c9fcd1..3111cc8c46834 100644 --- a/doc/src/build/programming-with-objects/ch1-object-basics.md +++ b/doc/src/build/programming-with-objects/ch1-object-basics.md @@ -71,10 +71,78 @@ public fun create(red: u8, green: u8, blue: u8, ctx: &mut TxContext) { ``` > :bulb: Naming convention: Constructors are typically named **`new`**, which returns an instance of the struct type. The **`create`** function is typically defined as an entry function that constructs the struct and transfers it to the desired owner (most commonly the sender). -You can find the full code in [ColorObject.move](../../../move_code/objects_tutorial/sources/Ch1/ColorObject.move). +We can also add a getter to `ColorObject` that returns the color values, so that modules outside of `ColorObject` are able to read their values: +```rust +public fun get_color(self: &ColorObject): (u8, u8, u8) { + (self.red, self.green, self.blue) +} +``` + +You can find the full code [ColorObject.move](../../../move_code/objects_tutorial/sources/Ch1_2/ColorObject.move). + +To compile the code, make sure you have followed the [Sui installation guide](../install.md) so that `sui-move` is in `PATH`. In the code [root directory](../../../move_code/objects_tutorial/) (where `Move.toml` is), you can run: +``` +sui-move build +``` + +### Writing Unit Tests +After defining the `create` function, we want to test this function in Move using unit tests, without having to go all the way to sending Sui transactions. Since Sui manages global storage separately outside of Move, there is no direct way to retrieve objects from global storage within Move. This poses a question: after calling the `create` function, how do we check that the object is properly transferred? + +To assist easy testing in Move, we provide a comprehensive testing framework in the [TestScenario](../../../../sui_programmability/framework/sources/TestScenario.move) module, which allows us to interact with objects that are put into the global storage, so that we can test the behavior of any function directly in Move unit tests. A lot of this is also covered in our [Move testing doc](../move.md#sui-specific-testing). + +The idea of `TestScenario` is to emulate a series of Sui transactions, each sent from a particular address. A developer writing a test starts the first transaction using the `TestScenario::begin` function that takes the address of the user sending this transaction as argument and returns an instance of the `Scenario` struct representing a test scenario. + +An instance of the `Scenario` struct contains a per-address object pool emulating Sui's object storage, with helper functions provided to manipulate objects in the pool. Once the first transaction is finished, subsequent transactions can be started using the `TestScenario::next_tx` function that takes an instance of the `Scenario` struct representing the current scenario and an address of a (new) user as arguments. + +Now let's try to write a test for the `create` function. Tests that need to use `TestScenario` must be in a separate module, either under `tests` directory, or in the same file but in a module annotated with `#[test_only]`. This is because `TestScenario` itself is a test-only module, and can only be used by test-only modules. +First of all, we begin the test with a hardcoded test address, which will also give us a transaction context as if we are sending a transaction from this address. We then call the `create` function, which should create a `ColorObject` and transfer it to the test address: +```rust +let owner = @0x1; +// Create a ColorObject and transfer it to @owner. +let scenario = &mut TestScenario::begin(&owner); +{ + let ctx = TestScenario::ctx(scenario); + ColorObject::create(255, 0, 255, ctx); +}; +``` +>:books: Note there is a "`;`" after "`}`". This is required except for the last statement in the function. Refer to the [Move book](https://move-book.com/syntax-basics/expression-and-scope.html) for detailed explanations. -### On-chain interactions -Now let's try to call `create` in transactions and see what happens. To do this we need to start Sui and the wallet. Follow the [Wallet Quick Start](../wallet.md) to start the Sui network and set up the wallet. +Now account `@0x1` should own the object. Let's first make sure it's not owned by anyone else: +```rust +let not_owner = @0x2; +// Check that @not_owner does not own the just-created ColorObject. +TestScenario::next_tx(scenario, ¬_owner); +{ + assert!(!TestScenario::can_remove_object(scenario), 0); +}; +``` +`TestScenario::next_tx` switches the transaction sender to `@0x2`, which is a new address than the previous one. +`TestScenario::can_remove_object` checks whether an object with the given type actually exists in the global storage owned by the current sender of the transaction. In this code, we assert that we should not be able to remove such an object, because `@0x2` does not own any object. +> :bulb: The second parameter of `assert!` is the error code. In non-test code, we usually define a list of dedicated error code constants for each type of error that could happen in production. For unit tests though, it's usually unnecessary because there will be way too many assetions and the stacktrace upon error is sufficient to tell where the error happened. Hence we recommend just putting `0` there in unit tests for assertions. + +Finally we check that `@0x1` owns the object and the object value is consistent: +```rust +TestScenario::next_tx(scenario, &owner); +{ + let object = TestScenario::remove_object(scenario); + let (red, green, blue) = ColorObject::get_color(&object); + assert!(red == 255 && green == 0 && blue == 255, 0); + TestScenario::return_object(scenario, object); +}; +``` +`TestScenario::remove_object` removes the object of given type from global storage that's owned by the current transaction sender (it also implicitly checks `can_remove_object`). If this line of code succeeds, it means that `owner` indeed owns an object of type `ColorObject`. +We also check that the field values of the object match with what we set in creation. At the end, we must return the object back to the global storage by calling `TestScenario::return_object` so that it's back to the global storage. This also ensures that if any mutations happened to the object during the test, the global storage is aware of the changes. +You may have noticed that `remove_object` picks the object only based on the type parameter. What if there are multiple objects of the same type owned by the account? How do we retrieve each of them? In fact, if you call `remove_object` when there are more than one object of the same type in the same account, an assertion failure will be triggered. We are working on adding an API just for this. Update coming soon. + +Again, you can find the full code [here](../../../move_code/objects_tutorial/sources/Ch1_2/ColorObject.move). + +To run the test, simply run the following in the code root directory: +``` +sui-move test +``` + +### On-chain Interactions +Now let's try to call `create` in actual transactions and see what happens. To do this we need to start Sui and the wallet. Please follow the [Wallet guide](../wallet.md) to start the Sui network and setup the wallet. Before starting, let's take a look at the default wallet address (this is address that will eventually own the object later): ``` @@ -91,10 +159,13 @@ You can find the published package object ID in the **Publish Results** output: ----- Publish Results ---- The newly published package object: (57258F32746FD1443F2A077C0C6EC03282087C19, SequenceNumber(1), o#b3a8e284dea7482891768e166e4cd16f9749e0fa90eeb0834189016c42327401) ``` -Note that the exact data you see will be different. The first hex string in that triple is the package object ID (`57258F32746FD1443F2A077C0C6EC03282087C19` in this case). +Note that the exact data you see will be different. The first hex string in that triple is the package object ID (`57258F32746FD1443F2A077C0C6EC03282087C19` in this case). For convenience, let's save it to an environment variable: +``` +$ export PACKAGE=57258F32746FD1443F2A077C0C6EC03282087C19 +``` Next we can call the function to create a color object: ``` -$ wallet call --gas-budget 1000 --package "57258F32746FD1443F2A077C0C6EC03282087C19" --module "ColorObject" --function "create" --args 0 255 0 +$ wallet call --gas-budget 1000 --package $PACKAGE --module "ColorObject" --function "create" --args 0 255 0 ``` In the **Transaction Effects** portion of the output, you will see an object showing up in the list of **Created Objects**, like this: @@ -102,9 +173,13 @@ In the **Transaction Effects** portion of the output, you will see an object sho Created Objects: 5EB2C3E55693282FAA7F5B07CE1C4803E6FDC1BB SequenceNumber(1) o#691b417670979c6c192bdfd643630a125961c71c841a6c7d973cf9429c792efa ``` +Again, for convenience, let's save the object ID: +``` +$ export OBJECT=5EB2C3E55693282FAA7F5B07CE1C4803E6FDC1BB +``` We can inspect this object and see what kind of object it is: ``` -$ wallet object --id 5EB2C3E55693282FAA7F5B07CE1C4803E6FDC1BB +$ wallet object --id $OBJECT ``` This will show you the metadata of the object with its type: ``` @@ -118,8 +193,8 @@ As we can see, it's owned by the current default wallet address that we saw earl You can also look at the data content of the object by adding the `--json` parameter: ``` -$ wallet object --id 5EB2C3E55693282FAA7F5B07CE1C4803E6FDC1BB --json +$ wallet object --id $OBJECT --json ``` This will print the values of all the fields in the Move object, such as the values of `red`, `green`, and `blue`. -Congratulations! You have learned how to define, create, and transfer objects. In the next chapter, we will learn how to use the objects that we own. +Congratulations! You have learned how to define, create, and transfer objects. We also showed how to write unit tests to mock transactions and interact with the objects. In the next chapter, we will learn how to use the objects that we own. 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 e69de29bb2d1d..60b45371b80e1 100644 --- a/doc/src/build/programming-with-objects/ch2-using-objects.md +++ b/doc/src/build/programming-with-objects/ch2-using-objects.md @@ -0,0 +1,155 @@ +## 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. +Sui authentication mechanism ensures that 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, you can pass them as parameters to an entry function. Similar to Rust, there are a few ways to pass parameters: + +### Pass objects by reference +There are two ways to pass objects by references: 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 `ColorObject`'s values with another `ColorObject`'s values. This will exercise using both read-only references and mutable references. +The `ColorObject` we defined in the previous chapter looks like the following: +```rust +struct ColorObject has key { + id: VersionedID, + red: u8, + green: u8, + blue: u8, +} +``` +Now let's add this function: +```rust +/// Copies the values of `from_object` into `into_object`. +public fun copy_into(from_object: &ColorObject, into_object, &mut ColorObject, _ctx: &mut TxContext) { + into_object.red = from_object.red; + into_object.green = from_object.green; + into_object.blue = from_object.blue; +} +``` +> :bulb: We added a `&mut TxContext` parameter to the function signature although it's not used. This is to allow the `copy_into` to be called as [entry function](../move.md#entry-functions) from transactions. The parameter is named with a `_` prefix to tell the compiler that it won't be used, so that we don't get unused parameter warning. + +In the above function signature, `from_object` can be a read-only reference because we only need to read its fields; while `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`**. + +> :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. + +We cannot write a unit test for the `copy_into` function just yet, as the support for retrieving multiple objects of same type from the same account is still work in progress. This will be updated as soon as we have that. + +### Pass objects by value +Objects can also be passed by value into an entry function. By doing so, the object is moved out from Sui storage (a.k.a. deleted). 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 `VersionedID` as a field, and the [VersionedID struct](../../../../sui_programmability/framework/sources/ID.move) does not have the `drop` ability, Sui object struct type [must not](https://github.com/diem/move/blob/main/language/documentation/book/src/abilities.md#drop) have `drop` ability either. Hence any Sui object cannot be arbitrarily dropped, and must be either consumed or unpacked. + +There are two ways we can deal with a pass-by-value Sui object in Move: +**1. Delete the object** +If the intention is to actually delete the object, we can unpack the object (this can only be done in the module that defined the struct type, due to Move's [privileged struct operations rules](https://github.com/diem/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. The `id` field of a Sui object, however requires special handling. We must call the following API in the [ID](../../../../sui_programmability/framework/sources/ID.move) module to signal Sui that we intend to delete this object: +```rust +public fun delete(versioned_id: VersionedID); +``` +Let's define a function in the `ColorObject` module that allows us to delete the object: +```rust + public fun delete(object: ColorObject, _ctx: &mut TxContext) { + let ColorObject { id, red: _, green: _, blue: _ } = object; + ID::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` cannot be dropped and must be explicitly deleted through the `ID::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: +```rust +let owner = @0x1; +// Create a ColorObject and transfer it to @owner. +let scenario = &mut TestScenario::begin(&owner); +{ + let ctx = TestScenario::ctx(scenario); + ColorObject::create(255, 0, 255, ctx); +}; +// Delete the ColorObject we just created. +TestScenario::next_tx(scenario, &owner); +{ + let object = TestScenario::remove_object(scenario); + let ctx = TestScenario::ctx(scenario); + ColorObject::delete(object, ctx); +}; +// Verify that the object was indeed deleted. +TestScenario::next_tx(scenario, &owner); +{ + assert!(!TestScenario::can_remove_object(scenario), 0); +} +``` +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 put it to 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, impossible) to return it to the storage. The last part checks that the object is indeed no longer in the global storage, and hence cannot be retrieved from there. + +**2. Transfer the object** +The owner of the object may want to transfer it to another account. To support this, the `ColorObject` module will need to define a `transfer` API: +```rust +public fun transfer(object: ColorObject, recipient: address, _ctx: &mut TxContext) { + Transfer::transfer(object, recipient) +} +``` +>:bulb: One cannot call `Transfer::transfer` directly in a transaction because it doesn't have `TxContext` as the last parameter, and hence it cannot be called as an entry function. + +Let's add a test for it too. First of all, we create an object in `owner`'s account, and then transfer it to a different account `recipient`: +```rust +let owner = @0x1; +// Create a ColorObject and transfer it to @owner. +let scenario = &mut TestScenario::begin(&owner); +{ + let ctx = TestScenario::ctx(scenario); + ColorObject::create(255, 0, 255, ctx); +}; +// Transfer the object to recipient. +let recipient = @0x2; +TestScenario::next_tx(scenario, &owner); +{ + let object = TestScenario::remove_object(scenario); + let ctx = TestScenario::ctx(scenario); + ColorObject::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 tranfser, we can verify that `owner` no longer owns the object, while `recipient` now owns it: +```rust +// Check that owner no longer owns the object. +TestScenario::next_tx(scenario, &owner); +{ + assert!(!TestScenario::can_remove_object(scenario), 0); +}; +// Check that recipient now owns the object. +TestScenario::next_tx(scenario, &recipient); +{ + assert!(TestScenario::can_remove_object(scenario), 0); +}; +``` + +### On-chain Interactions +Now it's time to try this out on-chain. Assume you have already followed the instructions in [Chapter 1](./ch1-object-basics.md#on-chain-interactions), we should already have the package published and a new object created. +Now we can try to transfer it to another account address. First let's see what other account addresses you own: +``` +$ wallet 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 `1416F3D5AF469905B0580B9AF843EC82D02EFD30`. Let's save it for convenience: +``` +$ export RECIPIENT=1416F3D5AF469905B0580B9AF843EC82D02EFD30 +``` +Now let's transfer the object to this address: +``` +$ wallet call --gas-budget 1000 --package $PACKAGE --module "ColorObject" --function "transfer" --args \"0x$OBJECT\" \"0x$RECIPIENT\" +``` +Now let's see what objects the `RECIPIENT` owns: +``` +$ wallet objects --address $RECIPIENT +``` +We should be able to see that one of the objects in the list is the new `ColorObject`! This means that the transfer was successful. +Let's also try to delete this object: +``` +$ wallet call --gas-budget 1000 --package $PACKAGE --module "ColorObject" --function "delete" --args \"0x$OBJECT\" +``` +Oops it will error out and complain that the account 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 wallet address to `$RECIPIENT`: +``` +$ wallet switch --address $RECIPIENT +``` +And try the deletion again: +``` +$ wallet call --gas-budget 1000 --package $PACKAGE --module "ColorObject" --function "delete" --args \"0x$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: +``` +$ wallet objects --address $RECIPIENT +``` +We will see that this object is no longer there in the wallet. \ No newline at end of file diff --git a/doc/src/build/programming-with-objects/index.md b/doc/src/build/programming-with-objects/index.md index b5ccd57461537..0d328f1a65c83 100644 --- a/doc/src/build/programming-with-objects/index.md +++ b/doc/src/build/programming-with-objects/index.md @@ -14,7 +14,7 @@ Prerequisite Readings: Index: - [Chapter 1: Object Basics](../../build/programming-with-objects/ch1-object-basics.md) - Defining Move object types, creating objects, transferring objects. -- Chapter 2: Using Objects +- [Chapter 2: Using Objects](../../build/programming-with-objects/ch2-using-objects.md) - Passing Move objects as arguments, mutating objects, deleting objects. - Chapter 3: Immutable Objects - Freezing an object, using immutable objects.