diff --git a/doc/move.md b/doc/move.md index 119385eee79f3..b5943ab00489a 100644 --- a/doc/move.md +++ b/doc/move.md @@ -281,7 +281,8 @@ current_directory ``` For convenience, please also make sure the path to Sui binaries -(sui/target/release) is part of your system path. +(sui/target/release), including the sui-move command used throughout +this tutorial, is part of your system path. We can now proceed to creating a package directory structure and an empty manifest file following the Move code organization described @@ -526,3 +527,179 @@ Running Move unit tests Test result: OK. Total tests: 1; passed: 1; failed: 0 ``` +The testing example we have seen so far is largely "pure" Move and has +little to do with Sui beyond using some Sui packages, such as +`Sui::TxContext` and `Sui::Transfer`. While this style of testing is +already very useful for developers writing Move code for Sui, they may +also want to test additional Sui-specific features. In particular, a +Move call in Sui is encapsulated in a Sui +[transaction](https://github.com/MystenLabs/fastnft/blob/main/doc/transactions.md), +and a developer may wish to test interactions between different +transactions within a single test (e.g. one transaction creating an +object and the other one transferring it). + +### Sui-specific testing + +Sui-specific testing is supported via the `TestScenario` +[module](../sui_programmability/framework/sources/TestScenario.move) +that provides Sui-related testing functionality otherwise unavailable +in "pure" Move and its testing +[framework](https://github.com/diem/move/blob/main/language/documentation/book/src/unit-testing.md). + +The main concept in the `TestScenario` is a scenario which emulates a +series of Sui transactions, each executed by a (potentially) different +user. At a high level, a developer writing a test starts the first +transaction using the `TestScenario::begin` function which takes an +address of the user executing this transaction as the first an only +argument, and return an instance of the `Scenario` struct representing +a 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. + +Let us extend our running example with a multi-transaction test that +uses the `TestScenario` to test sword creation and transfer from the +point of view of Sui developer. First, let us create entry +[functions](#entry-functions) callable from Sui that implement sword +creation and transfer and put the into the M1.move file: + +``` rust + public fun sword_create(magic: u64, strength: u64, recipient: address, ctx: &mut TxContext) { + use Sui::Transfer; + use Sui::TxContext; + // create a sword + let sword = Sword { + id: TxContext::new_id(ctx), + magic: magic, + strength: strength, + }; + // transfer the sword + Transfer::transfer(sword, recipient); + } + + public fun sword_transfer(sword: Sword, recipient: address, _ctx: &mut TxContext) { + use Sui::Transfer; + // transfer the sword + Transfer::transfer(sword, recipient); + } +``` + +The code of the new functions is self-explanatory and uses struct +creation and Sui-internal modules (`TxContext` and `Transfer`) in a +way similar to what we have seen in the previous sections. The +important part is for the entry functions to have correct signatures +as described [earlier](#entry-functions). In order for this code to +build, we need to add an additional import line at the module level +(as the first line in the module's main code block right before the +existing module-wide `ID` module import) to make `TxContext` struct +available for function definitions: + +``` rust + use Sui::TxContext::TxContext; +``` + +We can now build the module extended with the new functions, but still +have only one test defined. Let use change that by adding another test +function: + +``` rust + #[test] + public fun test_sword_transactions() { + use Sui::TestScenario; + + let admin = @0xBABE; + let initial_owner = @0xCAFE; + let final_owner = @0xFACE; + + // first transaction executed by admin + let scenario = &mut TestScenario::begin(&admin); + { + // create the sword and transfer it to the initial owner + sword_create(42, 7, initial_owner, TestScenario::ctx(scenario)); + }; + // second transaction executed by the initial sword owner + TestScenario::next_tx(scenario, &initial_owner); + { + // extract the sword owned by the initial owner + let sword = TestScenario::remove_object(scenario); + // transfer the sword to the final owner + sword_transfer(sword, final_owner, TestScenario::ctx(scenario)); + }; + // third transaction executed by the final sword owner + TestScenario::next_tx(scenario, &final_owner); + { + // extract the sword owned by the final owner + let sword = TestScenario::remove_object(scenario); + // verify that the sword has expected properties + assert!(magic(&sword) == 42 && strength(&sword) == 7, 1); + // return the sword to the object pool (it cannot be simply "dropped") + TestScenario::return_object(scenario, sword) + } + } +``` + +Let us now dive into some details of the new testing function. The +first thing we do is to create some addresses that represent users +participating in the testing scenario (we assume that we have one game +admin user and two regular users representing players). We then create +a scenario by starting the first transaction on behalf of the admin +address that creates a sword and transfers its ownership to the +initial owner. + +The second transaction is executed by the initial owner (passed as +argument to the `TestScenario::next_tx` function ) who then transfers +the sword it now owns the its final owner. Please note that in "pure" +Move we do not have the notion of Sui storage and, consequently, no +easy way for the emulated Sui transaction to retrieve it from +storage. This is where the `TestScenario` module comes to help - its +`remove_object` function makes an object of a given type (in this case +of type `Sword`) owned by an address executing the current transaction +available for manipulation by the Move code (for now we assume that +there is only one such object). In this case, the object retrieved +from storage is transferred to another address. + +The final transaction is executed by the final owner - it retrieves +the sword object from storage and checks if it has the expected +properties. Please remember that, as described +[earlier](#testing-a-package) in the "pure" Move testing scenario, +once an object is available in Move code (e.g., after its created or, +in this case, retrieved from emulated storage), it cannot simply +disappear. In the "pure" Move testing function we handled this problem +by transferring the sword object to the fake address but the +`TestScenario` package gives us a more elegant solution which is +closer to what happens when Move code is actually executed in the +context of Sui - we can simply return the sword to the object pool +using `TestScenario::return_object` function. + +We can now run the test command again and see that we now have two +successful tests for our module: + +``` shell +BUILDING MoveStdlib +BUILDING Sui +BUILDING MyMovePackage +Running Move unit tests +[ PASS ] 0x0::M1::test_sword_create +[ PASS ] 0x0::M1::test_sword_transactions +Test result: OK. Total tests: 2; passed: 2; failed: 0 +``` + +## Publishing a package + +For functions in a Move package to actually be callable from Sui +(rather than for Sui execution scenario to be emulated), the package +has to be _published_ to Sui's [distributed +ledger](SUMMARY.md#architecture) +where it is represented as a Sui object. At this point, however, the +sui-move command does not support package publishing. In fact it is +not clear if it even makes sense to accommodate package publishing, +which happens once per package creation, in the context of a unit +testing framework. Instead, one can use a sample Sui wallet to +[publish](wallet.md#package-publishing) Move code and to +[call](wallet.md#calling-move-code). Please see the wallet +[documentation](wallet.md#package-publishing) for a description on how +to publish the package we have [written](#writing-a-package) as as +part of this tutorial. diff --git a/doc/wallet.md b/doc/wallet.md index f6f98f68f733d..42f0a73225659 100644 --- a/doc/wallet.md +++ b/doc/wallet.md @@ -26,8 +26,8 @@ directory. ``` The genesis command creates 4 authorities, 5 user accounts each with 5 -gas objects, which are Sui objects used to pay for Sui -[transactions](https://github.com/MystenLabs/fastnft/blob/main/doc/transactions.md#transaction-metadata), +gas objects, which are Sui [objects](objects.md) used to pay for Sui +[transactions](transactions.md#transaction-metadata), such other object transfers or smart contract (Move) calls. These numbers represent a sample configuration and have been chosen somewhat arbitrarily, but the process of generating the genesis state can be @@ -331,8 +331,42 @@ DC5530627AFBFFBB1F52B81F273A7B666B31CB85: ... ## Package Publishing -TBD +In order for user-written code to be available in Sui, it must be +_published_ to to Sui's [distributed +ledger](SUMMARY.md#architecture). Please see Move developer +[documentation](move.md) for a +[description](move.md#writing-a-package) on how to write a simple Move +code package, which we can publish using Sui wallet's publish command. + +In order to show how to publish user-defined Move package, let us +continue where we left off in the previous +[section](#calling-move-code) describing how to call an existing Move +function. The publish command requires us to specify a directory where +user-defined package lives (it's a path to the `my_move_package` as +per package creation [description](move.md#writing-a-package)), a gas +object that will be used to pay for publishing the package (we use the +same gas object we used to pay for the function call in the previous +[section](#calling-move-code)), and gas budget to put an upper limit +(we use 1000 as our gas budget. The whole command looks as follows: +``` shell +./wallet --no-shell publish --path "$PACKAGE_PATH"/my_move_package --gas 1FD8DA0C56694229761E9A3DCE50C49AF2EA5DB1 1000 +``` + +The (abbreviated) result of running this command should show that one +object (package object) was created and one object (gas object) was +modified: + +``` shell +Created Objects: +BCC2E80B02B08226BF95522B0D4B59E687C5900E +Mutated Objects: +1FD8DA0C56694229761E9A3DCE50C49AF2EA5DB +``` + +From now on, we can use package object id in the Sui wallet's call +command just like we used 0x2 for built-in packages in the previous +[section](#calling-move-code). ## Genesis customization