diff --git a/book/404.md b/book/404.md index e7cfc78a0..2e14be2c3 100644 --- a/book/404.md +++ b/book/404.md @@ -1,3 +1,8 @@ +--- +description: "Page not found." +unlisted: true +--- + # Page not Found You're looking for a page that doesn't exist, or has been moved. Please check the URL and try again. diff --git a/book/appendix/acknowledgements.md b/book/appendix/acknowledgements.md index 28ba8c80f..e34119974 100644 --- a/book/appendix/acknowledgements.md +++ b/book/appendix/acknowledgements.md @@ -1,5 +1,7 @@ --- -description: "Acknowledgements for The Move Book: credits to The Rust Book, contributors, and the Move community." +description: + 'Acknowledgements for The Move Book: credits to The Rust Book, contributors, and the Move + community.' --- # Appendix F: Acknowledgements @@ -8,3 +10,16 @@ description: "Acknowledgements for The Move Book: credits to The Rust Book, cont personally grateful to the authors of the book, Steve Klabnik and Carol Nichols, for their work, as I have learned a lot from it. This book is a small tribute to their work and an attempt to bring a similar learning experience to the Move community. + +## The Move Community + +Just as important is the Move community, which has shaped this book from its very first edition - +written back when the language had no official documentation. Readers who asked questions, pointed +out confusing passages, reported mistakes, and opened issues and pull requests made the book what it +is today. The full list of contributors is available on +[GitHub](https://github.com/MystenLabs/move-book/graphs/contributors), and the +[Contributing](./contributing) appendix describes how to join them. + +Finally, none of this would exist without the people who created Move and continue to develop it +today - the original Move team and the many contributors advancing the language. The +[Move Reference](/reference) published alongside this book is adapted from their work. diff --git a/book/appendix/reserved-addresses.md b/book/appendix/reserved-addresses.md index 33d4cbbe3..90cf1c3a8 100644 --- a/book/appendix/reserved-addresses.md +++ b/book/appendix/reserved-addresses.md @@ -11,6 +11,10 @@ between environments and are used for specific native operations. - `0x2` - address of the [Sui Framework](./../programmability/sui-framework.md) (alias `sui`) - `0x5` - address of the `SuiSystem` object - `0x6` - address of the system [`Clock` object](./../programmability/epoch-and-time.md) -- `0x8` - address of the system `Random` object -- `0xc` - address of the system `CoinRegistry` object +- `0x8` - address of the system [`Random` object](./../programmability/randomness.md) +- `0xc` - address of the system + [`CoinRegistry` object](./../programmability/balance-and-coin.md#currency-and-the-coin-registry) +- `0xd` - address of the system `DisplayRegistry` object (see + [Object Display](./../programmability/display.md)) - `0x403` - address of the `DenyList` system object +- `0xacc` - address of the system `AccumulatorRoot` object diff --git a/book/before-we-begin/install-move-registry-cli.md b/book/before-we-begin/install-move-registry-cli.md index c55cb1cd4..c5b7fde5c 100644 --- a/book/before-we-begin/install-move-registry-cli.md +++ b/book/before-we-begin/install-move-registry-cli.md @@ -44,3 +44,8 @@ cargo install --locked --git https://github.com/mystenlabs/mvr --branch release For troubleshooting the installation process, please refer to the [Install MVR](https://docs.suins.io/move-registry/tooling/mvr-cli#installation) Guide. + +## Using MVR + +To learn how to find packages in the registry and use them as dependencies in your projects, see +the [Using Move Registry](./../guides/using-move-registry) guide. diff --git a/book/concepts/address.md b/book/concepts/address.md index cb56bf03b..4c4a1e4c8 100644 --- a/book/concepts/address.md +++ b/book/concepts/address.md @@ -4,34 +4,9 @@ description: "Learn about addresses in Sui — 32-byte unique identifiers used t # Address - - -Address is a unique identifier of a location on the blockchain. It is used to identify +An address is a unique identifier of a location on the blockchain. It is used to identify [packages](./packages), [accounts](./what-is-an-account), and [objects](./../object/object-model). -Address has a fixed size of 32 bytes and is usually represented as a hexadecimal string prefixed +An address has a fixed size of 32 bytes and is usually represented as a hexadecimal string prefixed with `0x`. Addresses are case insensitive. ```move @@ -43,7 +18,7 @@ with `0x`. Sui also has reserved addresses that are used to identify standard packages and objects. Reserved addresses are typically simple values that are easy to remember and type. For example, the address -of the Standard Library is `0x1`. Addresses, shorter than 32 bytes, are padded with zeros to the +of the Standard Library is `0x1`. Addresses shorter than 32 bytes are padded with zeros to the left. ```move @@ -52,11 +27,12 @@ left. Here are some examples of reserved addresses: -- `0x1` - address of the Sui Standard Library (alias `std`) +- `0x1` - address of the Move Standard Library (alias `std`) - `0x2` - address of the Sui Framework (alias `sui`) - `0x6` - address of the system `Clock` object -> You can find all reserved addresses in the [Appendix B](../appendix/reserved-addresses). +> You can find all reserved addresses in +> [Appendix B: Reserved Addresses](../appendix/reserved-addresses). ## Further Reading diff --git a/book/concepts/index.md b/book/concepts/index.md index cffc2fa98..31599bb07 100644 --- a/book/concepts/index.md +++ b/book/concepts/index.md @@ -4,8 +4,8 @@ description: "Core Sui and Move concepts: packages, accounts, transactions, addr # Concepts -In this chapter you will learn about the basic concepts of Sui and Move. You will learn what is a -package, how to interact with it, what is an account and a transaction, and how data is stored on -Sui. While this chapter is not a complete reference, and you should refer to the -[Sui Documentation](https://docs.sui.io) for that, it will give you a good understanding of the -basic concepts required to write Move programs on Sui. +In this chapter you will learn about the basic concepts of Sui and Move: what a package is and how +to interact with it, what an account and a transaction are, and how data is stored on Sui. While +this chapter is not a complete reference - refer to the [Sui Documentation](https://docs.sui.io) +for that - it will give you a good understanding of the concepts required to write Move programs +on Sui. diff --git a/book/concepts/manifest.md b/book/concepts/manifest.md index ee0c63ecf..73cf85d9a 100644 --- a/book/concepts/manifest.md +++ b/book/concepts/manifest.md @@ -1,3 +1,7 @@ +--- +description: "The Move.toml package manifest: package metadata, dependencies, named addresses, and dependency overrides explained." +--- + # Package Manifest The `Move.toml` is a manifest file that describes the [package](./packages) and its dependencies. It @@ -18,13 +22,11 @@ example = { git = "https://github.com/example/example.git", subdir = "path/to/pa ### Package The `[package]` section is used to describe the package. None of the fields in this section are -published on chain, but they are used in tooling and release management; they also specify the Move +published onchain, but they are used in tooling and release management; they also specify the Move edition for the compiler. - `name` - the name of the package when it is imported; -- `edition` - the edition of the Move language; currently, the only valid value is `2024`. - - +- `edition` - the edition of the Move language; currently, the only valid value is `2024`; ### Dependencies @@ -41,9 +43,9 @@ example = { git = "https://github.com/example/example.git", subdir = "path/to/pa my_package = { local = "../my-package" } ``` -Packages also import addresses from other packages. For example, the Sui dependency adds the `std` -and `sui` addresses to the project. These addresses can be used in the code as aliases for the -addresses. +Packages also import named addresses from their dependencies. For example, the Sui dependency adds +the `std` and `sui` addresses to the project, usable in the code in place of the full `0x1` and +`0x2` addresses. Starting with version 1.45 of the Sui CLI, the Sui system packages (`std`, `sui`, `system`, `bridge`, and `deepbook`) are automatically added as dependencies if none of them are explicitly @@ -89,4 +91,5 @@ local = "../my-package" ## Further Reading +- [Move Package Management](https://docs.sui.io/develop/manage-packages/move-package-management) in the Sui Docs. - [Packages](./../../reference/packages) in the Move Reference. diff --git a/book/concepts/packages.md b/book/concepts/packages.md index 30b8245c0..327fc3410 100644 --- a/book/concepts/packages.md +++ b/book/concepts/packages.md @@ -4,22 +4,6 @@ description: "Understand Move packages — the unit of code organization contain # Package - - Move is a language for writing smart contracts - programs that are stored and run on the blockchain. A single program is organized into a package. A package is published on the blockchain and is identified by an [address](./address). A published package can be interacted with by sending @@ -29,7 +13,7 @@ other packages. > To create a new package, use the `sui move new` command. To learn more about the command, run > `sui move new --help`. -Package consists of modules - separate scopes that contain functions, types, and other items. +A package consists of modules - separate scopes that contain functions, types, and other items. ``` package 0x... @@ -45,7 +29,7 @@ package 0x... Locally, a package is a directory with a `Move.toml` file and a `sources` directory. The `Move.toml` file - called the "package manifest" - contains metadata about the package, and the `sources` -directory contains the source code for the modules. Package usually looks like this: +directory contains the source code for the modules. A package usually looks like this: ``` sources/ @@ -60,14 +44,14 @@ Move.toml ``` The `tests` directory is optional and contains tests for the package. Code placed into the `tests` -directory is not published on-chain and is only available in tests. The `examples` directory can be -used for code examples, and is also not published on-chain. +directory is not published onchain and is only available in tests. The `examples` directory can be +used for code examples, and is also not published onchain. ## Published Package -During development, package doesn't have an address and it needs to be set to `0x0`. Once a package -is published, it gets a single unique [address](./address) on the blockchain containing its modules' -bytecode. A published package becomes _immutable_ and can be interacted with by sending +During development, a package doesn't have an address yet, and `0x0` is used in its place. Once a +package is published, it gets a single unique [address](./address) on the blockchain containing its +modules' bytecode. A published package becomes _immutable_ and can be interacted with by sending transactions. ``` @@ -76,7 +60,14 @@ transactions. another_module: ``` -## Links +While the published bytecode can never be changed, a package can be _upgraded_: an upgrade +publishes a new version of the package at a new address, leaving the old version intact. We touch +on the implications throughout the book: the +[Package Upgrades](./../programmability/package-upgrades) section explains the mechanics, and the +[Upgradeability Practices](./../guides/upgradeability-practices) guide covers how to design for +upgrades. + +## Further Reading - [Package Manifest](./manifest) - [Address](./address) diff --git a/book/concepts/what-is-a-transaction.md b/book/concepts/what-is-a-transaction.md index 3c45f2f7d..c26456720 100644 --- a/book/concepts/what-is-a-transaction.md +++ b/book/concepts/what-is-a-transaction.md @@ -4,21 +4,9 @@ description: "Learn how Sui transactions work: structure, commands, gas payments # Transaction -Transaction is a fundamental concept in the blockchain world. It is a way to interact with a -blockchain. Transactions are used to change the state of the blockchain, and they are the only way -to do so. In Move, transactions are used to call functions in a package, deploy new packages, and -upgrade existing ones. - - +A transaction is the fundamental way to interact with a blockchain. Transactions are used to +change the state of the blockchain, and they are the only way to do so. On Sui, a transaction can +call functions in published packages, deploy new packages, and upgrade existing ones. ## Transaction Structure @@ -31,11 +19,11 @@ Transactions consist of: - command inputs - the arguments for the commands: either `pure` - simple values like numbers or strings, or `object` - objects that the transaction will access; - a gas object - the `Coin` object used to pay for the transaction; -- gas price and budget - the cost of the transaction; +- a gas price and budget - the cost of the transaction. ## Inputs -Transaction inputs are the arguments for the transaction and are split between 2 types: +Transaction inputs are the arguments for the transaction, and come in two types: - Pure arguments: These are mostly [primitive types](../move-basics/primitive-types) with some extra additions. A pure argument can be: @@ -47,7 +35,7 @@ Transaction inputs are the arguments for the transaction and are split between 2 - [`std::ascii::String`](../move-basics/string#ascii-strings), ASCII strings. - [`vector`](../move-basics/vector), where `T` is a pure type. - [`std::option::Option`](../move-basics/option), where `T` is a pure type. - - [`std::object::ID`](../storage/uid-and-id), typically points to an object. See also + - [`sui::object::ID`](../storage/uid-and-id), typically points to an object. See also [What is an Object](../object/object-model). - Object arguments: These are objects or references of objects that the transaction will access. An object argument needs to be either a shared object, a frozen object, or an object that the @@ -61,6 +49,13 @@ publishing a package) or a call to a function in an already published package. T executed in the order they are listed in the transaction, and they can use the results of the previous commands, forming a chain. Transaction either succeeds or fails as a whole. +Any [`public`](../move-basics/visibility#public-visibility) function can be called as a command: +making a function `public` is all it takes for users to call it in a transaction, and it is the +default way to expose functionality in Move. (There is also the +[`entry`](../move-basics/visibility#entry-modifier) modifier, which creates functions callable +_only_ as transaction commands - a deliberately restricted option, covered in the +[Entry Functions](../move-advanced/entry-functions) section.) + Schematically, a transaction looks like this (in pseudo-code): ``` @@ -81,10 +76,6 @@ In this example, the transaction consists of three commands: with the given arguments - the `payment` object; 3. `TransferObjects` - a built-in command that transfers the object to the recipient. - - ## Transaction Effects Transaction effects are the changes that a transaction makes to the blockchain state. More @@ -104,4 +95,12 @@ The result of the executed transaction consists of different parts: - Events - the custom [events](./../programmability/events) emitted by the transaction; - Object Changes - the changes made to the objects, including the _change of ownership_; - Balance Changes - the changes made to the aggregate balances of the account involved in the - transaction; + transaction. + +## Further Reading + +- [Transactions](https://docs.sui.io/concepts/transactions) in the Sui Documentation. +- [Programmable Transaction Blocks](https://docs.sui.io/concepts/transactions/prog-txn-blocks) in + the Sui Documentation. +- [Using Address Balances](https://docs.sui.io/onchain-finance/asset-custody/address-balances/using-address-balances) + in the Sui Documentation - paying gas and moving funds without a `Coin` object. diff --git a/book/concepts/what-is-an-account.md b/book/concepts/what-is-an-account.md index 8ba77a3d0..60b48ecbf 100644 --- a/book/concepts/what-is-an-account.md +++ b/book/concepts/what-is-an-account.md @@ -4,31 +4,20 @@ description: "Understand Sui accounts: how they are generated from private keys, # Account - - An account is a way to identify a user. An account is generated from a private key, and is identified by an address. An account can own objects, and can send transactions. Every transaction has a sender, and the sender is identified by an [address](./address). -Sui supports multiple cryptographic algorithms for account generation. The two supported curves are -ed25519, secp256k1, and there is also a special way of generating an account - zklogin. The -cryptographic agility - the unique feature of Sui - allows for flexibility in the account -generation. +An account does not need to be created or registered anywhere: it exists as soon as a keypair is +generated, and any valid address can receive objects without prior setup. There is no onchain +record of "all accounts" - an address with no objects and no transaction history is +indistinguishable from one that was never used. - +Sui supports multiple signature schemes for accounts: ed25519, ECDSA (over the secp256k1 and +secp256r1 curves), passkeys (device authenticators such as Face ID, Touch ID, or a hardware +security key, based on the WebAuthn standard), multisig (an account controlled by a combination of +keys), and zkLogin, which derives an account from a Web2 login. This _cryptographic agility_ gives +Sui unusual flexibility in how accounts are created and controlled. ## Further Reading @@ -38,3 +27,4 @@ generation. the [Sui Docs](https://docs.sui.io) - [Signatures](https://docs.sui.io/guides/developer/cryptography/signing) in the [Sui Docs](https://docs.sui.io) +- [Passkey](https://docs.sui.io/develop/cryptography/passkeys) in the [Sui Docs](https://docs.sui.io) diff --git a/book/guides/2024-migration-guide.md b/book/guides/2024-migration-guide.md index 68950ba57..1705909a5 100644 --- a/book/guides/2024-migration-guide.md +++ b/book/guides/2024-migration-guide.md @@ -1,48 +1,68 @@ --- -description: "Migrate your Move code to the 2024 edition: updated syntax, new features, method syntax, and step-by-step migration instructions." +description: "Migrate your Move code to the 2024 edition: module labels, let mut, public structs, method syntax, enums and match, macros, clever errors, and step-by-step instructions." --- # Move 2024 Migration Guide -Move 2024 is the new edition of the Move language that is maintained by Mysten Labs. This guide is -intended to help you understand the differences between the 2024 edition and the previous version of -the Move language. +Move 2024 is the current edition of the Move language maintained by Mysten Labs, and the edition +this book teaches. This guide is written for readers migrating code - or knowledge - from the +original edition (referred to below as _Move 2020_): it lists what changed, feature by feature, +with a before-and-after example for each. -> This guide provides a high-level overview of the changes in the new edition. For a more detailed -> and exhaustive list of changes, refer to the -> [Sui Blog](https://blog.sui.io/move-2024-migration-guide). +> This guide is a high-level overview. Every feature listed here has a dedicated section in the +> book, linked from its heading - refer to them for the full story. -## Using the New Edition +## Using the 2024 Edition -To use the new edition, you need to specify the edition in the `move` file. The edition is specified -in the `move` file using the `edition` keyword. Currently, the only available edition is -`2024.beta`. +The edition is specified in the `[package]` section of the [Package Manifest](./../concepts/manifest). +The stable `2024` edition is the default choice and the one to prefer; the `2024.beta` and +`2024.alpha` editions give early access to features that are still in development and may change: -```ini +```toml +[package] +name = "my_package" edition = "2024" -# alternatively, for new features: -edition = "2024.beta" ``` ## Migration Tool -The Move CLI has a migration tool that updates the code to the new edition. To use the migration -tool, run the following command: +The Move CLI has a migration tool that updates legacy code to the new edition. To use the migration +tool, run the following command in the package directory: ```bash $ sui move migrate ``` -The migration tool will update the code to use the `let mut` syntax, the new `public` modifier for -structs, and the `public(package)` function visibility instead of `friend` declarations. +The migration tool handles the mechanical changes: the `let mut` syntax, the `public` modifier on +structs, and the `public(package)` visibility in place of `friend` declarations. + +## Module Label + +_See [Module](./../move-basics/module#module-block)._ + +A module no longer needs to wrap its body in a block: the _module label_ syntax declares the module +once, and everything that follows belongs to it - saving a level of indentation in the entire file. +The block syntax is still supported, but only useful for declaring multiple modules in one file, +which is not a recommended practice: + +```move +// Move 2020: module block +module book::my_module { + public struct Book {} +} + +// Move 2024: module label +module book::my_module; + +public struct Book {} +``` ## Mutable Bindings with `let mut` -Move 2024 introduces `let mut` syntax to declare mutable variables. The `let mut` syntax is used to -declare a mutable variable that can be changed after it is declared. +_See [Primitive Types](./../move-basics/primitive-types#variables-and-assignment)._ -> `let mut` declaration is now required for mutable variables. Compiler will emit an error if you -> try to reassign a variable without the `mut` keyword. +Move 2024 requires the `mut` keyword to declare a variable that can be reassigned or mutably +borrowed. The compiler emits an error on an attempt to change a variable declared without `mut`: ```move // Move 2020 @@ -54,8 +74,8 @@ let mut x: u64 = 10; x = 20; ``` -Additionally, the `mut` keyword is used in tuple destructuring and function arguments to declare -mutable variables. +Additionally, the `mut` keyword is used in tuple destructuring and function arguments, placed +before the variable name: ```move // takes by value and mutates @@ -64,145 +84,183 @@ fun takes_by_value_and_mutates(mut v: Value): Value { v } -// `mut` should be placed before the variable name +// in tuple destructuring fun destruct() { - let (x, y) = point::get_point(); let (mut x, y) = point::get_point(); - let (mut x, mut y) = point::get_point(); } // in struct unpack fun unpack() { let Point { x, mut y } = point::get_point(); - let Point { mut x, mut y } = point::get_point(); } ``` -## Friends are Deprecated +## Struct Visibility -In Move 2024, the `friend` keyword is deprecated. Instead, you can use the `public(package)` -visibility modifier to make functions visible to other modules in the same package. +_See [Custom Types with Struct](./../move-basics/struct#defining-a-struct)._ + +In Move 2024, struct declarations require a visibility modifier. Currently, the only available +visibility is `public`: ```move // Move 2020 -friend book::friend_module; -public(friend) fun protected_function() {} +struct Book {} // Move 2024 -public(package) fun protected_function_2024() {} +public struct Book {} ``` -## Struct Visibility +Note that `public` applies to the struct _type_ - the fields stay internal to the module, and only +the defining module can pack and unpack the struct, exactly as before. + +## Friends Are Deprecated + +_See [Visibility Modifiers](./../move-basics/visibility#package-visibility)._ -In Move 2024, structs get a visibility modifier. Currently, the only available visibility modifier -is `public`. +The `friend` declarations and the `public(friend)` visibility are deprecated. In their place, the +`public(package)` visibility makes a function callable from any module of the same package - with +no declaration required. The `friend book::module_name;` statements are gone entirely: ```move // Move 2020 -struct Book {} +friend book::friend_module; +public(friend) fun protected_function() {} -// Move 2024 -public struct Book {} +// Move 2024: no friend declaration needed +public(package) fun protected_function() {} ``` ## Method Syntax -In the new edition, functions which have a struct as the first argument are associated with the -struct. This means that the function can be called using the dot notation. Methods defined in the -same module with the type are automatically exported. +_See [Struct Methods](./../move-basics/struct-methods)._ -> Methods are automatically exported if the type is defined in the same module as the method. It is -> impossible to export methods for types defined in other modules. However, you can create -> [custom aliases](#method-aliases) for methods in the module scope. +Functions whose first argument is a type defined in the same module become _methods_ of that type, +callable with the dot syntax anywhere the type is used: ```move public fun count(c: &Counter): u64 { /* ... */ } -fun use_counter() { - // move 2020 - let count = counter::count(&c); +fun use_counter(c: &Counter) { + // Move 2020 + let count = counter::count(c); - // move 2024 + // Move 2024 let count = c.count(); } ``` -## Methods for Built-in Types +The standard library and the Sui Framework make full use of this: native and standard types come +with associated methods out of the box: + +```move +// vector to string and ascii string +let str: String = b"Hello, World!".to_string(); +let ascii: ascii::String = b"Hello, World!".to_ascii_string(); + +// address to bytes +let bytes = @0xa11ce.to_bytes(); +``` + +## `use fun` and Method Aliases -In Move 2024, some of the native and standard types received associated methods. For example, the -`vector` type has a `to_string` method that converts the vector into a UTF8 string. +_See [Struct Methods](./../move-basics/struct-methods#method-aliases)._ + +The `use fun` declaration associates a function with a type under a chosen method name. An alias +can be declared for any type locally to the module; or publicly - with `public use fun` - if the +type is defined in the same module: ```move -fun aliases() { - // vector to string and ascii string - let str: String = b"Hello, World!".to_string(); - let ascii: ascii::String = b"Hello, World!".to_ascii_string(); +// Local: the type is foreign to the module +use fun my_custom_function as vector.do_magic; - // address to bytes - let bytes = @0xa11ce.to_bytes(); -} +// Exported: the type is defined in the same module +public use fun kiosk_owner_cap_for as KioskOwnerCap.kiosk; ``` -For the full list of built-in aliases, refer to the -[Standard Library](./../move-basics/standard-library#source-code) and -[Sui Framework](./../programmability/sui-framework#source-code) source code. +## Index Syntax for Borrowing -## Borrowing Operator +_See [Vector](./../move-basics/vector#reading-elements) and +[Index Syntax](./../../reference/index-syntax) in the Move Reference._ -Some of the built-in types support borrowing operators. The borrowing operator is used to get a -reference to the element at the specified index. The borrowing operator is defined as `[]`. +Square brackets replace explicit `borrow` and `borrow_mut` calls on collection types: ```move fun play_vec() { - let v = vector[1,2,3,4]; - let first = &v[0]; // calls vector::borrow(v, 0) - let first_mut = &mut v[0]; // calls vector::borrow_mut(v, 0) - let first_copy = v[0]; // calls *vector::borrow(v, 0) + let mut v = vector[1, 2, 3, 4]; + let first = &v[0]; // calls vector::borrow(&v, 0) + let first_mut = &mut v[0]; // calls vector::borrow_mut(&mut v, 0) + let first_copy = v[0]; // calls *vector::borrow(&v, 0) } ``` -Types that support the borrowing operator are: - -- `vector` -- `sui::vec_map::VecMap` -- `sui::table::Table` -- `sui::bag::Bag` -- `sui::object_table::ObjectTable` -- `sui::object_bag::ObjectBag` -- `sui::linked_table::LinkedTable` - -To implement the borrowing operator for a custom type, you need to add a `#[syntax(index)]` -attribute to the methods. +The syntax is supported by `vector` and the collection types of the Sui Framework: `VecMap`, +`Table`, `Bag`, `ObjectTable`, `ObjectBag`, and `LinkedTable`. A custom type can implement it by +marking its borrow functions with the `#[syntax(index)]` attribute: ```move #[syntax(index)] -public fun borrow(c: &List, key: String): &T { /* ... */ } +public fun borrow(c: &List, key: String): &T { /* ... */ } #[syntax(index)] -public fun borrow_mut(c: &mut List, key: String): &mut T { /* ... */ } +public fun borrow_mut(c: &mut List, key: String): &mut T { /* ... */ } ``` -## Method Aliases +## String Literals + +_See [String](./../move-basics/string#string-literals)._ -In Move 2024, methods can be associated with types. The alias can be defined for any type locally to -the module; or publicly, if the type is defined in the same module. +Move 2020 offered only byte-string literals, and constructing a `String` required an explicit +conversion. The new edition adds the string literal `"..."`, with the type _inferred_ from context - +it becomes a `String`, an `ascii::String`, or a `vector`, whichever is expected: ```move -// my_module.move -// Local: type is foreign to the module -use fun my_custom_function as vector.do_magic; +// Move 2020: bytes, converted at runtime +let str: String = string::utf8(b"Hello"); -// sui-framework/kiosk/kiosk.move -// Exported: type is defined in the same module -public use fun kiosk_owner_cap_for as KioskOwnerCap.kiosk; +// Move 2024: the literal is checked and typed at compile time +let str: String = "Hello"; +let ascii: std::ascii::String = "ASCII"; +``` + +The contents are validated at compile time: a literal used as an `ascii::String` must contain only +ASCII characters, or the code will not compile. + +## Enums and `match` + +_See [Enums and Match](./../move-basics/enum-and-match)._ + +Move 2024 introduces _enums_ - user-defined types with multiple variants - and the `match` +expression for handling them. Together they allow expressing varying data structures under a single +type, something previously emulated with multiple structs and runtime checks: + +```move +/// One type - three different shapes of data. +public enum Segment has copy, drop { + Empty, + String(String), + Special { content: vector, encoding: u8 }, +} + +public fun is_empty(s: &Segment): bool { + match (s) { + Segment::Empty => true, + _ => false, + } +} ``` - + +The familiar `assert!` is no longer special-cased compiler magic - it is a regular macro, and its +error-code argument is now optional. The standard library ships a rich set of macros which quickly +became the idiomatic way to write iteration: + +```move +let v = vector[1, 2, 3]; + +// instead of a hand-written while loop: +let doubled = v.map!(|n| n * 2); +let sum = v.fold!(0, |acc, n| acc + n); +v.do!(|n| std::debug::print(&n)); +``` + +## Abort Without a Code + +_See [Aborting Execution](./../move-basics/assert-and-abort#omitting-the-abort-code)._ + +The abort code is now optional: a bare `abort` (and `assert!` without a second argument) derives +the code automatically, encoding the module and source line of the failure. It is a good fit for +branches that are not expected to be reachable: + +```move +// Move 2020: a code was always required +if (!is_valid) abort 0; + +// Move 2024 +if (!is_valid) abort; +assert!(is_valid); +``` + +## Clever Errors + +_See [Aborting Execution](./../move-basics/assert-and-abort#error-messages)._ + +Error constants marked with the `#[error]` attribute can carry a human-readable message - a +`vector` instead of a bare `u64`. On abort, tooling decodes the constant name, the message, and +the source line, removing the need to look up numeric codes: + +```move +#[error] +const ENotAuthorized: vector = "The caller is not authorized to perform this action"; + +public fun protected_action(/* ... */) { + assert!(is_authorized, ENotAuthorized); +} +``` + +## Extending Modules in Tests + +_See [Extending Modules](./../testing/extend-foreign-module)._ + +The `extend module` declaration adds test-only members to an existing module - including a module +from a foreign package - with full access to its private types. It solves the long-standing problem +of testing against dependencies that ship no test utilities: + +```move +#[test_only] +extend module pyth::price_info; + +// Functions defined here can pack and unpack the private +// types of `pyth::price_info` - in tests only. +``` + +> Module extensions are still in development and currently require the `2024.alpha` edition. + +## Further Reading + +- [Move 2024 Migration Guide](https://blog.sui.io/move-2024-migration-guide) on the Sui Blog. diff --git a/book/guides/code-quality-checklist.md b/book/guides/code-quality-checklist.md index 75656d51d..c4af47a19 100644 --- a/book/guides/code-quality-checklist.md +++ b/book/guides/code-quality-checklist.md @@ -44,8 +44,8 @@ Sui = { ... } ### Prefix Named Addresses -If your package has a generic name (e.g., `token`) – especially if your project includes multiple -packages – make sure to add a prefix to the named address: +If your package has a generic name (e.g., `token`) - especially if your project includes multiple +packages - make sure to add a prefix to the named address: ```toml # bad! not indicative of anything, and can conflict @@ -97,7 +97,7 @@ use my_package::my_module::OtherMember; use my_package::my_module::{Self, OtherMember}; ``` -### Error Constants are in `EPascalCase` +### Error Constants Are in `EPascalCase` ```move // bad! all-caps are used for regular constants @@ -107,14 +107,14 @@ const NOT_AUTHORIZED: u64 = 0; const ENotAuthorized: u64 = 0; ``` -### Regular Constant are `ALL_CAPS` +### Regular Constants Are `ALL_CAPS` ```move // bad! PascalCase is associated with error consts -const MyConstant: vector = b"my const"; +const MyConstant: vector = "my const"; // good! clear indication that it's a constant value -const MY_CONSTANT: vector = b"my const"; +const MY_CONSTANT: vector = "my const"; ``` ## Structs @@ -265,13 +265,17 @@ use std::string::utf8; let str = utf8(b"hello, world!"); -// good! -let str = b"hello, world!".to_string(); +// good! the literal is checked at compile time +let str: String = "hello, world!"; // also, for ASCII string -let ascii = b"hello, world!".to_ascii_string(); +let ascii: ascii::String = "hello, world!"; ``` +> The `.to_string()` and `.to_ascii_string()` methods on `vector` still have their place - +> converting bytes that are not known at compile time. For literals, prefer the string literal +> syntax. + ### UID has `delete` ```move @@ -545,7 +549,7 @@ assert!(is_success); ```move // bad! old-style code -assert!(result == b"expected_value", 0); +assert!(result == "expected_value", 0); // good! will print both values if fails use std::unit_test::assert_eq; diff --git a/book/guides/upgradeability-practices.md b/book/guides/upgradeability-practices.md index 37bfe016a..67534b463 100644 --- a/book/guides/upgradeability-practices.md +++ b/book/guides/upgradeability-practices.md @@ -4,6 +4,9 @@ description: "Best practices for upgrading Move packages on Sui: maintain compat # Upgradeability Practices +> This guide builds on the [Package Upgrades](./../programmability/package-upgrades) section, which +> explains the mechanics of upgrades: versions, the `UpgradeCap`, and state migrations. + To talk about best practices for upgradeability, we need to first understand what can be upgraded in a package. The base premise of upgradeability is that an upgrade should not break public compatibility with the previous version. The parts of the module which can be used in dependent @@ -25,7 +28,7 @@ public struct Book has key { title: String, } -// public structs can not be removed and can't be changed +// the same rule applies to event structs public struct BookCreated has copy, drop { /* ... */ } diff --git a/book/guides/using-move-registry.md b/book/guides/using-move-registry.md new file mode 100644 index 000000000..800e6786f --- /dev/null +++ b/book/guides/using-move-registry.md @@ -0,0 +1,214 @@ +--- +description: + 'Use Move Registry (MVR) to add external dependencies to your Move package: search for packages, + add them to the manifest, and call them in your code.' +--- + +# Using Move Registry + +Every package published on Sui is identified by its address. Addresses are precise but hard to +work with: they are not memorable, they differ between networks, and they say nothing about what +the package does or who published it. [Move Registry (MVR)](https://www.moveregistry.com) solves +this by mapping human-readable names, like `@potatoes/date`, to published package addresses. With +MVR, adding an external dependency to your package is a single command, and the toolchain resolves +the name to the right address for the network you are building against. + +This guide walks through the full cycle of using an external package: finding it in the registry, +adding it to the manifest, calling it in code, and testing the result. It assumes you have the MVR +CLI installed - if you don't, refer to the +[Install MVR](./../before-we-begin/install-move-registry-cli) section. + +## Package Names + +MVR names follow the `@organization/package-name` pattern: the organization part is backed by a +[SuiNS](https://suins.io) name, and the package name is registered under it by the organization's +owner. A name points to a published package on a specific network, so the same name can resolve to +different addresses on _mainnet_ and _testnet_. Additionally, since packages on Sui are versioned, +a name can also carry a version suffix, such as `@potatoes/date/1`; without it, the name resolves +to the latest version. + +In this guide we use the [`@potatoes/date`](https://www.moveregistry.com/package/@potatoes/date) +package - a small library that converts a timestamp into a `Date` structure and prints it in +ISO 8601, UTC (RFC 7231), or a custom format. + +## Finding a Package + +Packages can be discovered on the [MVR website](https://www.moveregistry.com) or directly from the +terminal with the `mvr search` command. The query can be a part of a package name or description, +or an `@organization/` prefix to list everything published by one organization: + +```bash +$ mvr search "@potatoes/" +``` + +```plaintext +- @potatoes/codec +# High performant encoding library for Sui, features: base64, base64url, urlencode, hex (base16) +Networks: mainnet, testnet + +- @potatoes/date +# Date and time printing / formatting tool, which supports RFC 7231 (UTC), ISO-8601 and custom +# formats, as well as constructing the date from string +Networks: mainnet, testnet +``` + +The `Networks` line is important: a name can only be resolved on a network where the package is +published. If a dependency is only available on _mainnet_, a build against _testnet_ will fail to +resolve it. + +## Adding a Dependency + +To add a package to your project, run the `mvr add` command in the directory containing the package +manifest: + +```bash +$ mvr add @potatoes/date +``` + +The command inserts a new record into the `[dependencies]` section of the `Move.toml`: + +```toml +[dependencies] +date = { r.mvr = "@potatoes/date" } +``` + +Unlike a git dependency, which points to a repository and revision, this record contains only the +registry name. The `r.` prefix stands for _external resolver_ - a plugin that the Sui CLI calls +during build to turn the name into a concrete package address and source location. The `mvr` +binary is that resolver, which is why it must be installed and available in the `PATH`. + +To pin the dependency to a specific version, add the version suffix to the name: + +```toml +[dependencies] +date = { r.mvr = "@potatoes/date/1" } +``` + +## Building the Package + +The dependency is fetched and resolved as a part of the regular build. During `sui move build`, +the CLI calls the MVR resolver for each `r.mvr` record, using the currently active environment to +pick the network: + +```plaintext +Output from mvr: + │ [mvr] resolving: "@potatoes/date" on network: testnet + +Output from mvr: + │ [mvr] resolving: "@potatoes/ascii/1" on network: testnet + +INCLUDING DEPENDENCY MoveStdlib +INCLUDING DEPENDENCY Sui +INCLUDING DEPENDENCY ascii +INCLUDING DEPENDENCY date +BUILDING postcard +``` + +Note the second resolver call: `@potatoes/date` itself depends on `@potatoes/ascii`, and the +resolver fetches it automatically. Transitive MVR dependencies require no extra records in your +manifest. + +The result of the resolution is recorded in the `Move.lock` file, pinning each dependency to an +exact source revision per network. The lock file should be checked into version control, so that +every build of your package uses the same dependency versions. + +## Using the Dependency + +Once the dependency is added, its modules can be imported with a regular `use` statement. The +address part of the path is the named address declared by the dependency itself - for +`@potatoes/date` it is `date`, so the `date` module in it is imported as `date::date`. + +The example below defines a `Postcard` object which stores a human-readable timestamp of its +creation, using the `Date` type from the dependency and the `Clock` object to get the current +time: + +```move +/// Module: postcard +module postcard::postcard; + +use date::date; +use std::string::String; +use sui::clock::Clock; + +/// A postcard which prints the date and time of its creation. +public struct Postcard has key, store { + id: UID, + message: String, + sent_at: String, +} + +/// Create a new `Postcard` with a message and a human-readable timestamp. +public fun new(message: String, clock: &Clock, ctx: &mut TxContext): Postcard { + let date = date::from_clock(clock); + + Postcard { + id: object::new(ctx), + message, + sent_at: date.to_utc_string(), + } +} +``` + +The `date::from_clock` function reads the timestamp from the `Clock` object (see +[Epoch and Time](./../programmability/epoch-and-time)) and converts it into a `Date` value, which +is then printed as a UTC string. The package also provides `to_iso_string` for ISO 8601 output and +`format` for custom formats: + +```move +// Jan 1, 2025, 12:30:00 UTC +let date = date::new(1735734600000); + +assert!(date.to_utc_string() == "Wed, 01 Jan 2025 12:30:00 GMT"); +assert!(date.to_iso_string() == "2025-01-01T12:30:00.000Z"); +assert!(date.format("DD MMM YYYY, HH:mm") == "01 Jan 2025, 12:30"); +``` + +## Testing + +External dependencies take part in tests like any other code. The test below creates a `Clock` +with a known timestamp and checks that the `Postcard` prints it correctly: + +```move +#[test] +fun test_postcard() { + let ctx = &mut tx_context::dummy(); + let mut clock = sui::clock::create_for_testing(ctx); + + // set time to Jan 1, 2025, 12:30:00 UTC + clock.set_for_testing(1735734600000); + + let postcard = new("Hello from Move!", &clock, ctx); + + assert!(postcard.sent_at == "Wed, 01 Jan 2025 12:30:00 GMT"); + + transfer::public_transfer(postcard, ctx.sender()); + clock.destroy_for_testing(); +} +``` + +Running `sui move test` resolves the dependencies, builds the package, and executes the test: + +```plaintext +Running Move unit tests +[ PASS ] postcard::postcard::test_postcard +Test result: OK. Total tests: 1; passed: 1; failed: 0 +``` + +## Summary + +- Move Registry (MVR) maps human-readable names, like `@potatoes/date`, to published package + addresses, per network. +- The `mvr search` command (or the [MVR website](https://www.moveregistry.com)) helps discover + packages and shows which networks they are published on. +- The `mvr add` command adds a dependency record to the `Move.toml`; the name is resolved during + build by the `mvr` binary, based on the active environment. +- Resolved dependencies are pinned in the `Move.lock` file, which should be checked into version + control. +- Modules of the dependency are imported using the named address declared by the dependency + itself. + +## Further Reading + +- [Move Registry documentation](https://docs.suins.io/move-registry) - including how to publish + and register your own package. +- [Package Manifest](./../concepts/manifest) section of this book. diff --git a/book/index.md b/book/index.md index 3520a27fd..9f0252078 100644 --- a/book/index.md +++ b/book/index.md @@ -4,7 +4,7 @@ description: "A comprehensive guide to the Move programming language and Sui blo # The Move Book - +_by Damir Shamanaev, with contributions from the Move community_ This is The Move Book - a comprehensive guide to the Move programming language and the Sui blockchain. The book is intended for developers who are interested in learning about Move and @@ -12,9 +12,9 @@ building on Sui.
-The book is in active development and a work in progress. If you have any feedback or suggestions, -feel free to open an issue or a pull request on the -[GitHub repository](https://github.com/MystenLabs/move-book). +Move and the Sui framework keep evolving, and this book grows with them - new features bring new +sections and updates to existing ones. If you have any feedback or suggestions, feel free to open +an issue or a pull request on the [GitHub repository](https://github.com/MystenLabs/move-book).
diff --git a/book/move-advanced/entry-functions.md b/book/move-advanced/entry-functions.md new file mode 100644 index 000000000..b1e6f92f8 --- /dev/null +++ b/book/move-advanced/entry-functions.md @@ -0,0 +1,190 @@ +--- +description: + 'Entry functions in Move: how the entry modifier restricts a function to transaction-only calls, + and the static hot-potato guarantee its arguments receive in return.' +--- + +# Entry Functions + +An [`entry`](./../move-basics/visibility#entry-modifier) function is a special kind of +transaction-callable function - one that deliberately _limits_ the options of its caller. As +covered in the [Visibility Modifiers](./../move-basics/visibility) chapter, `entry` is not a +visibility level, and it is not how functions are normally made callable from a +[transaction](./../concepts/what-is-a-transaction) - a `public` function already is, and `public` +remains the default. The `entry` modifier is only meaningful on _non-public_ functions - private or +`public(package)` - and what it creates is a function with a narrowed contract, in two directions: + +- _who can call it:_ outside of its own module (or package), the function can be invoked only as a + command in a transaction - no other package can wrap it, act on its result, or build it into + larger logic; +- _what the call can be combined with:_ the arguments passed to it must be free of obligations + created by other commands in the same transaction - they are checked to behave as if the `entry` + function were the only command. + +The first restriction follows directly from visibility rules and was covered in Move Basics. This +chapter describes the second - a static guarantee about the arguments, and the rules behind it. The +material requires familiarity with the +[hot potato pattern](./../programmability/hot-potato-pattern), the +[abilities](./../move-basics/abilities-introduction), and how transactions are structured, which is +why it lives here rather than in Move Basics. + +## The Hot Potato Guarantee + +Arguments to a non-`public` `entry` function (either private or `public(package)`) cannot be +_entangled_ with a [hot potato](./../programmability/hot-potato-pattern) - a value whose type has +neither `store` nor `drop`, and which therefore must be dealt with before the transaction ends. In +practice this means the arguments behave as if the `entry` function were the only command in the +transaction: no earlier command can force behavior on the transaction after the `entry` function is +called. + +> This guarantee is checked _statically_, before the transaction begins execution. A transaction +> that violates it fails verification and is not executed. These rules were introduced in Sui v1.62, +> replacing an older, more restrictive set. + +The canonical motivation is a _flash loan_ - borrowing funds that must be repaid within the same +transaction. A simplified lender looks like this: + +```move +module flash::loan; + +use sui::balance::Balance; +use sui::sui::SUI; + +public struct Bank has key { + id: UID, + holdings: Balance, +} + +/// A hot potato: no `store`, no `drop`. Once issued, the transaction +/// cannot succeed until it is destroyed by calling `repay`. +public struct Loan { + amount: u64, +} + +public fun issue(bank: &mut Bank, amount: u64): (Balance, Loan) { + assert!(bank.holdings.value() >= amount); + let loaned = bank.holdings.split(amount); + (loaned, Loan { amount }) +} + +public fun repay(bank: &mut Bank, loan: Loan, repayment: Balance) { + let Loan { amount } = loan; + assert!(repayment.value() == amount); + bank.holdings.join(repayment); +} +``` + +A developer writing an `entry` function that accepts a `Coin` may want to be sure the coin is really +"owned" by the sender, and not borrowed from such a bank with an outstanding repayment obligation. +The `entry` rules provide exactly that. + +## The Rules + +The verification tracks how many hot potato values are outstanding and which values they could +influence. Some terminology: + +- A _value_ is any argument of a transaction command: a transaction input, the result of a previous + command, or the gas coin. +- A value is _hot_ if its type has neither `store` nor `drop`. This leaves three possible shapes: a + type with no abilities at all, a type with only `copy`, or a type with only `key` (a type cannot + have both `key` and `copy`, since `sui::object::UID` does not have `copy`). +- Every value belongs to a _clique_ - a group of values that have been used together as arguments to + a command, along with the results of that command. Each clique counts its outstanding hot values. + +The algorithm walks the commands of the transaction in order: + +1. Each transaction input starts in its own clique with a count of zero. +2. When values are used together in a command - by value or by reference - their cliques are merged, + and their counts are added together. +3. The count is decremented for each hot value _moved_ into the command (taken by value, not + copied). +4. If the command calls a non-`public` `entry` function, the count of the merged clique must be zero + at this point. Note that this means an `entry` function _can_ take hot values - they must just be + the last hot values in their clique. +5. The results of the command join the merged clique, and the count is incremented for each hot + result. + +Let's walk through it. Given a module with the following functions: + +```move +module book::example; + +use sui::coin::Coin; +use sui::sui::SUI; + +public struct HotPotato() + +public fun hot(coin: &mut Coin): HotPotato { /* ... */ HotPotato() } +public fun cool(potato: HotPotato) { let HotPotato() = potato; } + +entry fun spend(coin: &mut Coin) { /* ... */ } +``` + +The following transaction is rejected. The call to `hot` produced a hot potato, so `Input(0)` is in +a clique with an outstanding hot value when `spend` is called: + +```text +// Invalid transaction +// Input 0: Coin +// cliques: { Input(0) } => 0 +0: book::example::hot(Input(0)); +// cliques: { Input(0), Result(0) } => 1 +1: book::example::spend(Input(0)); // INVALID, Input(0)'s clique has a count > 0 +2: book::example::cool(Result(0)); +``` + +Destroying the hot potato first brings the count back to zero, and the same call becomes valid: + +```text +// Valid transaction +// Input 0: Coin +// cliques: { Input(0) } => 0 +0: book::example::hot(Input(0)); +// cliques: { Input(0), Result(0) } => 1 +1: book::example::cool(Result(0)); +// cliques: { Input(0) } => 0 +2: book::example::spend(Input(0)); // Valid! Input(0)'s clique has a count of 0 +``` + +The clique is what makes the rule robust: entanglement spreads through _any_ shared usage, not just +direct one. Using the `flash::loan` module, the `Coin` below was created from the loaned `Balance` +and never touched the `Loan` directly - yet it is in the same clique, and cannot be passed to the +`entry` function until the loan is repaid: + +```text +// Invalid transaction +// Input 0: flash::loan::Bank +// Input 1: u64 +// cliques: { Input(0) } => 0, { Input(1) } => 0 +0: flash::loan::issue(Input(0), Input(1)); +// cliques: { Input(0), NestedResult(0,0), NestedResult(0,1) } => 1 +1: sui::coin::from_balance(NestedResult(0,0)); +// cliques: { Input(0), NestedResult(0,1), Result(1) } => 1 +2: book::example::spend(Result(1)); // INVALID, Result(1)'s clique has count > 0 +3: sui::coin::into_balance(Result(1)); +4: flash::loan::repay(Input(0), NestedResult(0,1), Result(3)); +``` + +If the loan were repaid before calling `spend`, the transaction would pass verification. + +## Shared Objects + +There is one special case: when a command takes a _shared object_ by value, the count of the merged +clique is set to infinity. A non-`public` `entry` function can still take a shared object by value +directly, but it cannot take a value whose clique previously interacted with one. + +The reason is that a shared object taken by value can force behavior in the rest of the transaction +much like a hot potato does: it cannot be wrapped or transferred, so it must be either re-shared or +deleted before the transaction ends. But unlike a hot potato, this obligation is not visible in the +type's abilities, so the verification has to assume the worst. + +[Party objects](./../appendix/transfer-functions) taken by value fall under the same restriction, +although in narrower cases than shared objects. + +> Because the rules are applied statically, before execution, they are deliberately pessimistic: a +> dynamic check could be more precise, but a static one is easier to describe and to rely on. + +## Further Reading + +- [Visibility Modifiers](./../move-basics/visibility) in Move Basics, for the basics of `entry`. +- [Visibility](./../../reference/functions#visibility) in the Move Reference. diff --git a/book/move-advanced/index.md b/book/move-advanced/index.md index 41db70f1b..ccdec0d07 100644 --- a/book/move-advanced/index.md +++ b/book/move-advanced/index.md @@ -1,5 +1,7 @@ --- -description: "Advanced Move language features: modes, extended build configurations, and advanced programming techniques for Sui developers." +description: + 'Advanced Move language features: modes, extended build configurations, and advanced programming + techniques for Sui developers.' --- # Advanced Move Usage @@ -7,3 +9,7 @@ description: "Advanced Move language features: modes, extended build configurati This chapter covers advanced features of the Move language, including various extended behaviors for advanced programming. This includes advanced usage of the language itself, plus the package and build system. + +- [Compilation Modes](./modes) - including unpublishable code in named build modes. +- [Entry Functions](./entry-functions) - the `entry` modifier and the static hot-potato guarantee + that makes it a safe transaction boundary. diff --git a/book/move-advanced/modes.md b/book/move-advanced/modes.md index 17bb42fdc..5494977e3 100644 --- a/book/move-advanced/modes.md +++ b/book/move-advanced/modes.md @@ -16,10 +16,10 @@ Modes at a glance: * Build with `--mode ` (or `--test` for unit testing). Items whose mode list contains a name you enabled are compiled in. Items whose mode list does **not** match are compiled **out**. * Code compiled with any mode enabled is **not publishable**. This keeps debug/test scaffolding from - ever making it on-chain. + ever making it onchain. * Items with **no** `#[mode(...)]`/`#[test_only]` annotation are always included. -> Tip: Modes are filters enforced at compile-time—they don’t affect bytecode at runtime. Use them +> Tip: Modes are filters enforced at compile-time - they don’t affect bytecode at runtime. Use them > for helpers, simulators, and other mock types and functions that should never be published. ## Syntax @@ -70,7 +70,7 @@ non-enabled mode are compiled **out**. Unannotated items are always compiled in. > **Publish safety**: Any artifact produced while a mode is enabled (including `--test`) is non-publishable. Always run a clean build **without** `--mode`/`--test` before `sui client publish`. -### Example — `test` mode (unit tests) +### Example - `test` mode (unit tests) `#[test_only]` is the built-in mode for unit testing. It works exactly like a mode named `test`. @@ -116,17 +116,20 @@ module my_pkg::bank { #[mode(debug)] module my_pkg::bank_debug { use std::debug; + use std::string::String; use my_pkg::bank; public fun transfer_debug(from: &signer, to: address, amount: u64) { // Perform debugging prints before the real call - debug::print(&b"[DEBUG] transfer begin".to_vector()); + let begin: String = "[DEBUG] transfer begin"; + debug::print(&begin); debug::print(&amount); debug::print(&to); // Main Call bank::transfer(from, to, amount); // More debugging prints - debug::print(&b"[DEBUG] transfer end".to_vector()); + let end: String = "[DEBUG] transfer end"; + debug::print(&end); } } ``` diff --git a/book/move-basics/abilities-introduction.md b/book/move-basics/abilities-introduction.md index 33d303b4d..acea2632a 100644 --- a/book/move-basics/abilities-introduction.md +++ b/book/move-basics/abilities-introduction.md @@ -4,53 +4,105 @@ description: "Introduction to Move abilities: copy, drop, key, and store — the # Abilities: Introduction -Move has a unique type system which allows customizing _type abilities_. -[In the previous section](./struct), we introduced the `struct` definition and how to use it. -However, the instances of the `Artist` and `Record` structs had to be unpacked for the code to -compile. This is default behavior of a struct without _abilities_. - -> Throughout the book you will see chapters with name `Ability: `, where `` is the name -> of the ability. These chapters will cover the ability in detail, how it works, and how to use it -> in Move. +Move has a unique type system in which each type declares what its values are allowed to do. In the +[previous section](./struct), every instance of `Artist` and `Record` had to be used: stored, +passed on, or unpacked - discarding a value, or copying it, was not an option. This is not +accidental strictness. By default, a Move value can only be created, moved around, and taken apart; +everything beyond that is a privilege the type must be granted explicitly. These privileges are +called _abilities_. ## What are Abilities? -Abilities are a way to allow certain behaviors for a type. They are a part of the struct declaration -and define which behaviors are allowed for the instances of the struct. +Abilities are permissions on a type. They are declared as a part of the struct definition, and the +compiler rejects any operation the type is not permitted to perform. An ability does not add any +functionality to the type itself - it unlocks behavior that is otherwise a compile error. + +There are four abilities in Move. Two of them control what can happen to a value during execution: + +- `copy` - the value can be _duplicated_; +- `drop` - the value can be _discarded_; + +and two control storage: + +- `key` - the value can be a unit of storage - on Sui, an _object_; +- `store` - the value can be stored _inside_ other values in storage. + +This "deny by default" design is what allows Move types to model assets faithfully: a type without +`copy` cannot be duplicated, and a type without `drop` cannot be lost - guarantees that a language +with ordinary, freely copyable values cannot give. + +> Throughout the book you will see sections named `Ability: `, each covering one ability in +> detail: how it works, and when to use it. ## Abilities Syntax -Abilities are set in the struct definition using the `has` keyword followed by a list of abilities. -The abilities are separated by commas. Move supports 4 abilities: `copy`, `drop`, `key`, and -`store`. Each ability defines a specific behavior for the struct instances. +Abilities are set in the struct definition using the `has` keyword followed by a comma-separated +list of abilities: + +```move file=packages/samples/sources/move-basics/abilities-introduction.move anchor=definition + +``` + +The two declared abilities change how instances of `VeryAble` behave. Compare the following code to +the pack-and-unpack ceremony from the [previous section](./struct): + +```move file=packages/samples/sources/move-basics/abilities-introduction.move anchor=use -```move -/// This struct has the `copy` and `drop` abilities. -public struct VeryAble has copy, drop { - // field: Type1, - // field2: Type2, - // ... -} ``` -## Overview +Now, let's take a quick tour of all four abilities, one at a time. + +## `drop`: Discarding Values + +The `drop` ability allows an instance to be _discarded_: assigned to an unused variable, ignored +with the `_` wildcard, or simply left behind when the scope ends. In other words, `drop` makes a +type behave the way values behave in most other programming languages. It belongs on types that +represent plain _data_, and its absence protects types that represent _assets_. The +[next section](./drop-ability) is dedicated to it. + +## `copy`: Duplicating Values + +The `copy` ability allows an instance to be _duplicated_, implicitly by the compiler or explicitly +with the `copy` keyword. All the primitive types - integers, `bool`, `address` - behave as if they +have it. Note that `copy` almost always comes together with `drop`: a value that can be duplicated +but not discarded would force every one of its copies to be used. The details are covered in the +[Ability: Copy](./copy-ability) section. + +## `key`: Objects and Storage + +The `key` ability marks a type as a _unit of storage_: an instance can be written to the blockchain +state and later found by its unique identifier - its "key". On Sui, a struct with the `key` ability +is called an _object_, and it is required to have an `id: UID` as its first field. Objects are the +heart of the Sui programming model, and the whole [Object Model](./../object/) chapter is dedicated +to them, followed by [Ability: Key](./../storage/key-ability) covering the ability itself. -A quick overview of the abilities: +## `store`: Storing Inside Objects -> All of the built-in types except [references](references) have `copy`, `drop`, and `store` -> abilities. References have `copy` and `drop`. +The `store` ability allows an instance to be stored _inside_ other structs that end up in storage. +While `key` makes a type a top-level record in the blockchain state, `store` permits a type to be a +_part_ of one. It is explained in the [Ability: Store](./../storage/store-ability) section. -- `copy` - allows the struct to be _copied_. Explained in the [Ability: Copy](./copy-ability) - chapter. -- `drop` - allows the struct to be _dropped_ or _discarded_. Explained in the - [Ability: Drop](./drop-ability) chapter. -- `key` - allows the struct to be used as a _key_ in a storage. Explained in the - [Ability: Key](./../storage/key-ability) chapter. -- `store` - allows the struct to be _stored_ in structs that have the _key_ ability. Explained in - the [Ability: Store](./../storage/store-ability) chapter. +## Abilities Come from Fields + +An ability is a promise about the whole value, including its contents - so a struct can only be +granted an ability that all of its field types support. A struct with `copy` requires every field to +have `copy`, and the same holds for `drop` and `store`; `key` requires every field to have `store`. +The compiler enforces this at the definition site, and the following code will not compile: + +```move +public struct NoAbilities {} + +public struct Wrapper has copy, drop { + inner: NoAbilities, + // ^ error! The struct was declared with the ability 'copy' + // so all fields require the ability 'copy' +} +``` -While it is important to briefly mention them here, we will go into more detail about each ability -in the following chapters and give proper context on how to use them. +> All of the built-in types except [references](./references) have the `copy`, `drop`, and `store` +> abilities, and references have `copy` and `drop`. Container types like [`vector`](./vector) and +> [`Option`](./option) support `copy`, `drop`, and `store` _conditionally_ - a vector can only be +> copied if its elements can. ## No Abilities diff --git a/book/move-basics/address.md b/book/move-basics/address.md index e1972bbb2..993d8c669 100644 --- a/book/move-basics/address.md +++ b/book/move-basics/address.md @@ -4,36 +4,23 @@ description: "The address type in Move: literals, named addresses, conversion fu # Address Type - - -Move uses a special type called [address](./../concepts/address) to represent addresses. It is a -32-byte value that can represent any address on the blockchain. Addresses can be written in two -forms: hexadecimal addresses prefixed with 0x and named addresses. +Move uses a special type called [address](./../concepts/address) to represent addresses - the +32-byte values that identify accounts, packages, and objects on the blockchain. In an expression, +an address literal starts with the `@` symbol, followed either by a hexadecimal number or by an +identifier: ```move file=packages/samples/sources/move-basics/address.move anchor=address_literal ``` -An address literal starts with the `@` symbol followed by a hexadecimal number or an identifier. The -hexadecimal number is interpreted as a 32-byte value. The identifier is looked up in the -[Move.toml](./../concepts/manifest) file and replaced with the corresponding address by the -compiler. If the identifier is not found in the Move.toml file, the compiler will throw an error. +The hexadecimal number is interpreted as a 32-byte value, with the missing leading bytes filled +with zeros - so `@0x2` is a shorthand for the address ending in `...0002`. The identifier is looked +up in the [Move.toml](./../concepts/manifest) file and replaced with the corresponding address by +the compiler; if it is not found there, compilation fails. + +> Some addresses are reserved by the system: for example, the [Standard Library](./standard-library) +> lives at `0x1` and the Sui Framework at `0x2`. The full list is in +> [Appendix B: Reserved Addresses](./../appendix/reserved-addresses). ## Conversion @@ -41,6 +28,10 @@ Sui Framework offers a set of helper functions to work with addresses. Given tha is a 32-byte value, it can be converted to a `u256` type and vice versa. It can also be converted to and from a `vector` type. +> The examples below use the [vector](./vector) and [String](./string) types, which are covered +> later in this chapter - for now, it is enough to know that a conversion to and from bytes and +> text exists. + Example: Convert an address to a `u256` type and back. ```move file=packages/samples/sources/move-basics/address.move anchor=to_u256 diff --git a/book/move-basics/assert-and-abort.md b/book/move-basics/assert-and-abort.md index f5fbfde52..c1fab77fc 100644 --- a/book/move-basics/assert-and-abort.md +++ b/book/move-basics/assert-and-abort.md @@ -4,39 +4,25 @@ description: "Error handling in Move: use abort to halt execution with error cod # Aborting Execution - - - - -A transaction can either succeed or fail. Successful execution applies all changes made to objects -and on-chain data, and the transaction is committed to the blockchain. Alternatively, if a -transaction aborts, changes are not applied. Use the `abort` keyword to abort a transaction and -revert any changes that were made. - -> It is important to note that there is no catch mechanism in Move. If a transaction aborts, the -> changes made so far are reverted, and the transaction is considered failed. +A transaction can end in one of two ways: it either succeeds, and all of the changes it made are +applied and committed to the blockchain, or it _aborts_, and none of the changes are applied. There +is nothing in between: a transaction cannot partially succeed, and an abort in a deeply nested +function call fails the entire transaction. This all-or-nothing model is what makes error handling +in Move simple and predictable - a function never needs to undo its changes, because an abort +undoes everything at once. + +> There is no catch mechanism in Move. An abort cannot be intercepted or recovered from: it always +> fails the whole transaction. This is a design choice - it trades flexibility for simplicity and +> makes it impossible to end up in a partially updated state. + +In this section, we look at the tools Move provides for aborting: the `abort` expression, the +`assert!` macro, and the conventions for defining error codes and error messages. ## Abort -The `abort` keyword is used to abort the execution of a transaction. It is used in combination with -an abort code, which is returned to the caller of the transaction. The abort code is an -[integer](./primitive-types) of type `u64`. +The `abort` keyword stops execution immediately. It is normally given an _abort code_ - an +[integer](./primitive-types) of type `u64` - which is returned to the caller of the transaction +together with the identity of the module that aborted. Here is an example: ```move file=packages/samples/sources/move-basics/assert-and-abort.move anchor=abort @@ -44,42 +30,106 @@ an abort code, which is returned to the caller of the transaction. The abort cod The code above will, of course, abort with abort code `1`. +Two properties of abort codes are worth internalizing early: + +- Abort codes are _local to the module_. Two different modules can both abort with code `1`, and + they mean different things; the caller has to interpret the code together with the module that + produced it. +- An abort code carries no message. The blockchain records only the numeric code and the location + of the abort - making the codes readable is up to the module author, which is what + [error constants](#error-constants) and [error messages](#error-messages) below are for. + +## Omitting the Abort Code + +The abort code can be omitted in the source - a bare `abort` expression is valid Move: + +```move file=packages/samples/sources/move-basics/assert-and-abort.move anchor=clean_abort + +``` + +Omitted does not mean absent, though: the caller still receives a `u64` abort code, derived +automatically by the compiler. The derived code uses the clever-error encoding described in +[Error Messages](#error-messages) below - it carries the module and the source line of the +failure, with the constant name and value left empty. + +This form, sometimes called a _clean abort_, is a good fit for branches that are not expected to be +reachable at all - such as the wildcard arm of a `match` expression covering values that cannot +occur (see [Enums and Match](./enum-and-match)). Since the derived code points at the failure but +says nothing about its _meaning_, for conditions that external callers can actually trigger, prefer +an explicit code or an error message. + ## assert! -The `assert!` macro is a built-in macro that can be used to assert a condition. If the condition is -false, the transaction will abort with the given abort code. The `assert!` macro is a convenient way -to abort a transaction if a condition is not met. The macro shortens the code otherwise written with -an `if` expression + `abort`. The `code` argument is optional, but has to be a `u64` value or an -`#[error]` (see below for more information). +The `assert!` macro is a built-in macro that checks a condition and aborts if the condition is +false. It is a shorthand for the `if` + `abort` combination you would otherwise write by hand, and +it is by far the most common way to abort in Move code. The first argument is the condition; the +second, optional, argument is the abort code - when it is omitted, a code is derived automatically, +the same way as for a bare `abort`: ```move file=packages/samples/sources/move-basics/assert-and-abort.move anchor=assert ``` -## Error constants +A common practice is to place asserts at the beginning of a function - check everything first, then +perform the changes. Because an abort reverts the whole transaction this is not required for +safety, but it makes the function's requirements visible at a glance and avoids wasting +[gas](./../concepts/what-is-a-transaction) on work that is bound to be thrown away. + +## Error Constants -To make error codes more descriptive, it is a good practice to define -[error constants](./constants). Error constants are defined as `const` declarations and are usually -prefixed with `E` followed by a camel case name. Error constants are similar to other constants and -do not have any special handling. However, they are commonly used to improve code readability and -make abort scenarios easier to understand. +A raw numeric code like `assert!(user_has_access, 1)` tells the reader nothing about what went +wrong. To make error codes descriptive, it is a good practice to define them as +[constants](./constants). Error constants follow their own naming convention - `E` followed by a +CamelCase description - which sets them apart from regular `ALL_CAPS` constants: ```move file=packages/samples/sources/move-basics/assert-and-abort.move anchor=error_const ``` -## Error messages +Error constants are regular `u64` constants and receive no special treatment from the compiler. +However, following the convention makes the code self-documenting - +`assert!(user_has_access, ENoAccess)` reads as a sentence - and a caller who receives the abort +code can find the matching constant in the module's source. A well-written module defines an error +constant for every abort scenario it can produce. + +## Error Messages -Move 2024 introduces a special type of error constant, marked with the `#[error]` attribute. This -attribute allows the error constant to be of type `vector` and can be used to store an error -message. +Move 2024 introduces _clever errors_ - error constants marked with the `#[error]` attribute. Unlike +regular error constants, they can be of any type - most usefully `vector`, holding a +human-readable error message: ```move file=packages/samples/sources/move-basics/assert-and-abort.move anchor=error_attribute ``` +The attribute does not change what an abort is: the transaction still fails with a `u64` abort +code. What changes is the content of that code - the compiler packs into it the source line number +of the abort (for a macro like `assert!`, the line of the call site) and references to the +constant's name and value. Tooling that understands the format - the Sui CLI, explorers, SDKs - +unpacks it and shows the full picture, along the lines of: + +```text +Error from 'book::assert_abort::update_value' (line 15), abort 'EValueTooLow': +"The value is too low, it should be at least 10" +``` + +Error messages remove the need to look up the meaning of a numeric code, which matters most in +public-facing applications, where the person reading the failure is often not the author of the +module. The flip side of the encoding is that the numeric value of a clever abort code depends on +the source layout: reformatting the module or adding a line changes it. Refer to these constants by +name - never by their compiled numeric value. The exact layout of the encoding is described in +[Clever Errors](./../../reference/abort-and-assert/clever-errors) in the Move Reference. + +## Aborts in Tests + +Aborting is a behavior worth testing like any other. The `#[expected_failure]` attribute marks a +test that is supposed to abort, and its `abort_code` argument asserts the exact code - the test +fails if the function succeeds or aborts with a different code. We cover this attribute in more +detail in the [Testing](./testing) section. + ## Further Reading - [Abort and Assert](./../../reference/abort-and-assert) in the Move Reference. +- [Clever Errors](./../../reference/abort-and-assert/clever-errors) in the Move Reference. - We suggest reading the [Better Error Handling](./../guides/better-error-handling) guide to learn about best practices for error handling in Move. diff --git a/book/move-basics/comments.md b/book/move-basics/comments.md index 8998d35ac..5cac0ea73 100644 --- a/book/move-basics/comments.md +++ b/book/move-basics/comments.md @@ -4,18 +4,6 @@ description: "How to use line comments, block comments, and doc comments in Move # Comments - - Comments are a way to add notes or document your code. They are ignored by the compiler and don't result in Move bytecode. You can use comments to explain what your code does, add notes to yourself or other developers, temporarily remove a part of your code, or generate documentation. There are @@ -45,16 +33,19 @@ This example is a bit extreme, but it shows all the ways that you can use block ## Doc Comment Documentation comments are special comments that are used to generate documentation for your code. -They are similar to block comments but start with three slashes `///` and are placed before the -definition of the item they document. +They are similar to line comments but start with three slashes `///` and are placed before the +definition of the item they document - a module, a struct, a function, or a constant. ```move file=packages/samples/sources/move-basics/comments-doc.move anchor=main ``` +Documentation tooling collects doc comments of the public members into reference pages - the +[standard library and framework documentation](https://docs.sui.io/references/framework) linked +throughout this book is generated exactly this way. A well-written doc comment states what the +function does, and under which conditions it aborts. + ## Whitespace -Unlike some languages, whitespace (spaces, tabs, and newlines) have no impact on the meaning of the +Unlike some languages, whitespace (spaces, tabs, and newlines) has no impact on the meaning of the program. - - diff --git a/book/move-basics/constants.md b/book/move-basics/constants.md index bb85805d8..9a83fea95 100644 --- a/book/move-basics/constants.md +++ b/book/move-basics/constants.md @@ -4,28 +4,12 @@ description: "Constants in Move: how to define immutable module-level values, na # Constants - - Constants are immutable values that are defined at the module level. They often serve as a way to give names to static values that are used throughout a module. For example, if there's a default price for a product, you might define a constant for it. Constants are stored in the module's -bytecode, and each time they are used, the value is copied. +bytecode, and each time they are used, the value is copied. Like every module member, constants are +private by default - and unlike functions or structs, they cannot be made public; the +[config pattern](#using-the-config-pattern) below shows how to share them between modules. ```move file=packages/samples/sources/move-basics/constants-shop-price.move anchor=shop_price @@ -36,15 +20,16 @@ bytecode, and each time they are used, the value is copied. Constants must start with a capital letter - this is enforced at the compiler level. For constants used as a value, the convention is to use all uppercase letters and underscores between words, which makes constants stand out from other identifiers in the code. An exception is made for -[error constants](./assert-and-abort#error-constants), which are written in ECamelCase. +[error constants](./assert-and-abort#error-constants), which are written as `E` followed by a +CamelCase description, as in `ENoAccess`. ```move file=packages/samples/sources/move-basics/constants-naming.move anchor=naming ``` -## Constants are Immutable +## Constants Are Immutable -Constants can't be changed and assigned new values. As part of the package bytecode, they are +Constants can't be changed or assigned new values. As part of the package bytecode, they are inherently immutable. ```move @@ -58,11 +43,12 @@ fun change_price() { } ``` -## Using Config Pattern +## Using the Config Pattern A common use case for an application is to define a set of constants that are used throughout the codebase. But due to constants being private to the module, they can't be accessed from other -modules. One way to solve this is to define a "config" module that exports the constants. +modules. One way to solve this is to define a "config" module that exports the constants through +public functions: ```move file=packages/samples/sources/move-basics/constants-config.move anchor=config @@ -72,7 +58,7 @@ This way other modules can import and read the constants, and the update process the constants need to be changed, only the config module needs to be updated during the package upgrade. -## Links +## Further Reading - [Constants](./../../reference/constants) in the Move Reference -- [Coding conventions for constants](./../guides/code-quality-checklist#regular-constant-are-all_caps) +- [Coding conventions for constants](./../guides/code-quality-checklist#regular-constants-are-all_caps) diff --git a/book/move-basics/control-flow.md b/book/move-basics/control-flow.md index ec5480521..6c685f91d 100644 --- a/book/move-basics/control-flow.md +++ b/book/move-basics/control-flow.md @@ -1,29 +1,12 @@ --- -description: "Control flow in Move: if/else expressions, while and loop constructs, break, continue, and return statements." +description: + 'Control flow in Move: if/else expressions, while and loop constructs, break, continue, and return + statements.' --- # Control Flow - - -Control flow statements are used to control the flow of execution in a program. They are used to +Control flow statements decide which code runs, how many times, and when to stop. They are used to make decisions, repeat a block of code, or exit a block of code early. Move includes the following control flow statements (explained in detail below): @@ -31,12 +14,13 @@ control flow statements (explained in detail below): code - [`loop` and `while` loops](#repeating-statements-with-loops) - repeating a block of code - [`break` and `continue` statements](#exiting-a-loop-early) - exiting a loop early +- [labeled control flow](#labeled-control-flow) - targeting an outer loop or block from a nested one - [`return`](#early-return) statement - exiting a function early ## Conditional Statements The `if` expression is used to make decisions in a program. It evaluates a -[boolean expression](./expression#literals) and executes a block of code if the expression is true. +[boolean](./primitive-types#booleans) expression and executes a block of code if the expression is true. Paired with `else`, it can execute a different block of code if the expression is false. The syntax for an `if` expression is: @@ -62,17 +46,28 @@ Let's see how we can use `if` and `else` to assign a value to a variable: ``` In this example, the value of the `if` expression is assigned to the variable `y`. If `x` is greater -than 0, `y` is assigned the value 1; otherwise, it is assigned 0. The `else` block is required -because both branches of the `if` expression must return a value of the same type. Omitting the -`else` block would result in a compiler error, as it ensures all possible branches are accounted for -and type safety is maintained. +than 0, `y` is assigned the value 1; otherwise, it is assigned 0. The `else` block is required here +because both branches of an `if` expression must have the same type. When the `else` is omitted, the +false branch defaults to the unit value `()`, so assigning an `if` without an `else` to a variable +is a type error: + +```move +let y = if (x > 0) 1; +// ^^^^^^^^^^^^ ERROR! Expected 'u64', but found '()' - the missing +// else branch defaults to the unit value `()`. +``` + +To choose between more than two branches, `if` expressions can be chained with `else if`. The +branches are checked top to bottom, and the first one whose condition is true is taken: - +```move file=packages/samples/sources/move-basics/control-flow.move anchor=else_if + +``` Conditional expressions are among the most important control flow statements in Move. They evaluate user-provided input or stored data to make decisions. One key use case is in the [`assert!` macro](./assert-and-abort), which checks if a condition is true and aborts execution if -it is not. We’ll explore this in detail shortly. +it is not. We explore it in detail later in this chapter. ## Repeating Statements with Loops @@ -85,6 +80,12 @@ Loops are useful for working with collections, such as vectors, or for repeating until a specific condition is met. However, take care to avoid infinite loops, which can exhaust gas limits and cause the transaction to abort. +> In practice, hand-written loops are relatively rare in Move. Iterating over a collection is more +> commonly expressed with the higher-level [macros](./macros) such as `do!`, `map!`, and `fold!`, +> which are covered in the [Vector](./vector#vector-macros) chapter. The `loop` and `while` +> constructs described here are the primitives those macros are built on, and remain the right tool +> when the iteration does not fit a simple collection traversal. + ## The `while` Loop The `while` statement executes a block of code repeatedly as long as the associated boolean @@ -178,6 +179,67 @@ The example below skips odd numbers and prints only even numbers from 0 to 10: `break` and `continue` statements can be used in both `while` and `loop` loops. +## Labeled Control Flow + +By default, `break` and `continue` act on the innermost loop that encloses them. This is a problem +when loops are nested: from inside an inner loop, there is no way to break out of the outer one. To +solve this, Move lets you attach a _label_ to a loop and then tell `break` or `continue` exactly +which one to target. + +A label is a name prefixed with a single quote, placed before the `loop` or `while` keyword. You can +then write `break 'label` or `continue 'label` to jump to the labeled loop instead of the innermost +one: + +```move +'outer: loop { + while (condition) { + // Exits both loops at once. + break 'outer; + + // Skips to the next iteration of the outer loop. + continue 'outer; + }; +}; +``` + +Consider a search over a grid - a vector of rows, where each row is itself a vector. Once we find +the value we are looking for, we want to stop scanning entirely, not just finish the current row. +Labeling the outer loop lets the inner `while` loop abandon the whole search in one step: + +```move file=packages/samples/sources/move-basics/control-flow.move anchor=labeled_loop + +``` + +Notice that the `break` statements also carry a value: `break false` and `break 'search true`. A +`loop` is an expression, so breaking out of it can produce a result - here, the boolean returned by +the function. This is specific to `loop`: a `while` loop always evaluates to the unit value `()`, so +its `break` cannot carry a value. Without the label, escaping both loops would require an extra flag +variable and a second check in the outer loop. + +### Labeled Blocks + +Labels are not limited to loops. A plain block `{ ... }` can also be labeled, and then exited early +with `return 'label `. This is useful for computing a value with several possible early +exits, without extracting the logic into a separate function: + +```move file=packages/samples/sources/move-basics/control-flow.move anchor=labeled_block + +``` + +Here the `'result` block produces a value, and any of the `return 'result` statements can end it +early. This becomes especially powerful together with the iteration [macros](./macros) mentioned +above, where a labeled block lets a lambda break out of the iteration with a result. + +Two rules are worth remembering: + +- A label can only be placed on a `loop`, a `while`, or a block `{}` - **not** on an `if` + expression. To label a conditional, label the block around it (an `if` branch is itself a block). +- `break` and `continue` work only with _loop_ labels, while `return` works only with _block_ + labels. Mixing them (for example `break` on a block label) is a compilation error. + +> The [Labeled Control Flow](./../../reference/control-flow/labeled-control-flow) chapter of the +> Move Reference covers these forms in more detail, including their interaction with macros. + ## Early Return The `return` statement is used to exit a [function](./function) early and return a value. It is diff --git a/book/move-basics/copy-ability.md b/book/move-basics/copy-ability.md index 21ef60e67..4dfce067c 100644 --- a/book/move-basics/copy-ability.md +++ b/book/move-basics/copy-ability.md @@ -4,61 +4,71 @@ description: "The copy ability in Move enables value duplication. Learn how to a # Abilities: Copy -In Move, the _copy_ ability on a type indicates that the instance or the value of the type can be -copied, or duplicated. While this behavior is provided by default when working with numbers or other -primitive types, it is not the default for custom types. Move is designed to express digital assets -and resources, and controlling the ability to duplicate resources is a key principle of the resource -model. However, the Move type system allows you to add the _copy_ ability to custom types: +In the [Ownership and Scope](./ownership-and-scope) section, we saw that primitive values are +_copied_ rather than moved: assigning a number to a new variable leaves both variables usable. +The `copy` ability is precisely what enables this behavior - and while it is built into the +primitive types, it is _not_ the default for custom types. Move is designed to express digital +assets and resources, and a resource that could be freely duplicated would not be much of a +resource. Duplication is therefore something a type must explicitly opt into: ```move file=packages/samples/sources/move-basics/copy-ability.move anchor=copyable ``` -In the example above, we define a custom type `Copyable` with the _copy_ ability. This means that -instances of `Copyable` can be copied, both implicitly and explicitly. +Once a type has the `copy` ability, its values are copied wherever a move would otherwise happen +and the original is still needed - implicitly, without any special syntax. The `copy` keyword can +be used to spell the copy out explicitly: ```move file=packages/samples/sources/move-basics/copy-ability.move anchor=copyable_test ``` -In the example above, `a` is copied to `b` implicitly, and then explicitly copied to `c` using the -dereference operator. If `Copyable` did not have the _copy_ ability, the code would not compile, and -the Move compiler would raise an error. +In the example above, `a` is copied into `b` implicitly - the compiler sees that `a` is used again +afterwards, and copies the value instead of moving it. Then `a` is copied into `c` explicitly with +the `copy` keyword. After the three assignments, there are three independent instances of +`Copyable` - and each of them has to be dealt with separately. -> Note: In Move, destructuring with empty brackets is often used to consume unused variables, -> especially for types without the drop ability. This prevents compiler errors from values going out -> of scope without explicit use. Also, Move requires the type name in destructuring (e.g., -> `Copyable` in `let Copyable {} = a;`) because it enforces strict typing and ownership rules. +> Note the unpacking at the end of the example: `Copyable` has `copy`, but not `drop`, so every +> instance - including each copy - must be used, and the test unpacks all three. Copying a value +> never bypasses the usage rules; it just creates more values to which those rules apply. ## Copying and Drop -The `copy` ability is closely related to the [`drop` ability](./drop-ability). If a type has the -_copy_ ability, it is very likely that it should have `drop` too. This is because the _drop_ ability -is required to clean up resources when the instance is no longer needed. If a type only has _copy_, -managing its instances gets more complicated, as the instances must be explicitly used or consumed. +As the example shows, `copy` without `drop` is a rather inconvenient combination: duplication is +allowed, but every duplicate still demands explicit handling. This is why the two abilities almost +always go together - a value that is cheap to duplicate is, in practice, always fine to discard. +Types that carry plain data, rather than assets, typically declare both: ```move file=packages/samples/sources/move-basics/copy-ability.move anchor=copy_drop ``` -All of the primitive types in Move behave as if they have the _copy_ and _drop_ abilities. This -means that they can be copied and dropped, and the Move compiler will handle the memory management -for them. +All of the primitive types behave as if they have `copy` and `drop`: they are copied on +assignment and discarded without a second thought - with the compiler managing all of it. + +Copying is not the only way to let several parts of a program read the same value. In the +[References](./references) section, we show how a value can be _borrowed_ instead, avoiding the +duplication altogether; and how the [dereference operator](./references#dereferencing) `*` turns a +reference back into a copy, which is only permitted for types with the `copy` ability. ## Types with the `copy` Ability All native types in Move have the `copy` ability. This includes: -- [bool](./../move-basics/primitive-types#booleans) +- [`bool`](./../move-basics/primitive-types#booleans) - [unsigned integers](./../move-basics/primitive-types#integer-types) -- [vector](./../move-basics/vector) -- [address](./../move-basics/address) +- [`vector`](./../move-basics/vector) when `T` has `copy` +- [`address`](./../move-basics/address) All of the types defined in the standard library have the `copy` ability as well. This includes: -- [Option](./../move-basics/option) -- [String](./../move-basics/string) -- [TypeName](./../move-basics/type-reflection) +- [`Option`](./../move-basics/option) when `T` has `copy` +- [`String`](./../move-basics/string) +- [`TypeName`](./../move-basics/type-reflection) + +Just like with [`drop`](./drop-ability#types-with-the-drop-ability), container types are only +copyable when their contents are: a `vector` can be duplicated only if duplicating `T` is +allowed in the first place. ## Further Reading diff --git a/book/move-basics/drop-ability.md b/book/move-basics/drop-ability.md index 5d45c297b..e3a7891d9 100644 --- a/book/move-basics/drop-ability.md +++ b/book/move-basics/drop-ability.md @@ -4,62 +4,52 @@ description: "The drop ability in Move allows struct instances to be discarded. # Abilities: Drop - +In most programming languages, doing nothing with a value is not a problem: an unused variable may +trigger a warning at most, and is forgotten the moment it goes out of scope. In Move, as we saw in +the [Struct](./struct#unpacking-a-struct) section, the default is the opposite: a struct value +must be _used_ - stored somewhere, passed on, or unpacked - and a program that silently discards a +value does not compile. - - - +> The `drop` ability only permits _discarding_ a value. It does not permit copying it or storing +> it - those are governed by the separate [`copy`](./copy-ability) and +> [`store`](./../storage/store-ability) abilities. -The `drop` ability - the simplest of them - allows the instance of a struct to be _ignored_ or -_discarded_. In many programming languages this behavior is considered default. However, in Move, a -struct without the `drop` ability is not allowed to be ignored. This is a safety feature of the Move -language, which ensures that all assets are properly handled. An attempt to ignore a struct without -the `drop` ability will result in a compilation error. +## When to Use `drop` -```move file=packages/samples/sources/move-basics/drop-ability.move anchor=main +A good rule of thumb: `drop` belongs on types that represent _data_, and its absence protects +types that represent _assets_ or _obligations_. -``` +Configuration values, metadata, intermediate results of a computation - none of these are worth +protecting, and forcing the programmer to explicitly destroy each one would be pure ceremony. +Giving such types the `drop` ability keeps the code clean. Collection types are a good example: +because `vector` has `drop` (when its contents do), a vector of numbers can simply be forgotten +when it is no longer needed. -The `drop` ability is often used on custom collection types to eliminate the need for special -handling of the collection when it is no longer needed. For example, a `vector` type has the `drop` -ability, which allows the vector to be ignored when it is no longer needed. However, the biggest -feature of Move's type system is the ability to not have `drop`. This ensures that the assets are -properly handled and not ignored. +The absence of `drop`, on the other hand, is one of the defining features of Move's type system. +A coin, a ticket, a receipt, an obligation to repay - a value like this must never silently +vanish, and a type without `drop` gives that guarantee at the compiler level: whoever holds the +value is _forced_ to do something meaningful with it. The compiler-enforced handling of values is +the foundation of the [Hot Potato pattern](./../programmability/hot-potato-pattern) mentioned in +the [previous section](./abilities-introduction#no-abilities), and we explore the full rules of +how values move between scopes in the [Ownership and Scope](./ownership-and-scope) section. -A struct with a single `drop` ability is called a _Witness_. We explain the concept of a _Witness_ -in the [Witness and Abstract Implementation](./../programmability/witness-pattern) section. +> A struct with `drop` as its single ability is called a _Witness_. We explain the concept of a +> _Witness_ in the [Witness and Abstract Implementation](./../programmability/witness-pattern) +> section. ## Types with the `drop` Ability @@ -76,6 +66,11 @@ All of the types defined in the standard library have the `drop` ability as well - [`String`](./../move-basics/string) - [`TypeName`](./../move-basics/type-reflection) +Note the pattern in the list: a container type like `vector` or `Option` can only be dropped when +its contents can. If the elements of a vector are protected from being discarded, the vector +holding them is protected too - otherwise dropping the container would be a loophole for dropping +the contents. + ## Further Reading - [Type Abilities](./../../reference/abilities) in the Move Reference. diff --git a/book/move-basics/enum-and-match.md b/book/move-basics/enum-and-match.md index 10506bd71..8c7af6615 100644 --- a/book/move-basics/enum-and-match.md +++ b/book/move-basics/enum-and-match.md @@ -6,14 +6,14 @@ description: "Enums and pattern matching in Move: define variant types, use matc An enum is a user-defined data structure that, unlike a [struct](./struct), can represent multiple variants. Each variant can contain primitive types, structs, or other enums. However, recursive enum -definitions — similar to recursive struct definitions — are not allowed. +definitions - similar to recursive struct definitions - are not allowed. ## Definition An enum is defined using the `enum` keyword, followed by optional abilities and a block of variant definitions. Each variant has a tag name and may optionally include either positional values or -named fields. Enum must have at least one variant. The structure of each variant is not flexible, -and the total number of variants can be relatively large - up to 100. +named fields. An enum must have at least one variant; the shape of each variant is fixed at +definition, and the total number of variants can be relatively large - up to 100. ```move file=packages/samples/sources/move-basics/enum-and-match.move anchor=definition @@ -31,7 +31,7 @@ abilities, and 3 variants: Enums are _internal_ to the module in which they are defined. This means an enum can only be constructed, read, and unpacked within the same module. -[Similar to structs](./struct#create-and-use-an-instance), enums are instantiated by specifying the +[Similar to structs](./struct#creating-an-instance), enums are instantiated by specifying the type, the variant, and the values for any fields defined in that variant. ```move file=packages/samples/sources/move-basics/enum-and-match.move anchor=constructors @@ -50,10 +50,13 @@ single type. To demonstrate this, let’s define a struct that contains a vector ``` -All variants of the Segment enum share the same type – `Segment` – which allows us to create a +All variants of the Segment enum share the same type - `Segment` - which allows us to create a homogeneous vector containing instances of different variants. This kind of flexibility is not achievable with structs, as each struct defines a single, fixed shape. +> `Segments` is a [positional struct](./struct#positional-structs) wrapping a single +> `vector` field; note how its abilities are declared after the parentheses. + ## Pattern Matching Unlike structs, enums require special handling when it comes to accessing the inner value or @@ -69,7 +72,7 @@ using the `match` expression, followed by the matched value in parenthesis and t arms_, defining the pattern and expression to be performed if the pattern is right. Let's extend our example by adding a set of `is_variant`-like functions, so external packages can -check the variant. Starting with `is_empty`. +check the variant, starting with `is_empty`: ```move file=packages/samples/sources/move-basics/enum-and-match.move anchor=is_empty @@ -85,7 +88,7 @@ use them, marking unused values with `_` to avoid compiler warnings). ### Trick #1 - _any_ Condition The Move compiler infers the type of the value used in a `match` expression and ensures that the -_match arms_ are exhaustive – that is, all possible variants or values must be covered. +_match arms_ are exhaustive - that is, all possible variants or values must be covered. However, in some cases, such as matching on a primitive value or a collection like a vector, it's not feasible to list every possible case. For these situations, match supports a wildcard pattern @@ -108,7 +111,7 @@ Similarly, we can use the same approach to define `is_special` and `is_string`: ### Trick #2 - `try_into` Helpers With the addition of `is_variant` functions, we enabled external modules to check which variant an -enum instance represents. However, this is often not enough – external code still cannot access the +enum instance represents. However, this is often not enough - external code still cannot access the inner value of a variant due to enums being internal to their module. A common pattern for addressing this is to define `try_into` functions. These functions match on the @@ -118,23 +121,29 @@ value and return an `Option` containing the inner contents if the `match` succee ``` -This pattern safely exposes internal data in a controlled way, avoiding abort. +This pattern safely exposes internal data in a controlled way, without the risk of an abort. ### Trick #3 - Matching on Primitive Values -The `match` expression in Move can be used with values of any type – enums, structs, or primitives. +The `match` expression in Move can be used with values of any type - enums, structs, or primitives. To demonstrate this, let’s implement a `to_string` function that creates a new `String` from a `Segment`. In the case of the `Special` variant, we will match on the `encoding` field to determine -how to decode the content. +how to interpret the content: `0` stands for UTF-8, and `1` for the stricter ASCII encoding. ```move file=packages/samples/sources/move-basics/enum-and-match.move anchor=to_string ``` -This function demonstrates two key things: +This function demonstrates several key things: - Nested `match` expressions can be used for deeper logic branching. - Wildcards are essential for covering all possible values in primitive types like `u8`. +- The function takes `s` by reference, but matching arms bind inner values _by value_. The `*s` + expression makes this possible: the [dereference operator](./references#dereferencing) `*` + copies the value behind the reference, which is allowed because `Segment` has the `copy` + ability. +- The wildcard arm uses `abort` without an abort code to reject unknown encodings - a _clean + abort_, covered in the [Aborting Execution](./assert-and-abort) section. ## The Final Test @@ -166,4 +175,4 @@ To learn more about enums and pattern matching, refer to the resources listed in ## Further Reading - [Enums](./../../reference/enums) in the Move Reference -- [Pattern Matching](/reference/control-flow/pattern-matching) in the Move Reference +- [Pattern Matching](./../../reference/control-flow/pattern-matching) in the Move Reference diff --git a/book/move-basics/expression.md b/book/move-basics/expression.md index bc002e977..39e83ff76 100644 --- a/book/move-basics/expression.md +++ b/book/move-basics/expression.md @@ -9,7 +9,7 @@ everything is an expression, with the sole exception of the `let` statement, whi In this section, we cover the types of expressions and introduce the concept of scope. > Expressions are sequenced with semicolons `;`. If there's "no expression" after the semicolon, the -> compiler will insert a `unit ()`, which represents an empty expression. +> compiler will insert a _unit_ `()` - a value that represents an empty expression. ## Literals @@ -23,6 +23,9 @@ functions. Move has the following literals: - Hexadecimal values: Numbers prefixed with 0x to represent integers, such as `0x0`, `0x1`, `0x123` - Byte vector values: Prefixed with `b`, such as `b"bytes_vector"` - Byte values: Hexadecimal literals prefixed with `x`, such as `x"0A"` +- String values: Double-quoted text, such as `"hello"`. Unlike other literals, the type of a string + literal is inferred from the context - it can be a `vector` or one of the two standard string + types. Strings are covered in detail in the [String](./string) section. ```move file=packages/samples/sources/move-basics/expression.move anchor=literals @@ -31,7 +34,8 @@ functions. Move has the following literals: ## Operators Arithmetic, logical, and bitwise operators are used to perform operations on values. Since these -operations produce values, they are considered expressions. +operations produce values, they are considered expressions. The integer operators - and when they +abort - are listed in the [Primitive Types](./primitive-types#operations) section. ```move file=packages/samples/sources/move-basics/expression.move anchor=operators @@ -47,12 +51,16 @@ semicolon). A block is an expression, so it can be used anywhere an expression i ``` +A block also delimits _scope_: a variable declared inside a block exists only until the block's +closing brace. What exactly happens to values when their scope ends is an important question in +Move, and the [Ownership and Scope](./ownership-and-scope) section is devoted to it. + ## Function Calls -We go into detail about functions in the [Functions](./function) section. However, we have already -used function calls in previous sections, so it's worth mentioning them here. A function call is an -expression that calls a function and returns the value of the last expression in the function body, -provided the last expression does not have a terminating semicolon. +We go into detail about functions in the very next section - [Functions](./function). Here, it is +enough to say that a function call is an expression: it calls a function and returns the value of +the last expression in the function body, provided the last expression does not have a terminating +semicolon. ```move file=packages/samples/sources/move-basics/expression.move anchor=fun_call @@ -67,3 +75,8 @@ section. Here's a very brief overview: ```move file=packages/samples/sources/move-basics/expression.move anchor=control_flow ``` + +## Further Reading + +- [Equality](./../../reference/equality) in the Move Reference. +- [Control Flow](./../../reference/control-flow) in the Move Reference. diff --git a/book/move-basics/function.md b/book/move-basics/function.md index 35b1125a8..cad3e67b6 100644 --- a/book/move-basics/function.md +++ b/book/move-basics/function.md @@ -8,7 +8,8 @@ Functions are the building blocks of Move programs. They are called from [user transactions](./../concepts/what-is-a-transaction) and from other functions and group executable code into reusable units. Functions can take arguments and return a value. They are declared with the `fun` keyword at the module level. Just like any other module member, by default -they're private and can only be accessed from within the module. +they're private and can only be accessed from within the module; making them visible to other +modules is the topic of the [Visibility Modifiers](./visibility) section, later in this chapter. ```move file=packages/samples/sources/move-basics/function.move anchor=math @@ -16,37 +17,39 @@ they're private and can only be accessed from within the module. In this example, we define a function `add` that takes two arguments of type `u64` and returns their sum. The `test_add` function, located in the same module, is a test function that calls `add`. The -test uses the `assert!` macro to compare the result of `add` with the expected value. If the -condition inside `assert!` evaluates to false, the execution is aborted automatically. +test uses the `assert_eq!` macro to compare the result of `add` with the expected value. If the +two values differ, the execution is aborted automatically. -## Function declaration +## Function Declaration > In Move, functions are typically named using the `snake_case` convention. This means function > names should be all lowercase, with words separated by underscores. Examples include > `do_something`, `add`, `get_balance`, `is_authorized`, and so on. A function is declared with the `fun` keyword followed by the function name (a valid Move -identifier), a list of arguments in parentheses, and a return type. The function body is a block of -code that contains a sequence of statements and expressions. The last expression the function body -is the return value of the function. +identifier), a list of arguments in parentheses, and a return type. The function body is a +[block](./expression#blocks), and, like in any block, the last expression without a semicolon is +the function's return value. The `return` keyword allows returning early - it is covered with the +other [control flow](./control-flow) expressions. ```move file=packages/samples/sources/move-basics/function.move anchor=return_nothing ``` -## Accessing functions +## Accessing Functions Just like other module members, functions can be imported and accessed using a path. The path consists of the module path and the function name, separated by ::. For example, if you have a function named `add` in the `math` module within the `book` package, its full path would be -`book::math::add`. If the module has already been imported, you can access it directly as -`math::add` as in the following example: +`book::math::add`. If the module has already been imported - imports are covered in the +[Importing Modules](./importing-modules) section - you can access it directly as `math::add`, as +in the following example: ```move file=packages/samples/sources/move-basics/function_use.move anchor=use_math ``` -## Multiple return values +## Multiple Return Values Move functions can return multiple values, which is particularly useful when you need to return more than one piece of data from a function. The return type is specified as a tuple of types, and the @@ -70,7 +73,7 @@ variable name: ``` -If some of the arguments are not used, they can be ignored with the `_` symbol: +If some of the returned values are not needed, they can be ignored with the `_` symbol: ```move file=packages/samples/sources/move-basics/function.move anchor=tuple_return_ignore diff --git a/book/move-basics/generics.md b/book/move-basics/generics.md index 7e3590813..ed5819980 100644 --- a/book/move-basics/generics.md +++ b/book/move-basics/generics.md @@ -4,127 +4,163 @@ description: "Generics in Move: write reusable functions and types that work wit # Generics -Generics are a way to define a type or function that can work with any type. This is useful when you -want to write a function which can be used with different types, or when you want to define a type -that can hold any other type. Generics are the foundation of many advanced features in Move -including collections, abstract implementations, and more. +Generics are a way to define a type or function that can work with any type, instead of one +specific type. You have already used generics in this chapter, perhaps without noticing: the +[vector](./vector) type is generic - a single definition can hold elements of any type - and so is +[Option](./option), which can wrap any value. Generics are the foundation of collections, abstract +implementations, and many advanced features of Move. -## In the Standard Library +## The Problem Generics Solve -In this chapter we already mentioned the [vector](./vector) type, which is a generic type that can -hold any other type. Another example of a generic type in the standard library is the -[Option](./option) type, which is used to represent a value that may or may not be present. +Suppose we need a type that wraps a single `u64` value. Simple enough: + +```move +public struct U64Container has drop { + value: u64, +} +``` + +But what if we also need to wrap a `bool`? And a `String`? And a struct of our own? Each version +would be identical except for the type of the `value` field, and every function that works with +containers would need to be duplicated for each version: + +```move +public struct BoolContainer has drop { value: bool } +public struct StringContainer has drop { value: String } +// ...a new struct for every type we want to store +``` + +Generics solve exactly this problem: we define the container _once_, with a placeholder instead of +a concrete type, and the placeholder is filled in when the type is used. ## Generic Syntax -To define a generic type or function, a type signature needs to have a list of generic parameters -enclosed in angle brackets (`<` and `>`). The generic parameters are separated by commas. +To define a generic type or function, add a list of _type parameters_ enclosed in angle brackets +(`<` and `>`) after the name. Multiple type parameters are separated by commas. ```move file=packages/samples/sources/move-basics/generics.move anchor=container ``` -In the example above, `Container` is a generic type with a single type parameter `T`, the `value` -field of the container stores the `T`. The `new` function is a generic function with a single type -parameter `T`, and it returns a `Container` with the given value. Generic types must be initialized -with a concrete type, and generic functions must be called with a concrete type, although in some -cases the Move compiler can infer the correct type. +In the example above, `Container` is a generic type with a single type parameter `T`, and the +`value` field of the container stores a value of type `T`. `T` is not a real type - it is a +placeholder that stands for "some type, to be specified later". The `new` function is a generic +function with the same type parameter, and it returns a `Container` with the given value. + +> By convention, type parameters are named with single capital letters - `T`, `U`, `K`, `V`. +> However, any valid name can be used: the standard library, for example, names the type parameter +> of `vector` `Element`. + +## Using Generic Types + +When we create an instance of a generic type, the placeholder is replaced with a concrete type. +Each replacement produces a distinct type: `Container`, `Container`, and +`Container` all come from the same definition, but they are three different types. + +The concrete type can be spelled out explicitly, or, in most cases, inferred by the compiler: ```move file=packages/samples/sources/move-basics/generics.move anchor=test_container ``` -In the test function `test_container`, we demonstrate three equivalent ways to create a new -`Container` with a `u8` value. Because numeric constants have ambiguous types, we must specify the -type of the number literal somewhere (in the type of the container, the parameter to `new`, or the -number literal itself); once we specify one of these the compiler can infer the others. +The first three lines of the test are equivalent - each creates a `Container`. Because numeric +literals have ambiguous types, we have to specify the type of the number somewhere: in the type +annotation of the variable, in the explicit type argument of `new`, or in the literal itself. Once +one of these is given, the compiler infers the rest. For values with unambiguous types, such as +`bool` or `String`, no annotations are needed at all. ## Multiple Type Parameters -You can define a type or function with multiple type parameters. The type parameters are separated -by commas. +A type or function can have more than one type parameter, separated by commas: ```move file=packages/samples/sources/move-basics/generics.move anchor=pair ``` In the example above, `Pair` is a generic type with two type parameters `T` and `U`, and the -`new_pair` function is a generic function with two type parameters `T` and `U`. The function returns -a `Pair` with the given values. The order of the type parameters is important, and should match the -order of the type parameters in the type signature. +`new_pair` function creates a `Pair` with the given values. ```move file=packages/samples/sources/move-basics/generics.move anchor=test_pair ``` -If we added another instance where we swapped type parameters in the `new_pair` function, and tried -to compare two types, we'd see that the type signatures are different, and cannot be compared. +The order of type parameters matters. A `Pair` and a `Pair` are two different, +incompatible types - even though they are built from the same definition and store the same data: ```move file=packages/samples/sources/move-basics/generics.move anchor=test_pair_swap ``` -Since the types for `pair1` and `pair2` are different, the comparison `pair1 == pair2` will not -compile. +Since the types of `pair1` and `pair2` differ, the comparison `pair1 == pair2` would not compile. +The values can only be compared field-by-field, after unpacking. ## Why Generics? -In the examples above we focused on instantiating generic types and calling generic functions to -create instances of these types. However, the real power of generics lies in their ability to define -shared behavior for the base, generic type, and then use it independently of the concrete types. -This is especially useful when working with collections, abstract implementations, and other -advanced features in Move. +So far we have focused on the mechanics: how to define generic types and create their instances. +The real power of generics is in defining shared data and behavior once, and letting a part of the +type vary. Consider a `User` type where the `name` and `age` fields are always the same, but +different applications need to attach different extra data: ```move file=packages/samples/sources/move-basics/generics.move anchor=user ``` -In the example above, `User` is a generic type with a single type parameter `T`, with shared fields -`name`, `age`, and the generic `metadata` field, which can store any type. No matter what `metadata` -is, all instances of `User` will contain the same fields and methods. +Functions defined for `User` work no matter what `metadata` is - they operate on the shared +fields and don't need to know the concrete type of `T`: ```move file=packages/samples/sources/move-basics/generics.move anchor=update_user ``` +```move file=packages/samples/sources/move-basics/generics.move anchor=test_user + +``` + +In the test above, one `User` instance stores a `u64` as its metadata, and the other stores a +`bool`, yet both are updated with the same `update_name` function, defined once. + ## Phantom Type Parameters -In some cases, you may want to define a generic type with a type parameter that is not used in the -fields or methods of the type. This is called a _phantom type parameter_. Phantom type parameters -are useful when you want to define a type that can hold any other type, but you want to enforce some -constraints on the type parameter. +Sometimes a type parameter is needed only as a _label_, without storing any value of that type. +Consider a `Coin` type: the actual data is just a numeric `value`, the same for every currency. +However, a US Dollar coin and a Euro coin must never be mixed up - they should be different types +in the eyes of the compiler. To express this, the type parameter is declared `phantom` - a +parameter that does not appear in any field: ```move file=packages/samples/sources/move-basics/generics.move anchor=phantom ``` -The `Coin` type here does not contain any fields or methods that use the type parameter `T`. It is -used to differentiate between different types of coins, and to enforce some constraints on the type -parameter `T`. +> Move requires every regular type parameter to be used in the fields of the struct. Since `T` is +> not stored anywhere in `Coin`, it must be marked with the `phantom` keyword. + +Currencies can then be defined as empty structs - they carry no data and exist only to be used as +labels: ```move file=packages/samples/sources/move-basics/generics.move anchor=test_phantom ``` -In the example above, we demonstrate how to create two different instances of `Coin` with different -phantom type parameters `USD` and `EUR`. The type parameter `T` is not used in the fields or methods -of the `Coin` type, but it is used to differentiate between different types of coins. This helps -ensure that the `USD` and `EUR` coins are not mistakenly mixed up. +Even though `Coin` and `Coin` store identical data, they are different types, and a +function expecting one will not accept the other. This pattern is used extensively in real +applications: the `Coin` type in the [Sui Framework](./../programmability/sui-framework) is defined +in exactly this way. ## Constraints on Type Parameters -Type parameters can be constrained to have certain abilities. This is useful when you need the inner -type to allow certain behaviors, such as _copy_ or _drop_. The syntax for constraining a type -parameter is `T: + `. +By default, a type parameter accepts _any_ type. However, sometimes the inner type must allow +certain behaviors, such as being copied or discarded, and for that the type parameter can be +constrained to have certain [abilities](./abilities-introduction). The syntax is +`T: + `: ```move file=packages/samples/sources/move-basics/generics.move anchor=constraints ``` -The Move Compiler will enforce that the type parameter `T` has the specified abilities. If the type -parameter does not have the specified abilities, the code will not compile. - - +A constraint is a promise the concrete type must keep: the Move compiler only allows instantiating +`Droppable` with types that have the [drop](./drop-ability) ability, and `CopyableDroppable` +with types that have both [copy](./copy-ability) and `drop`. A type without those abilities does +not compile: ```move file=packages/samples/sources/move-basics/generics.move anchor=test_constraints diff --git a/book/move-basics/importing-modules.md b/book/move-basics/importing-modules.md index ce2a59a3b..8e2cf706a 100644 --- a/book/move-basics/importing-modules.md +++ b/book/move-basics/importing-modules.md @@ -4,25 +4,6 @@ description: "How to import modules in Move using the use keyword: single import # Importing Modules - - - - Move achieves high modularity and code reuse by allowing module imports. Modules within the same package can import each other, and a new package can depend on already existing packages and use their modules too. This section will cover the basics of importing modules and how to use them in @@ -71,8 +52,8 @@ Importing function names is less common in Move, since the function names can ov confusion. A recommended practice is to import the entire module and use the module path to access the function. Types have unique names and should be imported individually. -To import members and the module itself in the group import, you can use the `Self` keyword. The -`Self` keyword refers to the module itself and can be used to import the module and its members. +To import both the module itself and some of its members in one group import, use the `Self` +keyword, which stands for the module: ```move file=packages/samples/sources/move-basics/importing-modules-self.move anchor=self @@ -106,13 +87,11 @@ Local = { local = "../my_other_package" } The `dependencies` section contains an entry for each package dependency. The key of the entry is the name of the package (`Example` or `Local` in the example), and the value is either a git import table or a local path. The git import contains the URL of the package, the subdirectory where the -package is located, and the revision of the package. The local path is a relative path to the qa +package is located, and the revision of the package. The local path is a relative path to the package directory. -If you add a dependency, all of its dependencies also become available to your package. - -If a dependency is added to the `Move.toml` file, the compiler will automatically fetch (and later -refetch) the dependencies when building the package. +The compiler automatically fetches (and later refetches) the listed dependencies when building the +package, and all of their dependencies become available to your package as well. > Starting with version 1.45 of the sui CLI, the system packages are automatically included as > dependencies for all packages if they are not present in `Move.toml`. Therefore, `MoveStdlib`, @@ -135,3 +114,7 @@ module path consists of the package address (or alias) and the module name, sepa > Note: Module address names come from the `[addresses]` section of the manifest file (`Move.toml`), > not the names used in the `[dependencies]` section. + +## Further Reading + +- [Uses and Aliases](./../../reference/uses) in the Move Reference. diff --git a/book/move-basics/index.md b/book/move-basics/index.md index 047479326..1719eecf2 100644 --- a/book/move-basics/index.md +++ b/book/move-basics/index.md @@ -4,10 +4,32 @@ description: "Learn Move language fundamentals: types, modules, functions, struc # Move Basics -This chapter is all about the basic syntax of the Move language. It covers the basics of the -language, such as types, modules, functions, and control flow. It focuses on the language without a -storage model or a blockchain, and explains the essential concepts of the language. To learn -features specific to Sui, such as storage functions and abilities, refer to the -[Using Objects](./../storage/) chapter, however, it is recommended to start with this chapter first. +This chapter covers the foundations of the Move language: the syntax, the type system, and the +concepts that every Move program is built from. It focuses on the language itself and mostly sets +the blockchain aside - everything here applies to any Move program, and the features specific to +storage and Sui are covered right after, starting with the [Object Model](./../object/) chapter. - +The sections build on one another and are meant to be read in order: + +- **How code is organized:** [modules](./module), [comments](./comments), + [primitive types](./primitive-types), the [address type](./address), + [expressions](./expression), and [functions](./function). +- **Defining custom types:** [structs](./struct), and the + [ability system](./abilities-introduction) that controls what values of a type can do - + starting with [drop](./drop-ability). +- **Reusing existing code:** [imports](./importing-modules) and the + [Standard Library](./standard-library) with its core types - [vector](./vector), + [Option](./option), and [String](./string). +- **Writing logic:** [control flow](./control-flow), [enums with pattern + matching](./enum-and-match), [struct methods](./struct-methods), and + [visibility modifiers](./visibility). +- **The core of Move's safety story:** [ownership and scope](./ownership-and-scope), the + [copy ability](./copy-ability), [constants](./constants) and + [aborting execution](./assert-and-abort), and [references](./references). +- **Abstraction tools:** [generics](./generics), [macro functions](./macros), + [internal permits](./internal-permit), [type reflection](./type-reflection), and, finally, + [testing](./testing). + +Every code sample in this chapter comes from a compiling, tested package. Most samples are +excerpts placed inside test functions, so you can copy any of them into the package created in the +[Hello World](./../your-first-move/hello-world) chapter and run them with `sui move test`. diff --git a/book/move-basics/internal-permit.md b/book/move-basics/internal-permit.md new file mode 100644 index 000000000..72d4d9713 --- /dev/null +++ b/book/move-basics/internal-permit.md @@ -0,0 +1,159 @@ +--- +description: "The std::internal module in Move: use Permit to restrict generic function calls to the module that defines the type T." +--- + +# Internal Permit + +In the [Custom Types with Struct](./struct#field-visibility) section we established a rule that +holds everywhere in Move: only the module that defines a type can access its fields, pack it, and +unpack it. This makes the defining module the sole authority over its type - all other code has to +go through the functions the module chooses to expose. + +However, this authority seems to disappear the moment a generic function enters the picture. A +public generic function can be called by _any_ module with _any_ type argument - the library that +defines the function has no way of knowing whether the caller has any relation to the type it was +called with. The `std::internal` module closes this gap: it provides a value that proves the call +was authorized by the module that defines the type. + +## The Problem + +Let's make the problem concrete. Suppose we want to build a type registry - a place where a type can +be registered under a human-readable name. A natural requirement: a type may only be registered by +the module that defines it, so no one can claim a name for someone else's type. + +A first attempt at the signature would look like this: + +```move +/// Registers the type `T` under the given `name`. +public fun register(registry: &mut Registry, name: String) { /* ... */ } +``` + +This function cannot enforce our requirement. Move has no way to inspect the caller at runtime - +there is no "get the calling module" function, and this is by design: what a function does must be +fully determined by its arguments. But that phrasing also points at the solution: if authorization +cannot be observed, it must be _passed in_ - as an argument that only the right module is able to +produce. + +## The Permit Type + +The `std::internal` module is tiny - it defines one struct and one function: + +```move +module std::internal; + +/// A privileged witness of the `T` type. +/// Instances can only be created by the module that defines the type `T`. +public struct Permit() has drop; + +/// Construct a new `Permit` for the type `T`. +/// Can only be called by the module that defines the type `T`. +public fun permit(): Permit { Permit() } +``` + +At first glance, there is nothing here: a public struct with no fields and a public function that +anyone should be able to call. The important part is the claim in the comment - `permit()` can +only be called by the module that defines `T`. Regular Move code cannot express such a restriction, +and indeed it is not expressed in the code: it is a special rule, checked by the compiler and by the +network when the package is published. We will see it in action in a moment. + +Two details of the definition are worth noting: + +- The type parameter is [phantom](./generics#phantom-type-parameters) - a `Permit` does not + contain a `T`, so a permit can be created for a type without constructing an instance of it. +- The only ability is `drop`: a permit can be discarded, but it cannot be copied and cannot be + stored. Whoever receives a `Permit` holds a proof that cannot be duplicated or stashed away for + later. + +## Using a Permit + +To put the rule to work, a library function lists `Permit` as an argument. That is the entire +recipe: since only the module defining `T` can create the value, receiving it _is_ the +authorization. Here is the registry from our problem statement, fixed: + +```move file=packages/samples/sources/move-basics/internal-permit.move anchor=registry + +``` + +The `register` function does not even look at the permit - the underscore in `_permit` says it is +intentionally unused. Its type is the check. + +> `std::internal`, like `std::option` and `std::vector`, is +> [imported implicitly](./standard-library#implicit-imports) - no `use` statement is needed. The +> recommended style is to keep the module prefix: write `internal::Permit` in signatures and +> `internal::permit()` at call sites, instead of importing `Permit` directly. + +On the other side, the module that defines a type creates a permit and passes it along: + +```move file=packages/samples/sources/move-basics/internal-permit-2.move anchor=use_permit + +``` + +The registration can now be exercised in a test: + +```move file=packages/samples/sources/move-basics/internal-permit-2.move anchor=test + +``` + +## Breaking the Rule + +What stops a third module from creating a permit for `MyApp` and registering it under a misleading +name? Let's try: + +```move +module book::registry_intruder; + +use book::registry_user::MyApp; +use book::type_registry::Registry; + +public fun register_foreign_type(registry: &mut Registry) { + let permit = internal::permit(); // ERROR! + registry.register(permit, "Not My App"); +} +``` + +The code above will not compile: + +```text +error[Sui E02011]: invalid internal permit call + ┌─ sources/registry_intruder.move:7:18 + │ +7 │ let permit = internal::permit(); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ │ + │ │ The type 'book::registry_user::MyApp' is not declared in the current module + │ Invalid call to an internal function. The function 'std::internal::permit' is + │ restricted to being called in the module that defines the type, 'book::registry_user' +``` + +The check does not stop at the compiler. The same rule is enforced by the bytecode verifier when a +package is published onchain, so it cannot be bypassed by hand-crafting bytecode or using a +modified compiler. A published `Permit` is a hard guarantee: if a function received one, the +module defining `T` created it. + +Type parameters restricted this way are called _internal type parameters_, and `permit` is not the +only function that has one: `sui::event::emit` and `sui::transfer::transfer`, which we cover +in the [Events](./../programmability/events) and +[Storage Functions](./../storage/storage-functions) sections, follow the same rule. What +`std::internal` adds is a way for _any_ library to demand this guarantee: the special rule applies +only to the creation of the permit, and from there it travels as an ordinary value to any function +that lists it as an argument. + +## Why It Works This Way + +The design of `Permit` follows a general Move principle: authority is represented by values, not by +runtime checks. A function proves it is allowed to do something by _possessing_ a value that could +only be created in an authorized place. This idea appears throughout Move and Sui - it is the basis +of the [Witness](./../programmability/witness-pattern) and +[Capability](./../programmability/capability) patterns - and `Permit` is its most compact form: a +standard, zero-field witness meaning "the module that defines `T` approved this call". + +The abilities of `Permit` are chosen to keep that meaning precise. Without `copy`, a function that +receives a permit cannot duplicate it; without `store`, it cannot be kept onchain and reused later. +The authorization is valid for the current call and then gone - every privileged action requires the +defining module to explicitly create a new permit. And because the type parameter is `phantom`, the +proof is free: no instance of `T` is created, copied, or consumed to produce it. + +## Further Reading + +- [std::internal](https://docs.sui.io/references/framework/std/internal) module documentation. +- [Witness Pattern](./../programmability/witness-pattern) - the broader pattern behind `Permit`. diff --git a/book/move-basics/macros.md b/book/move-basics/macros.md new file mode 100644 index 000000000..5c864c65c --- /dev/null +++ b/book/move-basics/macros.md @@ -0,0 +1,113 @@ +--- +description: "Macro functions in Move: compile-time expanded functions with lambda arguments - how to use standard library macros and define your own." +--- + +# Macro Functions + +Throughout this chapter, we have called quite a few functions whose names end with an exclamation +mark: the `assert!` and `assert_eq!` macros in tests, and the +[vector macros](./vector#vector-macros) such as `map!` and `fold!`. All of them are _macro +functions_, and now that we know [functions](./function) and [generics](./generics), we have +everything needed to understand how they work - and how to define our own. + +## What is a Macro Function? + +A macro function looks and feels like a regular function, but it does not exist at runtime. +Instead, the compiler _expands_ the macro: at every call site, the body of the macro is substituted +inline, with the arguments plugged into it, and only then is the resulting code type checked and +compiled. A macro call is easy to recognize - the macro name is always followed by the `!` mark. + +This compile-time expansion gives macros two abilities that regular functions do not have: + +- They can take _lambdas_ - inline blocks of code - as arguments. Move has no function values at + runtime, but because a macro is expanded during compilation, the lambda simply becomes part of + the generated code. +- Their bodies are type checked _after_ expansion, per call site, which permits operations that + regular [generics](./generics) cannot express - as we are about to see. + +## Defining a Macro + +A macro is defined with the `macro fun` keywords. The parameters - including type parameters - are +prefixed with the `$` sign, marking them as compile-time substitutions rather than runtime values: + +```move file=packages/samples/sources/move-basics/macros.move anchor=max + +``` + +The `max` macro returns the larger of its two arguments. Note something remarkable about the body: +it compares two values of the generic type `$T` with the `>` operator. A regular generic function +could not do this - there is no ability constraint for "comparable", so `fun max(a: T, b: T)` +would not compile. The macro sidesteps the problem entirely: by the time the body is type checked, +`$T` is already replaced with a concrete type at each call site: + +```move file=packages/samples/sources/move-basics/macros.move anchor=max_use + +``` + +> Also note the `let a = $a;` binding at the top of the body. A macro argument is substituted as an +> _expression_, not as a computed value: every occurrence of `$a` in the body would evaluate the +> argument expression again. Binding the argument to a local variable once is a good habit that +> avoids surprising double evaluation. + +## Lambda Arguments + +The real power of macros comes from lambda parameters. A lambda type is written as +`|argument_types|` (or `|argument_types| -> return_type` when it returns a value), and the caller +passes the lambda inline, using the `|arguments| expression` syntax: + +```move file=packages/samples/sources/move-basics/macros.move anchor=repeat + +``` + +```move file=packages/samples/sources/move-basics/macros.move anchor=repeat_use + +``` + +A lambda can read and even modify the variables of the enclosing scope - the `repeat!` call above +updates the local variable `sum` on every iteration. This is exactly the mechanism behind the +[vector macros](./vector#vector-macros): `v.do!(|el| ...)` is a macro with a lambda parameter, +expanded into a plain loop at compilation time. + +## Lazy Evaluation + +Because arguments are substituted rather than computed up front, an argument expression may be +evaluated once, many times - or not at all. The `assert!` macro is a good illustration: in +`assert!(condition, EMyError)`, the error code expression is only evaluated when the condition +fails. This is a feature - the failure branch costs nothing on the happy path - but it is also the +flip side of the double-evaluation caveat above: when writing your own macros, think about how many +times each `$` parameter is actually used. + +> Expansion at the call site has one more visible effect: an abort raised inside a macro body +> reports the line number of the macro _call_, not a line inside the macro definition. This is part +> of the [clever error](./assert-and-abort#error-messages) encoding, and it is why a failing +> `assert!` or `assert_eq!` points at the line in your code rather than somewhere in the standard +> library - a good reason to prefer a macro over a regular function when writing assertion helpers. + +## Macros in the Standard Library + +The [Standard Library](./standard-library) makes heavy use of macros, and they are the idiomatic +way to work with its core types. We have already seen the [vector macros](./vector#vector-macros); +`Option` and the integer types have their own sets: + +```move file=packages/samples/sources/move-basics/macros.move anchor=std_macros + +``` + +A quick overview of where to find them: + +- [std::vector](https://docs.sui.io/references/framework/std/vector) - `do!`, `map!`, `filter!`, + `fold!`, `count!`, `any!`, `all!`, `tabulate!`, and more; +- [std::option](https://docs.sui.io/references/framework/std/option) - `do!`, `map!`, + `destroy_or!`, `extract_or!`, `is_some_and!`; +- integer modules, e.g. [std::u64](https://docs.sui.io/references/framework/std/u64) - `do!`, + `range_do!`, `max_value!`; +- [std::unit_test](https://docs.sui.io/references/framework/std/unit_test) - `assert_eq!` and + `assert_ref_eq!`, available in tests. + +This section covers the day-to-day use of macros; the full feature set - including method syntax +for macros, `$` expressions in type positions, and hygiene rules - is described in the Move +Reference. + +## Further Reading + +- [Macro Functions](./../../reference/functions/macros) in the Move Reference. diff --git a/book/move-basics/module.md b/book/move-basics/module.md index d99b102a0..e853fdb03 100644 --- a/book/move-basics/module.md +++ b/book/move-basics/module.md @@ -1,30 +1,22 @@ --- -description: "Modules are the building blocks of Move: learn how to declare, organize, and compile modules in your Sui smart contracts." +description: + 'Modules are the building blocks of Move: learn how to declare, organize, and compile modules in + your Sui smart contracts.' --- # Module - - A module is the base unit of code organization in Move. Modules are used to group and isolate code, -and all members of the module are private to the module by default. In this section you will learn -how to define a module, declare its members, and access it from other modules. +and all members of the module are private to the module by default. This makes the module a boundary +of trust: as later sections will show, only the module that defines a type can create, modify, and +destroy its values. In this section you will learn how to define a module, declare its members, and +access it from other modules. ## Module Declaration -Modules are declared using the `module` keyword followed by the package address, module name, -semicolon, and the module body. The module name should be in `snake_case` - all lowercase letters -with underscores between words. Module names must be unique in the package. +Modules are declared using the `module` keyword followed by the package address and the module name, +separated by `::`, then a semicolon and the module body. The module name should be in `snake_case` - +all lowercase letters with underscores between words. Module names must be unique in the package. Usually, a single file in the `sources/` folder contains a single module. The file name should match the module name - for example, a `donut_shop` module should be stored in the `donut_shop.move` file. @@ -38,48 +30,45 @@ You can read more about coding conventions in the ``` -Structs, functions, constants and imports are all part of the module: - -- [Structs](./struct) -- [Functions](./function) -- [Constants](./constants) -- [Imports](./importing-modules) -- [Struct Methods](./struct-methods) - ## Address and Named Address -The module address can be specified as both: an address _literal_ (does not require the `@` prefix) -or a named address specified in the [Package Manifest](./../concepts/manifest). In the example -below, both are identical because there's a `book = "0x0"` record in the `[addresses]` section of -the `Move.toml`. +The module address can be specified in two ways: as an address _literal_ (which does not require the +`@` prefix) or as a package name declared in the [Package Manifest](./../concepts/manifest). ```move file=packages/samples/sources/move-basics/module.move anchor=address_literal ``` -Addresses section in the Move.toml: +Package section in the Move.toml: ```toml -# Move.toml -[addresses] -book = "0x0" +[package] +name = "book" +edition = "2024" ``` ## Module Members Module members are declared inside the module body. To illustrate this, let's define a simple module -with a struct, a function and a constant: +with an import, a constant, a struct, and a function: ```move file=packages/samples/sources/move-basics/module-members.move anchor=members ``` +Each member starts with its own keyword: `use` brings other modules into scope +([Importing Modules](./importing-modules)), `const` defines a value that never changes +([Constants](./constants)), `struct` declares a custom data type ([Struct](./struct)), and `fun` +declares a function ([Function](./function)). Don't worry about the details yet - each of these has +a dedicated section in this chapter; for now, it is enough to recognize the keywords and know that +all of them live at the module level. + ## Module Block The pre-2024 edition of Move required the body of the module to be a _module block_ - the contents -of the module needed to be surrounded by curly braces `{}`. The main reason to use block syntax and -not _label_ syntax is if you need to define more than one module in a file. However, using module -blocks is not a recommended practice. +of the module surrounded by curly braces `{}`. The block syntax is still supported, and the only +reason to prefer it over the _label_ syntax shown above is declaring more than one module in a +file - which is rarely needed, and not a recommended practice. ```move file=packages/samples/sources/move-basics/module.move anchor=members diff --git a/book/move-basics/option.md b/book/move-basics/option.md index f72c0bb9a..e59327905 100644 --- a/book/move-basics/option.md +++ b/book/move-basics/option.md @@ -1,74 +1,127 @@ --- -description: "The Option type in Move: represent optional values safely, with methods for creating, checking, extracting, and destroying options." +description: + 'The Option type in Move: represent a value that may be absent, create and inspect options, + extract values safely, and process them with option macros.' --- # Option -`Option` is a type that represents an optional value which may or may not exist. The concept of -`Option` in Move is borrowed from Rust, and it is a very useful primitive in Move. `Option` is -defined in the [Standard Library](./standard-library), and is defined as follows: +Some data is optional by nature: a user may or may not have a middle name, a lookup may or may not +find a match. Move has no `null` or `undefined` value - a variable of type `String` always holds a +string - so the absence of a value has to be expressed some other way. -```move -module std::option; - -/// Abstraction of a value that may or may not be present. -public struct Option has copy, drop, store { - vec: vector -} -``` - -_See [full documentation for std::option][option-stdlib] module._ +A first instinct might be to reserve a special value as a marker: an empty string for a missing +middle name, a zero for a missing number. This works - until an empty string becomes valid input, +and every function has to remember which values are "real" and which are placeholders. The standard +library offers a better tool: the `Option` type, a concept Move borrows from Rust. -> The 'std::option' module is implicitly imported in every module, so you don't need to add an -> explicit import. +## The Option Type -The `Option` type is a generic type with an `Element` type parameter. It contains a single field, -`vec`, which is a `vector` of `Element`. The vector can have a length of 0 or 1, representing the -absence or presence of a value, respectively. +`Option` is a wrapper around a value of type `Element`, and it is always in one of two +states, conventionally called `Some` and `None`: -> Note: You might be surprised that `Option` is a `struct` containing a `vector` instead of an -> [enum][enum-reference]. This is for historical reasons: `Option` was added to Move before it had -> support for enums. +- `Some` - the option contains a value; +- `None` - the option is empty. -The `Option` type has two variants: `Some` and `None`. The `Some` variant contains a value, while -the `None` variant represents the absence of a value. The `Option` type is used to represent the -absence of a value in a type-safe way, avoiding the need for empty or `undefined` values. +An option cannot be mistaken for the value it wraps: an `Option` is not a `String`, and the +value has to be checked for and taken out before it can be used. The possibility of absence becomes +part of the type, visible in every signature, instead of a convention every caller must remember. -## In Practice +`Option` is defined in the [Standard Library](./standard-library) and, like `vector`, is +[implicitly imported](./standard-library#implicit-imports) - it can be used in any module without a +`use` statement. The `Element` type parameter makes it [generic](./generics): the same definition +serves `Option`, `Option`, and any other element type. -To showcase why the `Option` type is necessary, let's look at an example. Consider an application -which takes a user input and stores it in a variable. Some fields are required, and some are -optional. For example, a user's middle name is optional. While we could use an empty string to -represent the absence of a middle name, it would require extra checks to differentiate between an -empty string and a missing middle name. Instead, we can use the `Option` type to represent the -middle name. +Here is the user record from the problem above, with the optional field expressed as an +`Option`: ```move file=packages/samples/sources/move-basics/option.move anchor=registry ``` -In the previous example, the `middle_name` field is of type `Option`. This means that the -`middle_name` field can either contain a String value, wrapped in Some, or be explicitly empty, -represented by None. Using the `Option` type makes the optional nature of the field clear, avoiding -ambiguity and the need for extra checks to differentiate between an empty string and a missing -middle name. +The type of the `middle_name` field says exactly what the special-value approach could not: the +value may be absent, and no `String` - empty or otherwise - is reserved as a marker. The two cases +are constructed with `option::some(value)` and `option::none()`: -## Creating and Using Option values +```move file=packages/samples/sources/move-basics/option.move anchor=registry_use -The `Option` type, along with the `std::option` module, is implicitly imported in Move. This means -you can use the `Option` type directly without needing a `use` statement. +``` + +## Creating and Using an Option -To create a value of the `Option` type, you can use the `option::some` or `option::none` methods. -`Option` values also support several operations (borrowing will be discussed in the -[references](references#references-1) chapter): +Once created, an option can be checked for a value, read, and emptied: ```move file=packages/samples/sources/move-basics/option.move anchor=usage ``` +> The `borrow` function yields a _reference_ to the value - a way to read it without taking it out +> of the option. References are covered in the [References](./references#immutable-references) +> section later in this chapter. + +The table below lists the most commonly used functions of the `std::option` module; see the +[module documentation][option-stdlib] for the full list: + +
+ +| Function | Description | Aborts If | +| ---------------------- | ------------------------------------------------------ | ------------------------ | +| `is_some` | Returns `true` if the option holds a value | - | +| `is_none` | Returns `true` if the option is empty | - | +| `contains` | Returns `true` if the option holds the given value | - | +| `borrow` | Returns a reference to the value | The option is empty | +| `borrow_mut` | Returns a mutable reference to the value | The option is empty | +| `fill` | Places a value into an empty option | The option holds a value | +| `extract` | Takes the value out, leaving the option empty | The option is empty | +| `swap` | Replaces the value, returning the old one | The option is empty | +| `destroy_some` | Destroys the option, returning the value | The option is empty | +| `destroy_none` | Destroys an empty option | The option holds a value | +| `destroy_with_default` | Destroys the option, returning the value or a default | - | + +
+ +Like a `vector`, an `Option` inherits its abilities from the element type: an option of a +non-[droppable](./drop-ability) type cannot be ignored, and must be destroyed explicitly with one of +the `destroy_*` functions above. + +## Option Macros + +Like the [vector macros](./vector#vector-macros), option macros replace the common +check-then-extract sequences with a single expression: + +```move file=packages/samples/sources/move-basics/option.move anchor=macros + +``` + +Other commonly used macros include `map!`, `filter!`, `extract_or!`, and `do_ref!` - the full list +is available in the [module documentation][option-stdlib], and macros in general are covered later +in this chapter, in the [Macro Functions](./macros) section. + +## Under the Hood + +`Option` is defined as a struct with a single field: a `vector` of `Element`, which is always +either empty (`None`) or holds exactly one value (`Some`): + +```move +module std::option; + +/// Abstraction of a value that may or may not be present. +public struct Option has copy, drop, store { + vec: vector +} +``` + +> You might be surprised that `Option` is a struct containing a `vector` rather than an +> [enum][enum-reference]. This is for historical reasons: `Option` was added to Move before the +> language had support for enums. In Rust, where the type originates, `Option` _is_ an enum with +> the `Some` and `None` _variants_ - Move keeps the terminology. + +The representation is an implementation detail: the functions and macros above cover regular use, +and the `vec` field is never accessed directly. + ## Further Reading -- [std::option][option-stdlib] in the standard library +- [std::option][option-stdlib] module documentation. [enum-reference]: ./../../reference/enums [option-stdlib]: https://docs.sui.io/references/framework/std/option diff --git a/book/move-basics/ownership-and-scope.md b/book/move-basics/ownership-and-scope.md index 4af9734af..41e8c8aaa 100644 --- a/book/move-basics/ownership-and-scope.md +++ b/book/move-basics/ownership-and-scope.md @@ -1,142 +1,177 @@ --- -description: "Ownership and scope in Move: how variables are owned, moved, and dropped when they go out of scope in the Move language." +description: "Ownership and scope in Move: how values are moved between scopes, why they cannot be copied or lost, and how the compiler enforces it." --- # Ownership and Scope -Every variable in Move has a scope and an owner. The scope is the range of code where the variable -is valid, and the owner is the scope that this variable belongs to. Once the owner scope ends, the -variable is dropped. This is a fundamental concept in Move, and it is important to understand how it -works. +Ownership is the central concept of Move - it is even where the language got its name. Move is +designed for digital assets, and its main promise is that a value cannot be duplicated and cannot be +accidentally lost. The mechanism behind this promise is ownership, and it is enforced by the +compiler: a program that breaks the rules does not compile. - +The rest of this section walks through these rules one by one. If some of them seem strict - that is +the point: the restrictions are what make it safe to treat a value in Move as an asset. -## Ownership +## Variable Scope -A variable defined in a function scope is owned by this scope. The runtime goes through the function -scope and executes every expression and statement. After the function scope ends, the variables -defined in it are dropped or deallocated. +A scope is the range of code in which a value is valid. A variable defined in a function is owned by +that function's scope: it comes into scope at the declaration, and goes out of scope when the +function ends. -```move -module book::ownership; +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=scope + +``` + +Nothing surprising so far - this is how local variables behave in most languages. Ownership becomes +interesting when a value needs to leave its scope. + +## Moving a Value + +To demonstrate the rules, we will use a small module with a `Coin` type and two functions - one that +creates a coin and one that destroys it: + +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=coin + +``` + +The `Coin` struct has no [abilities](./abilities-introduction), so the compiler places the strictest +constraints on its values: they cannot be copied and cannot be discarded. A value like this can only +change hands - which is exactly what we want from an asset. -public fun owner() { - let a = 1; // a is owned by the `owner` function -} // a is dropped here +When a value is passed to a function, it is _moved_ into the function's scope. The function becomes +the new owner, and the caller loses access to the value. This is called _move semantics_. -public fun other() { - let b = 2; // b is owned by the `other` function -} // b is dropped here +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=move_to_function +``` + +Let's see what happens if we break the rule and try to use `coin` after it was moved: + +```move #[test] -fun test_owner() { - owner(); - other(); - // a & b are not valid here +fun test_move_semantics() { + let coin = mint(100); + spend(coin); // ownership of the value moves into `spend` + spend(coin); // ERROR! `coin` was already moved } ``` -In the example above, the variable `a` is owned by the `owner` function, and the variable `b` is -owned by the `other` function. When each of these functions are called, the variables are defined, -and when the function ends, the variables are discarded. +The code above will not compile, and the compiler will point at the exact spot where the value was +moved: + +```text +error[E06002]: use of unassigned variable + ┌─ sources/ownership.move:12:11 + │ +11 │ spend(coin); + │ ---- + │ │ + │ The value of 'coin' was previously moved here. + │ Suggestion: use 'copy coin' to avoid the move. +12 │ spend(coin); + │ ^^^^ Invalid usage of previously moved variable 'coin'. +``` + +The compiler suggests using `copy coin`, but that only works for values that can be copied - and +`Coin` cannot. There is no way to spend the same coin twice, and this guarantee is checked before +the code ever runs. + +Assigning a value to a new variable is also a move. The value itself is not changed or copied - only +its owner is: + +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=move_to_variable + +``` ## Returning a Value -If we changed the `owner` function to return the variable `a`, then the ownership of `a` would be -transferred to the caller of the function. +Moves also work in the opposite direction: a function can return a value, moving it to the caller's +scope. This is how the `mint` function from our example transfers ownership of a newly created coin +to whoever called it. Combined with passing by value, this gives a full picture of a value's +lifetime: `mint` creates the coin and hands it to the test function, which then hands it over to +`spend`, which destroys it. At every point in the program, the coin has exactly one owner. -```move -module book::ownership; +## Every Value Must Be Used -public fun owner(): u8 { - let a = 1; // a defined here - a // scope ends, a is returned -} +What if a value is never passed on? Let's mint a coin and simply let the function end: +```move #[test] -fun test_owner() { - let a = owner(); - // a is valid here -} // a is dropped here +fun test_lose_a_coin() { + let coin = mint(100); +} // ERROR! `coin` still contains a value which cannot be discarded ``` -## Passing by Value +The third rule kicks in: a scope cannot end while it still owns a value that is not discardable. + +```text +error[E06001]: unused value without 'drop' + ┌─ sources/ownership.move:7:35 + │ +4 │ public struct Coin { value: u64 } + │ ---- To satisfy the constraint, the 'drop' ability would need to be added here + · +7 │ let coin = mint(100); + │ ---- ↑ The local variable 'coin' still contains a value. + │ The value does not have the 'drop' ability and must + │ be consumed before the function returns +``` -Additionally, if we passed the variable `a` to another function, the ownership of `a` would be -transferred to this function. When performing this operation, we _move_ the value from one scope to -another. This is also called _move semantics_. +Whether a value can be discarded is controlled by the `drop` ability, which we covered in the +[Ability: Drop](./drop-ability) section. For a type like `Coin`, the absence of `drop` means a coin +cannot be forgotten in a local variable and silently vanish - the code holding it is forced to do +something with it. -```move -module book::ownership; +## Copyable Types -public fun owner(): u8 { - let a = 10; - a -} // a is returned +Some values do not need this level of protection. All primitive types - integers, `bool`, `address` +- have the `copy` ability, and instead of being moved, they are copied when assigned or passed to a +function: -public fun take_ownership(v: u8) { - // v is owned by `take_ownership` -} // v is dropped here +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=copy_types -#[test] -fun test_owner() { - let a = owner(); - // `u8` is copyable, pass `move a` when calling the function to force the transfer of its ownership - take_ownership(move a); - // a is not valid here -} ``` -## Scopes with Blocks +Copying is implicit for primitive types because they are small and cheap to duplicate. Custom types +can also opt into this behavior by adding the `copy` ability, which we cover in the +[Ability: Copy](./copy-ability) section. -Each function has a main scope, and it can also have sub-scopes via the use of blocks. A block is a -sequence of statements and expressions, and it has its own scope. Variables defined in a block are -owned by this block, and when the block ends, the variables are dropped. +If needed, a copyable value can still be moved explicitly with the `move` keyword: + +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=explicit_move -```move -module book::ownership; - -public fun owner() { - let a = 1; // a is owned by the `owner` function's scope - { - let b = 2; // the block that declares b owns it - { - let c = 3; // the block that declares c owns it - }; // c is dropped here - }; // b is dropped here - // a = b; // error: b is not valid here - // a = c; // error: c is not valid here -} // a is dropped here ``` -However, if we return a value from a block, the ownership of the variable is transferred to the -caller of the block. +## Scopes and Blocks + +Besides the function's main scope, every block forms its own scope. Variables declared inside a +block are owned by it and go out of scope when the block ends. Code inside a block can access the +variables of the enclosing scope, but not the other way around: + +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=blocks -```move -module book::ownership; - -public fun owner(): u8 { - let a = 1; // a is owned by the `owner` function's scope - let b = { - let c = 2; // the block that declares c owns it - c // c is returned from the block and transferred to b - }; - a + b // both a and b are valid here -} ``` -## Copyable Types +A block is an expression, and its resulting value is moved out to the enclosing scope - the same +move semantics as returning a value from a function: + +```move file=packages/samples/sources/move-basics/ownership-and-scope.move anchor=block_return + +``` + +## Next Steps -Some types in Move are _copyable_, which means that they can be copied without transferring -ownership. This is useful for types that are small and cheap to copy, such as integers and booleans. -The Move compiler will automatically copy these types when they are passed to or returned from a -function, or when they're _moved_ to another scope and then accessed in their original scope. +So far, the only way to let a function use a value was to give the ownership away. Doing that for +every operation would be impractical - reading a field should not require handing over the whole +value. Move solves this with _references_, which allow a function to borrow a value without taking +ownership. We cover them in the [References](./references) section. ## Further Reading diff --git a/book/move-basics/primitive-types.md b/book/move-basics/primitive-types.md index e57cdb61d..674608058 100644 --- a/book/move-basics/primitive-types.md +++ b/book/move-basics/primitive-types.md @@ -1,48 +1,37 @@ --- -description: "Move primitive types: booleans, unsigned integers (u8 to u256), addresses, and type casting for blockchain smart contracts." +description: + 'Move primitive types: booleans and unsigned integers from u8 to u256 - literals and type + inference, arithmetic and comparison, casting with as, and overflow behavior.' --- # Primitive Types - +Move is a statically typed language: every value has a type, known at compilation time. This +section introduces the simplest of them - the built-in _primitive_ types: booleans and unsigned +integers. Together with [addresses](./address), covered in the next section, they are the material +every other type is built from. -For simple values, Move has a number of built-in primitive types. They're the foundation for all -other types. The primitive types are: - -- [Booleans](#booleans) -- [Unsigned Integers](#integer-types) -- [Addresses](./address) - covered in the next section - -Before we get to the primitive types, let's first take a look at how to declare and assign variables -in Move. +> The code samples in this chapter are excerpts: expressions like the ones below live inside a +> function - usually a [test function](./testing) - in a module, which we omit for brevity. To try +> a sample yourself, place it inside a `#[test]` function of the package created in the +> [Hello World](./../your-first-move/hello-world) chapter and run `sui move test`. ## Variables and Assignment -Variables are declared using the `let` keyword. They are immutable by default, but can be made -mutable by adding the `mut` keyword: - -``` -let [: ] = ; -let mut [: ] = ; -``` - -Where: - -- `` - the name of the variable -- `` - the type of the variable, optional -- `` - the value to be assigned to the variable +Variables are declared with the `let` keyword, and they are _immutable_ by default: once a value is +assigned, it cannot be replaced. A variable that needs to change is declared with `let mut`, and +only then can it be reassigned with the `=` operator: ```move file=packages/samples/sources/move-basics/primitive-types.move anchor=variables_and_assignment ``` -A mutable variable can be reassigned using the `=` operator. - -```move -y = 43; -``` +The type annotation - the `: u8` after the name - is optional wherever the compiler can infer the +type from the value or from later use; writing it out is a matter of clarity, not necessity. -Variables can also be shadowed by re-declaring them. +A variable name can also be reused by declaring it again, which is called _shadowing_. Unlike +reassignment, shadowing creates a new variable, so it works on immutable variables and can change +the type: ```move file=packages/samples/sources/move-basics/primitive-types.move anchor=shadowing @@ -50,37 +39,48 @@ Variables can also be shadowed by re-declaring them. ## Booleans -The `bool` type represents a boolean value - yes or no, true or false. It has two possible values: -`true` and `false`, which are keywords in Move. For booleans, the compiler can always infer the type -from the value, so there is no need to explicitly specify it. +The `bool` type has exactly two values - the keywords `true` and `false` - and the compiler always +infers it, so a `bool` never needs a type annotation. Booleans combine with the logical operators +`&&` (and), `||` (or), and `!` (not), where `&&` and `||` short-circuit: the right-hand side is not +evaluated if the left-hand side already decides the result. ```move file=packages/samples/sources/move-basics/primitive-types.move anchor=boolean ``` -Booleans are often used to store flags and to control the flow of the program. Please refer to the -[Control Flow](./control-flow) section for more information. +Booleans store flags and drive conditions - the `if` and `while` expressions covered in the +[Control Flow](./control-flow) section. ## Integer Types -Move supports unsigned integers of various sizes, from 8-bit to 256-bit. The integer types are: +Move has six integer types, differing only in size - and all of them _unsigned_: there are no +negative integers in Move, and no dedicated signed types. + +
+ +| Type | Size (bits) | Maximum Value | +| ------ | ----------- | ------------------------------ | +| `u8` | 8 | `255` | +| `u16` | 16 | `65_535` | +| `u32` | 32 | `4_294_967_295` | +| `u64` | 64 | `18_446_744_073_709_551_615` | +| `u128` | 128 | 2128 − 1 | +| `u256` | 256 | 2256 − 1 | + +
-- `u8` - 8-bit -- `u16` - 16-bit -- `u32` - 32-bit -- `u64` - 64-bit -- `u128` - 128-bit -- `u256` - 256-bit +The workhorse is `u64` - token amounts, sizes, and indices all use it. Integer literals are written +in decimal (`42`), with optional underscores for readability (`1_000_000`), or in hexadecimal with +the `0x` prefix (`0x2A`): ```move file=packages/samples/sources/move-basics/primitive-types.move anchor=integers ``` -While boolean literals like `true` and `false` are clearly booleans, an integer literal like `42` -could be any of the integer types. In most cases, the compiler will infer the type from the -value, usually defaulting to `u64`. However, sometimes the compiler is unable to infer the type and -will require an explicit type annotation. It can either be provided during assignment or by using a -type suffix. +While `true` and `false` are unambiguously booleans, a literal like `42` could be any of the six +integer types. The compiler infers the type from how the value is used, defaulting to `u64`; when +inference is not enough - or when being explicit reads better - the type can be given as an +annotation or as a literal suffix: ```move file=packages/samples/sources/move-basics/primitive-types.move anchor=integer_explicit_type @@ -89,7 +89,10 @@ type suffix. ### Operations Move supports the standard arithmetic operations for integers: addition, subtraction, -multiplication, division, and modulus (remainder). The syntax for these operations is: +multiplication, division, and modulus (remainder). None of them can produce a value outside the +range of the type - instead of wrapping around, the operation aborts: + +
| Syntax | Operation | Aborts If | | ------ | ------------------- | ---------------------------------------- | @@ -99,31 +102,41 @@ multiplication, division, and modulus (remainder). The syntax for these operatio | % | modulus (remainder) | The divisor is 0 | | / | truncating division | The divisor is 0 | -> For more operations, including bitwise operations, please refer to the -> [Move Reference](./../../reference/primitive-types/integers#bitwise). +
+ +Division is _truncating_: there are no fractional values, and any remainder is discarded, so +`7 / 2` is `3`. Integers can also be compared with `==`, `!=`, `<`, `>`, `<=`, and `>=`, producing +a `bool`: -The types of the operands _must match_, or the compiler will raise an error. The result of the -operation will be of the same type as the operands. To perform operations on different types, the -operands need to be cast to the same type. +```move file=packages/samples/sources/move-basics/primitive-types.move anchor=comparison - - +``` + +In every operation and comparison, the types of the operands _must match_ - there is no implicit +conversion between integer types, and adding a `u8` to a `u64` is a compilation error. To operate +on different types, one of the operands has to be explicitly cast. + +> For more operations, including bitwise operations, refer to the +> [Move Reference](./../../reference/primitive-types/integers#bitwise). ### Casting with `as` -Move supports explicit casting between integer types. The syntax is as follows: +The `as` operator converts an integer from one type to another. Note that an expression with a cast +often needs parentheses around it to prevent ambiguity: + +```move file=packages/samples/sources/move-basics/primitive-types.move anchor=cast_as -```move - as ``` -Note that parentheses around the expression may be required to prevent ambiguity: +Casting _up_ to a larger type always succeeds. Casting _down_ must fit: unlike languages that +silently truncate the value, Move aborts when it is out of range: -```move file=packages/samples/sources/move-basics/primitive-types.move anchor=cast_as +```move file=packages/samples/sources/move-basics/primitive-types.move anchor=downcast ``` -A more complex example, preventing overflow: +A common use of upcasting is making room for an intermediate result that would not fit into the +original type: ```move file=packages/samples/sources/move-basics/primitive-types.move anchor=overflow @@ -131,18 +144,21 @@ A more complex example, preventing overflow: ### Overflow and Underflow -Move does not support overflow / underflow; an operation that results in a value outside the range -of the type will raise a runtime error. This is a safety feature to prevent unexpected behavior. +As the operations table shows, arithmetic in Move never wraps around. An operation whose result +does not fit into the type - too large, or below zero - aborts at runtime: -```move -let x = 255u8; -let y = 1u8; +```move file=packages/samples/sources/move-basics/primitive-types.move anchor=overflow_abort -// This will raise an error -let z = x + y; ``` +This is a deliberate safety feature. Silent overflow is a classic source of smart contract bugs - +a balance that wraps around to zero, or a check that passes because a value quietly became small. +Move turns every such case into a loud failure that reverts the transaction. + ## Further Reading - [Bool](./../../reference/primitive-types/bool) in the Move Reference. - [Integer](./../../reference/primitive-types/integers) in the Move Reference. +- [std::u64](https://docs.sui.io/references/framework/std/u64) module documentation - every integer + type has a helper module (`std::u8` through `std::u256`) with functions like `min`, `max`, `sqrt`, + and more. diff --git a/book/move-basics/references.md b/book/move-basics/references.md index 0d4e6605d..4bf4dd10c 100644 --- a/book/move-basics/references.md +++ b/book/move-basics/references.md @@ -4,22 +4,6 @@ description: "References in Move: immutable and mutable borrows, the borrow chec # References - - In the [Ownership and Scope](./ownership-and-scope) section, we explained that when a value is passed to a function, it is _moved_ to the function's scope. This means that the function becomes the owner of the value, and the original scope (owner) can no longer use it. This is an important @@ -35,11 +19,12 @@ will look at 4 different scenarios where a card can be: 3. Used at the turnstile to enter the metro, and purchase a ride 4. Recycled after it's empty -## Layout +## The Metro Pass Application The initial layout of the metro pass application is simple. We define the `Card` type and the `USES` [constant](./constants) that represents the number of rides on a single card. We also add -[error constants](./assert-and-abort#error-constants) for the case when the card is empty and when the card is not empty. +[error constants](./assert-and-abort#error-constants) for the case when the card is empty and when +the card is not empty. ```move file=packages/samples/sources/move-basics/references.move anchor=header_new module book::metro_pass; @@ -47,9 +32,7 @@ module book::metro_pass; ``` - - -## References +## Immutable References References are a way to _show_ a value to a function without giving up ownership. In our case, when we show the Card to the inspector, we don't want to give up ownership of it, and we don't allow the @@ -68,6 +51,17 @@ to it, meaning it cannot modify the number of rides. Additionally, the function that it cannot be called without a Card instance. This is an important property that allows the [Capability Pattern](./../programmability/capability), which we will cover in the next chapters. +The `&` operator is not limited to function signatures: it is an expression that can be applied to +any value or to a single field of a struct. The resulting reference can be stored in a local +variable and passed on: + +```move +let card = purchase(); + +let card_ref = &card; // reference to the whole value +let uses_ref = &card.uses; // reference to a single field +``` + Creating a reference to a value is often referred to as "borrowing" the value. For example, the method to get a reference to the value wrapped by an `Option` is called `borrow`. @@ -84,6 +78,40 @@ signature. As you can see in the function body, the `&mut` reference allows mutating the value, and the function can spend rides. +A mutable reference can be used anywhere an immutable one is expected: passing `&mut card` to the +`is_valid` function is perfectly fine, the function will simply not be able to modify the value. +The reverse is not true - an immutable reference can never be turned into a mutable one. + +## The Borrow Checker + +References are compiled with the help of the _borrow checker_ - the part of the compiler that +tracks every borrow and rejects programs which could use references unsafely. The rules it +enforces are: + +- While a value is borrowed, it cannot be moved, passed by value, or destroyed; +- There can be either a single mutable reference to a value, or any number of immutable + references - never both at the same time; +- A reference cannot outlive the value it points to. + +To see the borrow checker in action, let's try to break the first rule and recycle the card while +the inspector is still looking at it: + +```move +let card = purchase(); +let card_ref = &card; + +recycle(card); // ERROR! Invalid move of the local `card`: + // the value is still being borrowed by `card_ref`. + +is_valid(card_ref); +``` + +The compiler rejects this program: as long as `card_ref` is alive and used, the value it points to +must stay in place. The same mechanism prevents two mutable references from existing at once, or a +value from being modified while it is immutably borrowed. Thanks to these rules, a reference in +Move can never point at destroyed or moved-away data, and functions can trust their arguments +without any runtime checks. + ## Passing by Value Lastly, let's illustrate what happens when we pass the value itself to the function. In this case, @@ -95,42 +123,93 @@ owner of the Card can recycle it and thereby relinquish ownership to the functio ``` In the `recycle` function, the Card is passed by value, transferring ownership to the function. This -allows it to be unpacked and destroyed. +allows it to be [unpacked](./struct#unpacking-a-struct) and destroyed. -> Note: In Move, `_` is a wildcard pattern used in destructuring to ignore a field while still -> consuming the value. Destructuring must match all fields in a struct type. If a struct has fields, -> you must list all of them explicitly or use `_` to ignore unwanted fields. +## Returning References -## Full Example +A function can not only take references - it can also return them. This is exactly how _getters_, +which we mentioned in the [struct section](./struct#getters-and-setters), provide access to the +fields of a struct from other modules. Let's add one to the metro pass application: -To illustrate the full flow of the application, let's put all the pieces together in a test. +```move file=packages/samples/sources/move-basics/references.move anchor=getter -```move file=packages/samples/sources/move-basics/references.move anchor=move_2024 +``` + +A returned reference must point into a value that the caller owns - in other words, it must be +_derived_ from one of the reference parameters of the function. Returning a reference to a local +value is impossible, since the local is destroyed when the function returns: +```move +// Won't compile! +public fun dangling(): &u8 { + let x = 10; + &x // ERROR! The local `x` is destroyed at the end of the function. +} ``` -## Further Reading +Returning a mutable reference to a field is also possible - and it is a decision to make carefully, +as it allows any caller to modify the field directly. The borrow checker rules apply to returned +references just as they do to local borrows: while the returned reference is alive, the value it +was derived from stays borrowed. + +## References Cannot Be Stored -- [References](/reference/primitive-types/references) in the Move - Reference. +References in Move are _ephemeral_: they exist as function arguments, local variables, and return +values, but they can never be put into a struct. A field of a reference type is a compilation +error, so no value can carry a reference beyond the end of a function call. If a struct needs to +refer to another value long-term, it stores a copy of the data or an identifier of it, never a +reference. - +This restriction has consequences you will meet throughout the book. It is why references have only +the `copy` and `drop` [abilities](./abilities-introduction) and can never be stored; and why +collection types hand out a fresh reference on every `borrow` call instead of keeping one. It is +also the reason Move needs no _lifetime_ annotations for references - a reference can never escape +the call in which it was created. - +## Dereferencing - +A reference gives access to a value, but sometimes the code holding a reference needs a copy of the +value itself. The _dereference operator_ `*` reads the value behind a reference and produces a copy +of it - the original value stays where it was, untouched: - +You may also encounter the `*&` combination - borrow and immediately dereference - which is the +idiomatic way to write an explicit copy of a field or variable. - + +Notice that not a single `&` appears in the test, yet references are doing all the work. When a +function is called with the method syntax, the compiler borrows the receiver _automatically_, +based on the signature of the function: `card.is_valid()` borrows `card` immutably as `&Card`, +`card.enter_metro()` borrows it mutably as `&mut Card`, and `card.recycle()` passes the value as +is, by value. This is why everyday Move code rarely spells out the borrow operator - most borrows +happen implicitly at method call sites, following the same borrow checker rules described above. + +## Summary + +- References allow showing a value to a function without giving up ownership: `&` for read-only + access, `&mut` for read-write access. +- The borrow checker enforces the safety rules: no moving of borrowed values, a single `&mut` _or_ + any number of `&`, and no reference may outlive its value. +- Functions can return references derived from their reference parameters - the basis of getters. +- References cannot be stored in structs - they never outlive the function call. +- Method calls borrow the receiver automatically, based on the function signature. + +## Further Reading + +- [References](./../../reference/primitive-types/references) in the Move Reference. diff --git a/book/move-basics/standard-library.md b/book/move-basics/standard-library.md index 146ab22f3..343fafbd0 100644 --- a/book/move-basics/standard-library.md +++ b/book/move-basics/standard-library.md @@ -4,8 +4,6 @@ description: "Overview of the Move Standard Library: common modules for strings, # Standard Library - - The Move Standard Library provides functionality for native types and operations. It is a standard collection of modules that do not interact with storage, but provide basic tools for working with and manipulating data. It is the only dependency of the @@ -26,13 +24,17 @@ and which module implements it. | [std::ascii](https://docs.sui.io/references/framework/std/ascii) | Provides basic ASCII operations | - | | [std::option](https://docs.sui.io/references/framework/std/option) | Implements `Option` | [Option](./option) | | [std::vector](https://docs.sui.io/references/framework/std/vector) | Native operations on the vector type | [Vector](./vector) | +| [std::internal](https://docs.sui.io/references/framework/std/internal) | Provides the `Permit` type for module-authorized calls | [Internal Permit](./internal-permit) | | [std::bcs](https://docs.sui.io/references/framework/std/bcs) | Contains the `bcs::to_bytes()` function | [BCS](./../programmability/bcs) | | [std::address](https://docs.sui.io/references/framework/std/address) | Contains a single `address::length` function | [Address](./address) | | [std::type_name](https://docs.sui.io/references/framework/std/type_name) | Allows runtime _type reflection_ | [Type Reflection](./type-reflection) | | [std::hash](https://docs.sui.io/references/framework/std/hash) | Hashing functions: `sha2_256` and `sha3_256` | - | | [std::debug](https://docs.sui.io/references/framework/std/debug) | Contains debugging functions, which are available in only in **test** mode | - | +| [std::unit_test](https://docs.sui.io/references/framework/std/unit_test) | The `assert_eq!` and `assert_ref_eq!` macros for **test** code | [Testing](./testing) | | [std::bit_vector](https://docs.sui.io/references/framework/std/bit_vector) | Provides operations on bit vectors | - | -| [std::fixed_point32](https://docs.sui.io/references/framework/std/fixed_point32) | Provides the `FixedPoint32` type | - | +| [std::uq32_32](https://docs.sui.io/references/framework/std/uq32_32) | Fixed-point arithmetic: the `UQ32_32` type | - | +| [std::uq64_64](https://docs.sui.io/references/framework/std/uq64_64) | Fixed-point arithmetic: the `UQ64_64` type | - | +| [std::fixed_point32](https://docs.sui.io/references/framework/std/fixed_point32) | The `FixedPoint32` type; deprecated in favor of `std::uq32_32` | - | @@ -42,8 +44,10 @@ The Move Standard Library provides a set of functions associated with integer ty are split into multiple modules, each associated with a specific integer type. The modules should not be imported directly, as their functions are available on every integer value. -> All of the modules provide the same set of functions. Namely, `max`, `diff`, -> `divide_and_round_up`, `sqrt` and `pow`. +> All of the modules provide the same set of functions: `min`, `max`, `diff`, +> `divide_and_round_up`, `sqrt`, `pow`, and `to_string`; checked conversions to smaller types - +> `try_as_u8`, `try_as_u16`, and so on; and macros, such as `max_value!` and the iteration +> helpers `do!` and `range_do!`.
@@ -61,13 +65,8 @@ not be imported directly, as their functions are available on every integer valu ## Exported Addresses -The Standard Library exports a single named address - `std = 0x1`. Note the alias `std` is defined -here. - -```toml -[addresses] -std = "0x1" -``` +The Standard Library exports a single named address - `std = 0x1`. This is where the `std` alias +used throughout the book is defined. ## Implicit Imports @@ -77,12 +76,17 @@ import. For the Standard Library, these modules and types include: - std::vector - std::option - std::option::Option +- std::internal + +Note that `std::internal` is imported as a module, not a member: its members keep the module +prefix, as in `internal::Permit` and `internal::permit()` - no `use` statement required. See +the [Internal Permit](./internal-permit) section for how it is used. ## Importing std without Sui Framework The Move Standard Library can be imported to the package directly. However, `std` alone is not enough to build a meaningful application, as it does not provide any storage capabilities and can't -interact with the on-chain state. +interact with the onchain state. ```toml MoveStdlib = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "framework/mainnet" } diff --git a/book/move-basics/string.md b/book/move-basics/string.md index 5ad3b8ae8..dc9cf334d 100644 --- a/book/move-basics/string.md +++ b/book/move-basics/string.md @@ -1,3 +1,7 @@ +--- +description: "Strings in Move: string literals, the UTF-8 and ASCII String types, common operations, and conversions between them in Sui smart contracts." +--- + # String While Move does not have a built-in type to represent strings, it does have two standard @@ -5,22 +9,11 @@ implementations for strings in the [Standard Library](./standard-library). The ` defines a `String` type and methods for UTF-8 encoded strings, and the second module, `std::ascii`, provides an ASCII `String` type and its methods. -> The Sui execution environment automatically converts bytevector into `String` in transaction -> inputs. As a result, in many cases, constructing a String directly within the -> [Transaction Block](./../concepts/what-is-a-transaction) is unnecessary. - - +> Both types are named `String`, which may be confusing at first. When the distinction matters, we +> refer to them by their module: `string::String` and `ascii::String`. In most application code, the +> UTF-8 `string::String` is the type to use. -## Strings are bytes +## Strings Are Bytes No matter which type of string you use, it is important to know that strings are just bytes. The wrappers provided by the `string` and `ascii` modules are just that: wrappers. They do provide @@ -31,13 +24,64 @@ bytes. ``` +Both standard string types follow this exact pattern - a struct holding a `vector`. What makes +them different from a plain byte vector, and from each other, is the _guarantee_ they carry about +the contents: + +- `ascii::String` guarantees that every byte is a valid ASCII character. ASCII is the oldest and + simplest character encoding: it defines 128 characters - Latin letters, digits, and punctuation - + and each character takes exactly one byte. +- `string::String` guarantees that the bytes are valid UTF-8. UTF-8 is the modern standard encoding: + it can represent any Unicode character - alphabets, hieroglyphs, emoji - using one to four bytes + per character. + +UTF-8 is backward compatible with ASCII: every ASCII string is also a valid UTF-8 string, but not +the other way around. + +## String Literals + +A [literal](./expression#literals) is a value written directly in the source code. Move offers two +syntaxes for writing strings: the string literal `"..."` and the byte string literal `b"..."`. The +byte string always yields a `vector`, while the type of a string literal is _inferred_ from the +context - it becomes whichever of the three byte-carrying types (`vector`, `string::String`, or +`ascii::String`) the compiler expects in that spot: + +```move file=packages/samples/sources/move-basics/string.move anchor=literals + +``` + +The compiler also checks the contents of the literal against the expected type at compile time. A +string literal used as an `ascii::String` must contain only ASCII characters, and the following code +will not compile: + +```move +let s: std::ascii::String = "héllo"; +// ^ error! 'é' is not a valid ASCII character +``` + +If the compiler cannot tell the type from the context, the literal defaults to `vector`, and a +warning is emitted. This is also why a method cannot be called directly on a bare literal - +`"Hello".to_string()` does not compile, because the compiler cannot infer the type of the literal +before resolving the method. A `vector` is not yet a string - both string modules provide +functions to convert bytes into strings at runtime, which we show below. + +### Escape Sequences + +Some characters cannot be typed into a literal directly: a newline, a tab, or the `"` character +itself, which would end the literal. Like most languages, Move uses the backslash `\` to _escape_ +special characters. Arbitrary bytes can also be written as `\x` followed by two hex digits. + +```move file=packages/samples/sources/move-basics/string.move anchor=escapes + +``` + ## Working with UTF-8 Strings -While there are two types of strings (`string` and `ascii`) in the standard library, the `string` -module should be considered the default. It has native implementations of many common operations, -leveraging low-level, optimized runtime code for superior performance. In contrast, the `ascii` -module is fully implemented in Move, relying on higher-level abstractions and making it less -suitable for performance-critical tasks. +While there are two types of strings in the standard library, the `string` module should be +considered the default. It has native implementations of many common operations, leveraging +low-level, optimized runtime code for superior performance. In contrast, the `ascii` module is fully +implemented in Move, relying on higher-level abstractions and making it less suitable for +performance-critical tasks. ### Definition @@ -56,41 +100,45 @@ _See [full documentation for std::string][string-stdlib] module._ ### Creating a String -To create a new UTF-8 `String` instance, you can use the `string::utf8` method. The -[Standard Library](./standard-library) provides an alias `.to_string()` on the `vector` for -convenience. +A string literal, as shown above, is the most common way to create a `String`. Alternatively, an +existing `vector` can be turned into a `String` at runtime with the `string::utf8` function, or +its convenient alias `.to_string()` on the `vector` type. Both abort if the bytes are not valid +UTF-8. ```move file=packages/samples/sources/move-basics/string.move anchor=utf8 ``` +> The Sui execution environment automatically converts byte vectors into `String` in transaction +> inputs. As a result, in many cases, constructing a `String` directly within the +> [transaction](./../concepts/what-is-a-transaction) is unnecessary. + ### Common Operations -UTF8 String provides a number of methods to work with strings. The most common operations on strings -are: concatenation, slicing, and getting the length. Additionally, for custom string operations, the -`bytes()` method can be used to get the underlying byte vector. +The UTF-8 `String` provides a number of methods to work with strings. The most common operations on strings +are: concatenation, slicing, searching, and getting the length. Additionally, for custom string +operations, the `as_bytes()` method can be used to get the underlying byte vector. -```move -let mut str = b"Hello,".to_string(); -let another = b" World!".to_string(); +```move file=packages/samples/sources/move-basics/string.move anchor=common_ops -// append(String) adds the content to the end of the string -str.append(another); +``` -// `sub_string(start, end)` copies a slice of the string -str.sub_string(0, 5); // "Hello" +Note the behavior of `index_of` when there is no occurrence: instead of aborting or returning an +`Option`, it returns the length of the string - an index just past the last byte. Also note what is +_not_ on the list: Move has no string interpolation or formatting, and no way to split a string by a +separator. Strings in a smart contract are typically stored and displayed, not parsed. -// `length()` returns the number of bytes in the string -str.length(); // 12 (bytes) +> Older code may use the `sub_string` and `bytes` functions - they are deprecated aliases of +> `substring` and `as_bytes`. -// methods can also be chained! Get a length of a substring -str.sub_string(0, 5).length(); // 5 (bytes) +### Converting Numbers to Strings -// whether the string is empty -str.is_empty(); // false +A common practical task is building a string out of numbers - for a name, a label, or an error +message. Every unsigned integer type has a `to_string` method that converts the number into its +decimal representation. + +```move file=packages/samples/sources/move-basics/string.move anchor=number_to_string -// get the underlying byte vector for custom operations -let bytes: &vector = str.bytes(); ``` ### Safe UTF-8 Operations @@ -115,18 +163,54 @@ because UTF-8 is a variable-length encoding, and the length of a character can b 4 bytes. Similarly, the `length()` method returns the number of bytes in the string, not the number of characters. -However, methods like `sub_string` and `insert` validate character boundaries and abort if the -specified index falls within the middle of a character. +```move file=packages/samples/sources/move-basics/string.move anchor=limitations + +``` + +Byte positions matter for methods that take indices, such as `substring` and `insert`. These methods +validate character boundaries and abort if the specified index falls within the middle of a +character: + +```move file=packages/samples/sources/move-basics/string.move anchor=substring_abort + +``` + +> One more consequence of "strings are bytes": two strings that look identical on screen may have +> different byte representations. For example, "é" can be encoded as a single character or as "e" +> followed by a combining accent mark - they render the same, but compare as different, because `==` +> compares bytes, not what the reader sees. ## ASCII Strings -This section is coming soon! +The `ascii::String` type is a good fit for values that are known to be plain Latin letters, digits, +and punctuation: tickers, symbols, identifiers, or URLs. For example, the +[Sui Framework](./../programmability/sui-framework) uses `ascii::String` for the `symbol` field of +the `CoinMetadata` type. + +What the ASCII encoding lacks in expressiveness, it makes up for in simplicity: every character is +exactly one byte. This lifts the limitations of UTF-8 strings - `ascii::String` allows operating on +individual characters (represented by the `ascii::Char` type), and offers methods that would be +ambiguous for UTF-8, such as changing the case of a string. + +An ASCII string is created the same way as a UTF-8 one: with a string literal, or by converting a +`vector` at runtime - this time with the `ascii::string` function or the `.to_ascii_string()` +alias on `vector`. There is a `try_string` counterpart as well, following the same `try_*` +convention described above. + +The two string types can be converted into one another. Since every ASCII string is also valid +UTF-8, `to_string()` on an `ascii::String` always succeeds; the reverse conversion - `to_ascii()` - +aborts if the string contains non-ASCII characters. + +```move file=packages/samples/sources/move-basics/string.move anchor=ascii + +``` + +_See [full documentation for std::ascii][ascii-stdlib] module._ ## Further Reading - [std::string][string-stdlib] module documentation. - [std::ascii][ascii-stdlib] module documentation. -[enum-reference]: /reference/enums [string-stdlib]: https://docs.sui.io/references/framework/std/string [ascii-stdlib]: https://docs.sui.io/references/framework/std/ascii diff --git a/book/move-basics/struct-methods.md b/book/move-basics/struct-methods.md index cea67aa7d..52785fb29 100644 --- a/book/move-basics/struct-methods.md +++ b/book/move-basics/struct-methods.md @@ -4,21 +4,20 @@ description: "Struct methods in Move: use receiver syntax to call functions on s # Struct Methods -Move Compiler supports _receiver syntax_ `e.f()`, which allows defining methods which can be called -on instances of a struct. The term "receiver" specifically refers to the instance that receives the -method call. This is like the method syntax in other programming languages. It is a convenient way -to define functions that operate on the fields of a struct, providing direct access to the struct's -fields and creating cleaner, more intuitive code than passing the struct as a parameter. +Throughout the previous sections we have called functions on values with the dot operator: +`v.length()`, `opt.is_some()`, `artist.name()`. This is the _receiver syntax_ - "receiver" refers +to the instance that receives the method call - and this section explains how it works and how to +control it. Methods make code that operates on a struct read naturally: the value comes first, the +operation follows, and there is no need to import or spell out the function's module. -## Method syntax +## Method Syntax -If the first argument of a function is a struct internal to the module that defines the function, -then the function can be called using the `.` operator. However, if the type of the first argument -is defined in another module, then method won't be associated with the struct by default. In this -case, the `.` operator syntax is not available, and the function must be called using standard -function call syntax. - -When a module is imported, its methods are automatically associated with the struct. +The core rule: a function is callable with the `.` operator when its first argument is a struct +defined in the _same module_ as the function. Such methods are automatically available everywhere +the struct is used - this is exactly why `vector` and `Option` values could be called with the dot +syntax as soon as we had them. If the type of the first argument is defined in another module, the +function is not associated with the struct by default and must be called with the standard function +call syntax - unless an _alias_ is declared, as shown below. ```move file=packages/samples/sources/move-basics/struct-methods.move anchor=hero @@ -61,16 +60,19 @@ structs. > `public use fun hero_health as Hero.health`, which provides controlled access to the private > field. - +The alias only exists within the module that declares it - which is exactly why it cannot be +`public`: the module does not own the `String` type, so it cannot extend its interface for everyone +else. ## Further Reading diff --git a/book/move-basics/struct.md b/book/move-basics/struct.md index ecbda4e1e..7c35dc136 100644 --- a/book/move-basics/struct.md +++ b/book/move-basics/struct.md @@ -1,19 +1,23 @@ --- -description: "Define custom types with struct in Move: create, pack, unpack, and access fields for building data models in Sui smart contracts." +description: "Define custom types with struct in Move: pack, unpack, access fields, and control field visibility with getters and setters in Sui smart contracts." --- # Custom Types with Struct -Move's type system shines when it comes to defining custom types. User defined types can be custom -tailored to the specific needs of the application, not only on the data level, but also in its -behavior. In this section we introduce the struct definition and how to use it. +A _struct_ is a user-defined type that groups related values into a single unit, giving a name to +both the group and each value inside it. If you are familiar with object-oriented languages, a +struct is similar to an object's data attributes. Instead of passing around a loose title, artist, +and release year, an application can define a `Record` type and handle all three as one value. -## Struct +Custom types are the backbone of a Move program: they describe the application's data, and - as +later sections will show - the module that defines a type controls everything that can be done with +its values. In this section we introduce the struct definition and how to use it. -To define a custom type, you can use the `struct` keyword followed by the name of the type. After -the name, you can define the fields of the struct. Each field is defined with the -`field_name: field_type` syntax. Field definitions must be separated by commas. The fields can be of -any type, including other structs. +## Defining a Struct + +To define a custom type, use the `public struct` keywords followed by the name of the type, and a +block of fields. Each field is defined with the `field_name: field_type` syntax, and field +definitions must be separated by commas. The fields can be of any type, including other structs. > Move does not support recursive structs, meaning a struct cannot contain itself as a field. @@ -21,69 +25,195 @@ any type, including other structs. ``` -In the example above, we define a `Record` struct with five fields. The `title` field is of type -`String`, the `artist` field is of type `Artist`, the `year` field is of type `u16`, the `is_debut` -field is of type `bool`, and the `edition` field is of type `Option`. The `edition` field is of -type `Option` to represent that the edition is optional. +In the example above, we define an `Artist` struct with a single field, and a `Record` struct with +five fields. The `title` field is of type [`String`](./string), the `artist` field uses the custom +`Artist` type we just defined, the `year` field is of type `u16`, the `is_debut` field is of type +`bool`, and the `edition` field is of type [`Option`](./option) to represent that the edition +is optional. + +The `String` type is not built into the language - it is defined in the +[Standard Library](./standard-library) and brought into scope with the `use` statement at the top of +the example; imports are covered in the [Importing Modules](./importing-modules) section. The angle +brackets in `Option` denote a _type parameter_: `Option` is an `Option` that holds a +`u16`. Type parameters are covered in the [Generics](./generics) section. -Structs are private by default, meaning they cannot be imported and used outside of the module they -are defined in. Their fields are also private and can't be accessed from outside the module. See -[visibility](./visibility) for more information on different visibility modifiers. +> A struct definition can also declare _abilities_ - properties that relax the default restrictions +> on values of the type. They are listed with the `has` keyword, either before the fields - +> `public struct Foo has copy, drop { ... }` - or after them, terminated with a semicolon - +> `public struct Foo { ... } has copy, drop;`. Abilities are introduced in the +> [Abilities Introduction](./abilities-introduction) section. -> Fields of a struct are private and can only be accessed by the module defining the struct. Reading -> and writing the fields of a struct in other modules is only possible if the module defining the -> struct provides public functions to access the fields. +## Creating an Instance -## Create and use an instance +We described the _definition_ of a struct. Now let's see how to create an instance of one. Creating +an instance of a struct is called _packing_, and it is done with the +`StructName { field1: value1, field2: value2, ... }` syntax. The fields can be set in any order, but +all of them must be set - a struct cannot be partially initialized. -We described the _definition_ of a struct. Now let's see how to initialize a struct and use it. A -struct can be initialized using the `struct_name { field1: value1, field2: value2, ... }` syntax. -The fields can be initialized in any order, and all of the required fields must be set. +> The examples on this page live inside a [test function](./testing) in the same module that defines +> the structs - as we are about to see, structs can only be created and taken apart within their +> module. The `assert_eq!` used throughout is a _macro_ - hence the `!` in the name - that compares +> two values and fails if they differ; it is covered in the [Testing](./testing) section. ```move file=packages/samples/sources/move-basics/struct.move anchor=pack ``` -In the example above, we create an instance of the `Artist` struct and set the `name` field to a -string "The Beatles". +In the example above, we create an instance of the `Artist` struct and set the `name` field to the +string "The Beatles". The value `"The Beatles"` is a _string literal_: the compiler sees that the +`name` field expects a `String` and infers the type of the literal automatically. Strings are +covered in more detail in the [String](./string) section. + +Move also offers a shorthand: if a local variable has the same name as the field, the field name can +be given just once. This is called _field name punning_. + +```move file=packages/samples/sources/move-basics/struct.move anchor=pack_shorthand + +``` -To access the fields of a struct, you can use the `.` operator followed by the field name. +## Accessing Fields + +To access the fields of a struct, use the `.` (dot) operator followed by the field name. Fields can +be read, and, if the variable is declared as `mut`, assigned a new value. ```move file=packages/samples/sources/move-basics/struct.move anchor=access ``` -Only the module defining the struct can access its fields (both mutably and immutably). So the above -code should be in the same module as the `Artist` struct. +Accessing fields this way works only in the module that defines the struct. To understand why, +let's take a closer look at struct visibility. + +## Field Visibility + +As you may have noticed, every struct is declared with the `public` modifier - it is required, and +declaring a struct without it is a compilation error. The `public` modifier makes the struct _type_ +visible to other modules: it can be [imported](./importing-modules), used in type definitions, and +in function signatures. + +However, the _contents_ of a struct always stay internal to the module that defines it. Unlike some +languages, Move has no per-field visibility modifiers - there is no way to mark a field public. +Outside of the defining module it is impossible to: + +- read or write the fields of a struct; +- create ("pack") an instance of a struct; +- destroy ("unpack") an instance of a struct. + +This is a feature, not a limitation. It means the module has full control over how its types are +created, used, and destroyed, and no external code can violate the rules the module sets. In the +[Object Model](./../object/) chapter, we show how this property is used to model assets and enforce +guarantees on them. + +> Note that just because a struct field is not accessible from other modules does not mean its value +> is confidential - it is always possible to read the contents of an onchain object from outside of +> Move. You should never store unencrypted secrets inside of objects. + +## Getters and Setters + +Because fields are only accessible inside the defining module, the module needs to expose public +functions if other modules should read or update them. A function that returns the value of a field +is conventionally called a _getter_, and a function that updates a field is called a _setter_. + +A getter typically takes a [reference](./references) to the struct and returns the field value: + +```move file=packages/samples/sources/move-basics/struct.move anchor=getter + +``` + +A setter takes a mutable reference to the struct and the new value: + +```move file=packages/samples/sources/move-basics/struct.move anchor=setter + +``` - -## Unpacking a struct +Because these functions are `public`, any module that imports `Artist` can call them. Note the +parentheses: `artist.name()` is a function call and works anywhere the function is visible, while +the field access `artist.name` would not compile outside the defining module. + +> The `public fun` syntax defines a public function; functions are covered in detail in the +> [Functions](./function) section. The `&` and `&mut` in the signatures are references - they allow +> a function to read or modify a value without taking ownership of it. We cover them in the +> [References](./references) section, and the dot-call syntax in the +> [Struct Methods](./struct-methods) section. + +While getters are very common, setters are defined less often, and usually with extra checks. The +choice of which functions to expose is what defines the interface of the type - the module decides +what external code can and cannot do with its structs. -Structs are non-discardable by default, meaning that the initialized struct value must be used, -either by storing it or unpacking it. Unpacking a struct means deconstructing it into its fields. -This is done using the `let` keyword followed by the struct name and the field names. +## Unpacking a Struct + +Structs are non-discardable by default: a struct value cannot simply be left behind at the end of a +function - code that does so will not compile. Every created value must be used: either stored (for +example, placed inside another struct, or kept in onchain storage, as shown in the +[Using Objects](./../storage/) chapter) or _unpacked_. Unpacking a struct means deconstructing it +into its fields, and it is the mirror image of packing: the `let` keyword, followed by the struct +name and the field names to bind. ```move file=packages/samples/sources/move-basics/struct.move anchor=unpack ``` In the example above we unpack the `Artist` struct and create a new variable `name` with the value -of the `name` field. Because the variable is not used, the compiler will raise a warning. To -suppress the warning, you can use the underscore `_` to indicate that the variable is intentionally -unused. +of the `name` field. The struct value no longer exists after this line - it has been broken up into +its parts. + +If a field is not needed, it can be ignored by binding it to the underscore `_`. However, since the +struct itself cannot be discarded, all of its fields must still be listed in the pattern: ```move file=packages/samples/sources/move-basics/struct.move anchor=unpack_ignore ``` +For structs with many fields, listing every ignored field gets verbose. The `..` pattern - the +_rest_ pattern - matches all of the remaining fields at once: + +```move file=packages/samples/sources/move-basics/struct.move anchor=unpack_rest + +``` + +In the example above, we pack a full `Record` - the `option::none()` call creates an empty `Option` +value, see the [Option](./option) section - and then unpack it, keeping the `title` and `artist` +fields and ignoring the rest with `..`. + +Note that ignoring a field - whether with `_` or `..` - discards its value, which is only allowed +for values that can be discarded. Simple values like `String`, `u16`, and `bool` can be discarded +freely, but `Artist` cannot - which is why the example unpacks the `artist` binding as well instead +of ignoring it. Which values can be discarded and which cannot is determined by _abilities_, +explained in the next sections - [Abilities Introduction](./abilities-introduction) and +[Ability: Drop](./drop-ability). + +## Positional Structs + +So far, every struct on this page had named fields. Move also supports _positional_ structs, whose +fields have no names and are identified by their position instead. A positional struct is defined +with parentheses instead of curly braces, and the definition has no body - it ends right after the +field list: + +```move file=packages/samples/sources/move-basics/struct.move anchor=positional_def + +``` + +Abilities can be placed before or after the fields here as well; in the post-fix form they follow +the parentheses: `public struct Duration(u64, u64) has copy, drop;`. + +Positional structs are packed and unpacked with parentheses as well, and their fields are accessed +with the `.` operator followed by the field index, starting at zero: + +```move file=packages/samples/sources/move-basics/struct.move anchor=positional_use + +``` + +Positional structs are a good fit when field names would not add anything to what the type name +already says - typically in small wrapper types with one or two fields. All of the rules described +on this page still apply to them: fields are only accessible within the defining module, and a +value must be used - stored or unpacked. For structs with more fields, named fields are usually the +better choice. + ## Further Reading - [Structs](./../../reference/structs) in the Move Reference. diff --git a/book/move-basics/testing.md b/book/move-basics/testing.md index fc8222425..80a82806b 100644 --- a/book/move-basics/testing.md +++ b/book/move-basics/testing.md @@ -28,13 +28,35 @@ fun test_fail() { } ``` +A test passes if it runs to completion and fails if it aborts - which is exactly what the +`assert_eq!` macro does when its two values differ. For arbitrary conditions there is the more +general [`assert!`](./assert-and-abort) macro; both are the workhorses of Move tests. The second +test above inverts the outcome: `#[expected_failure(abort_code = 0)]` makes the test pass only if +it aborts with the given code, which is the way to test error conditions. + +## Test-Only Code + +The `#[test_only]` attribute marks a module member - or an entire module - as compiled only for +tests. Test helpers, mock constructors, and imports like the `std::unit_test` import above are +marked this way: the published bytecode stays free of testing machinery, while tests get access to +everything they need, including things the public API deliberately does not expose. + ## Explore More This page only scratches the surface. The dedicated [Testing](./../testing/index.md) chapter walks through test scenarios, coverage reports, gas profiling, working with system objects, and best practices for writing tests you can actually trust in production. +## What's Next + +This page concludes the Move Basics chapter. You can now define modules and custom types, control +whether values can be copied or discarded, pass them around by reference or by value, write logic +with pattern matching, abstract it with generics and macros - and test all of it. What we have set +aside so far is what makes Move on Sui special: the storage model. The +[Object Model](./../object/) chapter picks up exactly there - it introduces _objects_, the Move +structs that become onchain assets, and the chapters after it show how to store, own, and +transfer them. + ## Further Reading -- [Unit Testing](/reference/unit-testing) in the Move Reference. -- [Testing](./../testing/index.md) chapter in the Move Book. +- [Unit Testing](./../../reference/unit-testing) in the Move Reference. diff --git a/book/move-basics/type-reflection.md b/book/move-basics/type-reflection.md index 4a6499ef3..2fff41f87 100644 --- a/book/move-basics/type-reflection.md +++ b/book/move-basics/type-reflection.md @@ -10,8 +10,9 @@ a value at runtime. This is handy when you need to store type information in a h collection, or when you want to check if a type comes from a particular package. Type reflection is implemented in the [Standard Library](./standard-library) module -[`std::type_name`][type-name-stdlib]. It provides a set of functions, main of which are -`with_defining_ids` and `with_original_ids`. +[`std::type_name`][type-name-stdlib]. Its main functions are `with_defining_ids` and +`with_original_ids`, which capture the type as a `TypeName` value, and their lighter counterparts +that return only the package address: ```move let defining_type_name: TypeName = type_name::with_defining_ids(); @@ -59,10 +60,10 @@ public struct Version1 has drop {} public struct Version2 has drop {} ``` -## In practice +## In Practice -The module is straightforward, and operations allowed on the result are limited to getting a string -representation and extracting the module and address of the type. +The module is straightforward: the operations allowed on the resulting `TypeName` are limited to +getting a string representation and extracting the module name and address of the type. ```move file=packages/samples/sources/move-basics/type-reflection.move anchor=main diff --git a/book/move-basics/vector.md b/book/move-basics/vector.md index d2a9bf6ea..86491b065 100644 --- a/book/move-basics/vector.md +++ b/book/move-basics/vector.md @@ -1,57 +1,129 @@ --- -description: "Vectors in Move: create, manipulate, and iterate over dynamic collections with push, pop, and destroy operations." +description: + 'Vectors in Move: create dynamic collections, read, add and remove elements, iterate with vector + macros, and destroy vectors of non-droppable types.' --- # Vector -Vectors are a native way to store collections of elements in Move. They are similar to arrays in -other programming languages, but with a few differences. In this section, we introduce the `vector` -type and its operations. +A `vector` is the built-in way to store collections of elements in Move. It is an ordered, growable +collection, similar to arrays or lists in other programming languages, and it is a building block +for other types: the [`Option`](./option) and [`String`](./string) types introduced in the sections +that follow are both backed by a vector. In this section, we introduce the `vector` type, its +operations, and the macros that make working with it convenient. -## Vector syntax +## Vector Syntax The `vector` type is written using the `vector` keyword followed by the type of the elements in angle brackets. The type of the elements can be any valid Move type, including other vectors. -Move has a vector literal syntax that allows you to create vectors using the `vector` keyword +Move also has a vector literal syntax that allows you to create vectors using the `vector` keyword followed by square brackets containing the elements (or no elements for an empty vector). ```move file=packages/samples/sources/move-basics/vector.move anchor=literal ``` -The `vector` type is a built-in type in Move, and does not need to be imported from a module. -Vector operations are defined in the `std::vector` module, which is implicitly imported -and can be used directly without explicit `use` import. +The `vector` type is a built-in type in Move, and does not need to be imported from a module. Vector +operations are defined in the `std::vector` module of the [Standard Library](./standard-library), +which is implicitly imported and can be used directly without an explicit `use` statement. -## Vector operations +> In this section we call vector functions with the dot syntax, for example `v.length()` instead of +> `vector::length(&v)`. This is the _receiver syntax_, available for standard library types out of +> the box; we explain how it works in the [Struct Methods](./struct-methods) section. -The standard library provides methods to manipulate vectors. The following are some of the most -commonly used operations: +## Reading Elements -- `push_back`: Adds an element to the end of the vector. -- `pop_back`: Removes the last element from the vector. -- `length`: Returns the number of elements in the vector. -- `is_empty`: Returns true if the vector is empty. -- `remove`: Removes an element at a given index. +The most basic things to ask of a collection are its size and its elements. The `length` function +returns the number of elements, `is_empty` tells whether there are none, and the index syntax `v[i]` +accesses a single element. Indices start at zero, and accessing an index outside of bounds aborts +execution: + +```move file=packages/samples/sources/move-basics/vector.move anchor=access + +``` + +> The `v[i]` syntax is a shorthand for calling the `borrow` function - it yields a +> [reference](./references) to the element, not the element itself. For copyable types, like the +> integers above, the difference is invisible; for types that cannot be copied, taking an element +> _out_ of a vector requires `pop_back`, `remove`, or `swap_remove` described below. The details of +> this syntax are described in [Index Syntax](./../../reference/index-syntax) in the Move Reference. + +## Adding and Removing Elements + +A mutable vector can grow and shrink. The most efficient operations work on the _end_ of the +vector - `push_back` and `pop_back` - while `insert` and `remove` work at an arbitrary index and +shift all of the elements after it: ```move file=packages/samples/sources/move-basics/vector.move anchor=methods ``` -## Destroying a Vector of non-droppable types +The table below lists the most commonly used functions of the `std::vector` module; see the [module +documentation][vector-stdlib] for the full list: + +
+ +| Function | Description | Aborts If | +| --------------- | -------------------------------------------------- | -------------------------- | +| `length` | Returns the number of elements | - | +| `is_empty` | Returns `true` if the vector has no elements | - | +| `push_back` | Adds an element to the end | - | +| `pop_back` | Removes and returns the last element | The vector is empty | +| `insert` | Inserts an element at the index, shifting the rest | The index is out of bounds | +| `remove` | Removes and returns the element at the index | The index is out of bounds | +| `swap_remove` | Swaps the element with the last one and removes it | The index is out of bounds | +| `swap` | Swaps the elements at two indices | An index is out of bounds | +| `contains` | Returns `true` if the vector contains the element | - | +| `index_of` | Returns `(true, index)` if the element is found | - | +| `append` | Moves all elements from another vector to the end | - | +| `reverse` | Reverses the order of the elements | - | +| `destroy_empty` | Destroys an empty vector | The vector is not empty | + +
+ +> Note that `remove` shifts every element after the removed one, which makes it more expensive the +> longer the vector is. If the order of elements does not matter, `swap_remove` does the same job in +> constant time. + +## Vector Macros + +Reading, transforming, or aggregating every element of a vector is such a common task that the +standard library provides a set of _macros_ for it. Macro names end with a `!` and take a _lambda_ +(an inline function written as `|argument| expression`) which the macro applies to the elements. +Under the hood a macro expands into a regular loop at compilation time, so using one costs nothing +extra at runtime: + +```move file=packages/samples/sources/move-basics/vector.move anchor=macros + +``` + +Other commonly used macros include `filter!`, `any!`, `all!`, `find_index!`, and `tabulate!` - each +of them replaces a hand-written loop with a single expressive line. The full list is available in +the [module documentation][vector-stdlib], and macros in general are covered later in this chapter, +in the [Macro Functions](./macros) section. -A vector of non-droppable types cannot be discarded. If you define a vector of types without the -`drop` ability, the vector value cannot be ignored. If the vector is empty, the compiler requires an -explicit call to the `destroy_empty` function. +## Destroying a Vector of Non-Droppable Types + +The `vector` type inherits its [abilities](./abilities-introduction) from its elements: a +`vector` can only be [dropped](./drop-ability) if `T` can. A vector of types without the `drop` +ability cannot be ignored, even when it is empty, and the compiler requires an explicit call to the +`destroy_empty` function: ```move file=packages/samples/sources/move-basics/vector.move anchor=no_drop ``` -The `destroy_empty` function will fail at runtime if you call it on a non-empty vector. +The `destroy_empty` function will fail at runtime if you call it on a non-empty vector. This is the +resource model at work: if the elements of a vector represent assets, neither the assets nor the +vector holding them can silently disappear - every element must be taken out and handled before the +vector itself is destroyed. ## Further Reading - [Vector](./../../reference/primitive-types/vector) in the Move Reference. -- [std::vector](https://docs.sui.io/references/framework/std/vector) module documentation. +- [Index Syntax](./../../reference/index-syntax) in the Move Reference. +- [Macro Functions](./../../reference/functions/macros) in the Move Reference. +- [std::vector][vector-stdlib] module documentation. + +[vector-stdlib]: https://docs.sui.io/references/framework/std/vector diff --git a/book/move-basics/visibility.md b/book/move-basics/visibility.md index f4cb5e1fb..45d46eca1 100644 --- a/book/move-basics/visibility.md +++ b/book/move-basics/visibility.md @@ -1,5 +1,7 @@ --- -description: "Visibility modifiers in Move: private, public, public(package), and entry functions for controlling access to module members." +description: + 'Visibility modifiers in Move: private, public, public(package), and entry functions for + controlling access to module members.' --- # Visibility Modifiers @@ -7,8 +9,10 @@ description: "Visibility modifiers in Move: private, public, public(package), an Every module member has a visibility. By default, all module members are _private_ - meaning they are only accessible within the module they are defined in. However, you can add a visibility modifier to make a module member _public_ - visible outside the module, or _public(package)_ - -visible in the modules within the same package, or _entry_ - can be called from a transaction but -can't be called from other modules. +visible in the modules within the same package. Additionally, a function can be marked with the +_entry_ modifier, which allows a _non-public_ function to be called from a transaction. Unlike the +rest, `entry` is not a visibility level - it can be combined with them, and it controls how the +function interacts with transactions rather than with other modules. ## Internal Visibility @@ -27,9 +31,7 @@ fun call_internal() { } ``` -The following code will not compile: - - +The following code will not compile, because `internal` is private to `book::internal_visibility`: ```move module book::try_calling_internal; @@ -39,11 +41,14 @@ use book::internal_visibility; // Different module -> can't call internal() fun try_calling_internal() { internal_visibility::internal(); + // ^ ERROR! [E04001]: restricted visibility + // Invalid call to internal function + // 'book::internal_visibility::internal' } ``` Note that just because a struct field is not visible from Move does not mean that its value is kept -confidential — it is always possible to read the contents of an on-chain object from outside +confidential — it is always possible to read the contents of an onchain object from outside of Move. You should never store unencrypted secrets inside of objects. ## Public Visibility @@ -71,7 +76,11 @@ fun try_calling_public() { } ``` -Unlike some languages, struct fields cannot be made public. +A `public` function can also be called directly from a +[transaction](./../concepts/what-is-a-transaction). Making a function `public` is the default - and +recommended - way to expose functionality to users: a public function can be a command in a +transaction, be freely combined with other commands in it, and serve as a building block for other +packages. No extra modifier is needed for any of this. ## Package Visibility @@ -97,6 +106,56 @@ fun try_calling_package() { } ``` +## Entry Modifier + +As shown [above](#public-visibility), a `public` function is already callable from a +[transaction](./../concepts/what-is-a-transaction) - `public` is the default and preferred way to +make a function available, to transactions and other modules alike. The `entry` modifier serves the +opposite goal: a function that can be called _only_ as a command in a transaction. Marking a +_non-public_ function with `entry` keeps it out of reach of other modules' code, while permitting +it as a transaction command - deliberately limiting who can call it and how. It is not a visibility +level: an `entry` function keeps whatever visibility it is declared with. A function marked `entry` +with no other modifier stays _private_ - callable as a transaction command and from its own module, +and nothing else. + +```move +module book::entry_functions; + +// Can be called from a transaction, but not from other modules +entry fun from_transaction_only() { /* ... */ } + +// Can be called from a transaction and from modules of the same package +public(package) entry fun from_package_or_transaction() { /* ... */ } +``` + +Public functions can already be called from transactions, so `entry` adds nothing to a `public` +function, and the compiler warns about the combination: + +```text +warning[Lint W99010]: unnecessary `entry` on a `public` function + │ +7 │ public entry fun both() { } + │ ^^^^^ `entry` on `public` is meaningless. In conjunction with `public`, + │ `entry` adds no additional permissions or restrictions. +``` + +Any Move function can be marked `entry` - there are no restrictions on its signature. The value of +the modifier lies in what it does for _non-public_ functions: they become callable as transaction +commands while staying out of the module's API - and the transaction calling them accepts +additional checks on the arguments it passes. + +That guarantee concerns _hot potatoes_ - values that must be consumed before a transaction ends: the +arguments of a non-`public` `entry` function are statically guaranteed not to be entangled with any +such outstanding obligation, which is what lets `entry` serve as a safe transaction boundary. The +full rules, with a worked flash-loan example, are covered in +[Entry Functions](./../move-advanced/entry-functions) in the Advanced Move Features chapter. + +To summarize: `entry` limits composability - in both directions. A non-public `entry` function is +not part of the module's API, so other packages cannot call it or build on it; and inside a +transaction, its arguments face restrictions that `public` function arguments do not. Reach for it +when that is the point - when a function should be callable _only_ as a transaction command, or +when it needs the argument guarantee. For everything else, `public` is the right choice. + ## Native Functions Some functions in the [framework](./../programmability/sui-framework) and diff --git a/book/object/digital-assets.md b/book/object/digital-assets.md index b2f3a48e5..41e510d1d 100644 --- a/book/object/digital-assets.md +++ b/book/object/digital-assets.md @@ -15,17 +15,18 @@ indivisible assets. These standards laid the groundwork for the complex digital -However, Ethereum's programming model lacked a native representation of assets. In other words, -externally, a Smart Contract behaved like an asset, but the language itself did not have a way to -inherently represent assets. From the start, Move aimed to provide a first-class abstraction for -assets, opening up new avenues for thinking about and programming assets. +However, Ethereum's programming model lacked a native representation of assets. From the outside, +an ERC-20 token behaved like an asset, but inside the contract it existed only as entries in a +ledger - a mapping of addresses to balances - with no value in the language that _is_ the asset. +From the start, Move aimed to provide a first-class abstraction for assets, opening up new avenues +for thinking about and programming assets. It is important to highlight which properties are essential for an asset: -- **Ownership:** Every asset is associated with an owner(s), mirroring the straightforward concept - of ownership in the physical world—just as you own a car, you can own a digital asset. Move +- **Ownership:** Every asset is associated with an owner, mirroring the straightforward concept of + ownership in the physical world, just as you own a car, you can own a digital asset. Move enforces ownership in such a way that once an asset is _moved_, the previous owner completely loses any control over it. This mechanism ensures a clear and secure change of ownership. @@ -39,8 +40,14 @@ It is important to highlight which properties are essential for an asset: transferred or destroyed. This property guarantees the deliberate handling of digital assets, preventing accidental loss and ensuring accountability in asset management. -Move managed to encapsulate these properties in its design, becoming an ideal language for digital -assets. +You have already met all three of these properties as language features. Ownership is enforced by +[move semantics](./../move-basics/ownership-and-scope): passing a value by value _moves_ it, and +the previous scope loses access. And the ability system controls the other two: a struct without +the [`copy`](./../move-basics/copy-ability) ability cannot be duplicated, and a struct without the +[`drop`](./../move-basics/drop-ability) ability cannot be thrown away. What looked like a set of +restrictions in the [Move Basics](./../move-basics) chapter turns out to be the exact toolkit for +modeling assets: a type with neither `copy` nor `drop` _must_ be explicitly handled - stored, +transferred, or destroyed - every time it is created. ## Summary @@ -48,6 +55,8 @@ assets. create and manage assets natively. - Essential properties of digital assets include ownership, non-copyability, and non-discardability, which Move enforces in its design. +- These properties map directly onto language features you already know: move semantics and the + `copy` and `drop` abilities. - Move's asset model mirrors real-world asset management, ensuring secure and accountable asset ownership and transfer. diff --git a/book/object/evolution-of-move.md b/book/object/evolution-of-move.md index e38ce89dd..7398c4761 100644 --- a/book/object/evolution-of-move.md +++ b/book/object/evolution-of-move.md @@ -4,27 +4,42 @@ description: "The evolution of Move from Diem to Sui: how the storage model chan # Evolution of Move -While Move was created to manage digital assets, its initial storage model was bulky and not -well-suited for many use cases. For instance, if Alice wanted to transfer an asset X to Bob, Bob had -to create a new "empty" resource, and then Alice could transfer asset X to Bob. This process was not -intuitive and presented implementation challenges, partly due to the restrictive design of -[Diem](https://www.diem.com/en-us). Another drawback of the original design was the lack of built-in -support for a "transfer" operation, requiring every module to implement its own storage transfer -logic. Additionally, managing heterogeneous collections of assets in a single account was -particularly challenging. - -Sui addressed these challenges by redesigning the storage and ownership model of objects to more -closely resemble real-world object interactions. With a native concept of ownership and _transfer_, -Alice can directly transfer asset X to Bob. Furthermore, Bob can maintain a collection of different -assets without any preparatory steps. These improvements laid the foundation for the Object Model in -Sui. +Move was created at [Diem](https://www.diem.com/en-us) to manage digital assets, and its original +storage model reflected the design of that blockchain. Storage was _account-based_: every piece of +data - called a _resource_ - lived under an account address, and a module could store, read, and +remove resources only under the accounts that interacted with it. In its original form, Move had +dedicated global storage operators for this, and a resource could only be placed under an account +if that account agreed to it by signing a transaction. + +This model had practical consequences that made everyday asset operations surprisingly hard: + +- There was no built-in _transfer_ operation. If Alice wanted to send an asset X to Bob, the module + defining X had to implement transfer logic itself: Bob first had to publish an "empty" resource + under his account (agreeing to receive the asset), and only then could Alice's transaction move + the balance into it. Every module reinvented this dance. +- Assets were stored per-type, per-account. Managing a heterogeneous collection - say, a single + account holding many different kinds of items - required significant effort and preparation for + each new type. +- Because data lived under accounts, an asset did not have an identity of its own: there was no way + to point at "this specific item" and follow it across owners. + +Sui addressed these challenges by redesigning the storage model around the assets themselves. In +Sui, the unit of storage is not an account but an _object_ - a typed value with its own unique +identifier and an owner recorded by the system. Ownership and _transfer_ became native operations: +Alice can directly transfer asset X to Bob, without Bob preparing anything in advance, and Bob can +hold any number of assets of any types. The global storage operators of the original Move are +absent in Move on Sui - in the [Using Objects](./../storage) chapter, we will see that they are +replaced by functions operating on objects. + +These changes laid the foundation for the Object Model, which we describe in the next section. ## Summary -- Move's initial storage model was not well-suited for managing digital assets, requiring complex - and restrictive transfer operations. -- Sui introduced the Object Model, which provides a native concept of ownership, simplifying asset - management and enabling heterogeneous collections. +- Original Move used account-based global storage: resources lived under account addresses, there + was no native transfer operation, and heterogeneous collections were hard to manage. +- Sui redesigned storage around _objects_ - typed values with their own identity and + system-tracked ownership - making transfer a native operation. +- Move on Sui removes the global storage operators, replacing them with object storage functions. ## Further Reading diff --git a/book/object/fast-path-and-consensus.md b/book/object/fast-path-and-consensus.md index 41c6412e4..9411d7025 100644 --- a/book/object/fast-path-and-consensus.md +++ b/book/object/fast-path-and-consensus.md @@ -2,7 +2,7 @@ description: "Fast path vs consensus in Sui: how owned objects skip consensus for faster transactions while shared objects require ordering." --- -# Fast Path & Consensus +# Fast Path and Consensus The Object Model allows for variable transaction execution paths, depending on the object's ownership type. The transaction execution path determines how the transaction is processed and @@ -23,36 +23,57 @@ transaction succeeds while the other is rightfully rejected. ## Fast Path -However, not all transactions require the same level of validation and consensus. For example, if -Alice wants to transfer an object that she owns to Bob, the network can process this transaction -without sequencing it with respect to all other transactions in the network, as only Alice has the -authority to access the object. This is known as the _fast path_ execution, where transactions -accessing account-owned objects are processed quickly without the need for extensive consensus. No -concurrent data access -> simpler challenge -> fast path. +However, not all transactions require the same level of validation. If Alice transfers an object +she owns to Bob, no other party could have touched that object in the first place - Alice is its +single owner. There is no conflict to resolve, so the network does not need to order this +transaction against all other transactions in the network. Transactions that access only +account-owned objects take the _fast path_: they skip full sequencing and are processed quickly. +This is a direct payoff of the [single owner](./ownership#account-owner-or-single-owner) model - +exclusive access removes the concurrency problem entirely. -Another ownership model that allows for fast path execution is the _immutable state_. Since -immutable objects cannot change, transactions involving them can be processed quickly without the -need to sequence them. +Immutable objects also qualify for the fast path. Since a +[frozen object](./ownership#immutable-frozen-state) can never change, any number of transactions +can read it concurrently without any ordering. ## Consensus Path -Transactions that do access shared state - on Sui it is represented with shared objects - require -sequencing to ensure that the state is updated and consistent across all nodes. This is known as the -execution through _consensus_, where transactions accessing shared objects are subject to the -agreement process to maintain network consistency. +Transactions that access _shared_ objects are the case consensus exists for: multiple parties may +attempt to modify the same object at the same time, so the network must agree on the order of +these modifications. Such transactions go through the _consensus path_ - they are sequenced by the +consensus protocol before execution, which keeps the state consistent across all nodes. - +[Party objects](./ownership#party-objects) also take the consensus path, even though they have a +single owner - that is precisely their trade-off: owner-only access with consensus ordering. -## Objects owned by Objects +An important detail: consensus on Sui orders transactions _per object_, not globally. Two +transactions touching two unrelated shared objects do not compete with each other - only +transactions accessing the _same_ shared object need to be ordered relative to each other. This is +what allows Sui to execute non-conflicting transactions in parallel. -Lastly, it is important to mention that objects owned by other objects are subject to the same rules -as the parent object. If the parent object is _shared_, the child object is also transitively -shared. If the parent object is immutable, the child object is also immutable. +A single transaction can mix inputs: if it accesses both owned and shared objects, it goes through +consensus - the execution path is determined by the "slowest" input. This is worth keeping in mind +when designing an application: whether your central state is a shared object or stays within owned +objects directly affects how your users' transactions are executed. + +## Objects Owned by Objects + +Lastly, objects owned by other objects follow the execution path of their parent - a child is only +reachable through its parent, so accessing it means accessing the parent first. If the parent +object is _shared_, working with the child requires consensus; if the parent is account-owned, the +whole chain qualifies for the fast path. ## Summary -- **Fast Path:** Transactions involving account-owned objects or immutable shared state are - processed quickly without the need for extensive consensus. -- **Consensus Path:** Transactions involving shared objects require sequencing and consensus to - ensure network integrity. -- **Objects owned by Objects:** Child objects inherit the ownership model of the parent object. +- **Fast Path:** Transactions involving only account-owned or immutable objects are processed + quickly without full consensus sequencing. +- **Consensus Path:** Transactions involving shared or party objects are sequenced by consensus - + per object, allowing non-conflicting transactions to run in parallel. +- **Mixed Inputs:** A transaction touching both owned and shared objects goes through consensus. +- **Objects Owned by Objects:** Child objects follow the execution path of their parent. + +## Next Steps + +This concludes the conceptual tour of the Object Model: you know what an object is, who can own +it, and how ownership shapes execution. The next chapter - [Using Objects](./../storage) - turns +these concepts into code: how to define an object, and how to transfer, share, and freeze it from +a Move module. diff --git a/book/object/index.md b/book/object/index.md index a45e1430a..c086ae6be 100644 --- a/book/object/index.md +++ b/book/object/index.md @@ -4,17 +4,28 @@ description: "The Sui Object Model explained: theory and concepts behind digital # Object Model -This chapter describes the Object Model of Sui. It focuses on the theory and concepts behind the -Object Model, preparing you for a practical dive into Sui Storage operations and resource ownership. -For convenience and easier lookup, we split the chapter into several sections, each covering a -specific aspect of the Object Model. +So far we have studied Move as a language: types, functions, and abilities, all operating on values +that live and die within a single transaction. But a smart contract is only useful if its state +persists. This chapter introduces the _Object Model_ - the answer Sui gives to the question of how +data is stored, owned, and accessed onchain. -
+The chapter focuses on theory and concepts, preparing you for a practical dive into storage +operations and resource ownership. It reads best in order: -In no way should this chapter be considered a comprehensive guide to the Object Model. It is only a -high-level overview of the concepts and principles behind the Object Model. +- [Language for Digital Assets](./digital-assets) - why Move treats assets as first-class values, + and which properties make an asset; +- [Evolution of Move](./evolution-of-move) - how the original account-based storage model worked, + and why Sui replaced it; +- [What is an Object?](./object-model) - the object as the unit of storage: type, ID, owner, + version, and digest; +- [Ownership](./ownership) - the five ways an object can be owned, and what each of them allows; +- [Fast Path and Consensus](./fast-path-and-consensus) - how ownership determines the way a + transaction is executed. -For a more detailed description, refer to the -[Sui Documentation](https://docs.sui.io/guides/developer/objects/object-model). +The chapters that follow build directly on these concepts: [Using Objects](./../storage) shows how +objects are defined and managed in code, and +[Advanced Programmability](./../programmability) covers the features built on top of them. -
+> This chapter is a high-level overview of the concepts and principles behind the Object Model. For +> a more detailed, protocol-level description, refer to the +> [Sui Documentation](https://docs.sui.io/guides/developer/objects/object-model). diff --git a/book/object/object-model.md b/book/object/object-model.md index 2bd685a9b..2aa78ac9d 100644 --- a/book/object/object-model.md +++ b/book/object/object-model.md @@ -1,52 +1,69 @@ --- -description: "What is a Sui Object: unique identifiers, types, ownership, and native operations like transfer and share for on-chain digital assets." +description: "What is a Sui Object: unique identifiers, types, ownership, and native operations like transfer and share for onchain digital assets." --- # What is an Object? -The Object Model in Sui can be viewed as a high-level abstraction representing digital assets as -_objects_. These objects have their own type and associated behaviors, a unique identifier, and -support native storage operations like _transfer_ and _share_. Designed to be intuitive and easy to -use, the Object Model enables a wide range of use cases to be implemented with ease. +An _object_ is the unit of storage on Sui. Where the original Move stored data under accounts, Sui +stores objects directly in the global state, each with its own identity, type, and owner recorded +by the system. Objects support native storage operations like _transfer_ and _share_, and are +designed to make the asset properties from the +[previous sections](./digital-assets) - ownership, non-copyability, non-discardability - practical +to work with. -Objects in Sui have the following properties: +In Move code, an object is not a new kind of value - it is a regular [struct](./../move-basics/struct) +with the `key` ability and a special `id` field: + +```move +/// A game character; a struct like any other, made an object +/// by the `key` ability and the `id: UID` field. +public struct Hero has key { + id: UID, + level: u8, +} +``` + +Everything you know about structs still applies. What the object adds is the system-level metadata +attached to it in storage. We cover the definition rules in detail in the +[Using Objects](./../storage) chapter; here we focus on the properties every object has: - **Type:** Every object has a type, defining the structure and behavior of the object. Objects of different types cannot be mixed or used interchangeably, ensuring objects are used correctly according to their type system. - **Unique ID:** Each object has a unique identifier, distinguishing it from other objects. This ID - is generated upon the object's creation and is immutable. It's used to track and identify objects - within the system. - - + is generated upon the object's creation and is immutable, so an object can be tracked and + referenced across transactions and owners. This is the `id: UID` field in the definition above. - **Owner:** Every object is associated with an owner, who has control over changes to the object. - Ownership on Sui can be exclusive to an account, shared across the network, or frozen, allowing - read-only access without modification or transfer capabilities. We will discuss ownership in more - detail in the following sections. + An object can be owned exclusively by an account, owned by another object, shared with the whole + network, made immutable, or held in the _party_ state - a middle ground between exclusive and + shared ownership. We discuss all five ownership states in detail in the + [Ownership](./ownership) section. Note that ownership does not control the confidentiality of an object — it is always - possible to read the contents of an on-chain object from outside of Move. You should never store + possible to read the contents of an onchain object from outside of Move. You should never store unencrypted secrets inside of objects. - **Data:** Objects encapsulate their data, simplifying management and manipulation. The data - structure and operations are defined by the object's type. + structure and operations are defined by the object's type - the fields of the struct. -- **Version:** The transition from accounts to objects is facilitated by object versioning. - Traditionally, blockchains use a _nonce_ to prevent replay attacks. In Sui, the object's version - acts as a nonce, preventing replay attacks for each object. +- **Version:** Every object carries a version number, which the system increments each time a + transaction modifies the object. The version protects against _replay_: a transaction refers to + its input objects at specific versions, so the same transaction - or a stale reference to an + already-changed object - cannot be executed twice. It plays the role a _nonce_ plays in + account-based blockchains, but per object rather than per account. -- **Digest:** Every object has a digest, which is a hash of the object's data. The digest is used to - cryptographically verify the integrity of the object's data and ensures that it has not been - tampered with. The digest is calculated when the object is created and is updated whenever the - object's data changes. +- **Digest:** Every object has a digest, which is a hash of the object's data. The digest is used + to cryptographically verify the integrity of the object's data and ensure that it has not been + tampered with. It is recalculated whenever the object's data changes. ## Summary -- Objects in Sui are high-level abstractions representing digital assets. -- Objects have a type, unique ID, owner, data, version, and digest. -- The Object Model simplifies asset management and enables a wide range of use cases. +- Objects are the unit of storage on Sui: typed values stored in the global state with + system-tracked identity and ownership. +- In Move code, an object is a struct with the `key` ability and an `id: UID` field. +- Every object has a type, unique ID, owner, data, version, and digest. ## Further Reading diff --git a/book/object/ownership.md b/book/object/ownership.md index e00d0c6c9..f44243e9b 100644 --- a/book/object/ownership.md +++ b/book/object/ownership.md @@ -4,15 +4,12 @@ description: "Object ownership types in Sui: single owner, shared state, immutab # Ownership -Sui introduces four distinct ownership types for objects: single owner, shared state, immutable -shared state, and object-owner. Each model offers unique characteristics and suits different use -cases, enhancing flexibility and control in object management. +Every object on Sui is in one of five ownership states: _single owner_, _shared_, _immutable +(frozen)_, _object owner_, or _party_. Each model offers unique characteristics and suits different +use cases, and - as we will see in the [next section](./fast-path-and-consensus) - the choice of +ownership also determines how transactions touching the object are executed. -Note that ownership does not control the confidentiality of an object — it is always possible -to read the contents of an on-chain object from outside of Move. You should never store unencrypted -secrets inside of objects. - -See the [Storage Functions](../storage/storage-functions.md) chapter for details on how to change +See the [Storage Functions](../storage/storage-functions.md) section for details on how to change the owner or ownership type of an object. ## Account Owner (or Single Owner) @@ -20,46 +17,61 @@ the owner or ownership type of an object. The account owner, also known as the _single owner_ model, is the foundational ownership type in Sui. Here, an object is owned by a single account, granting that account exclusive control over the object within the behaviors associated with its type. This model embodies the concept of _true -ownership_, where the account possesses complete authority over the object, making it inaccessible -to others for modification or transfer. This level of ownership clarity is a significant advantage -over other blockchain systems, where ownership definitions can be more ambiguous, and smart -contracts may have the ability to alter or transfer assets without the owner's consent. +ownership_: only the owner can use the object in a transaction - whether to read it, modify it, or +transfer it away - and nobody else can touch it. This level of ownership clarity is a significant +advantage over other blockchain systems, where ownership definitions can be more ambiguous, and +smart contracts may have the ability to alter or transfer assets without the owner's consent. -Just like your private mobile phone, you own it, you can easily unlock and operate it, but others cannot. -Sui is designed to prevent things like cracking someone else's phone password (bypassing permission -checks to use objects that do not belong to others). Therefore, no one can use your assets unless -authorized by you. +Think of it like your mobile phone: you can unlock and operate it, and others cannot. Sui enforces +this at the system level - there is no way to "crack the password" and use an object that belongs +to someone else, so no one can use your assets unless you authorize it. ## Shared State -Single owner model has its limitations: for example, it is very tricky to implement a marketplace -for digital assets without a shared state. For a generic marketplace scenario, imagine that Alice -owns an asset X, and she wants to sell it by putting it into a shared marketplace. Then Bob can come -and buy the asset directly from the marketplace. The reason why this is tricky is that it is -impossible to write a smart contract that would "lock" the asset in Alice's account and take it out -when Bob buys it. First, it will be a violation of the single owner model, and second, it requires a -shared access to the asset. - -To facilitate a problem of shared data access, Sui has introduced a shared ownership model. In this -model, an object can be shared with the network. Shared objects can be read and modified by any -account on the network, and the rules of interaction are defined by the implementation of the -object. Typical uses for shared objects are: marketplaces, shared resources, escrows, and other -scenarios where multiple accounts need access to the same state. +The single owner model has its limitations. Consider a marketplace for digital assets: Alice owns +an asset X and wants to list it for sale, so that Bob - or anyone else - can come and buy it. With +only single-owner objects this is surprisingly hard to express: for the sale to happen without +Alice's participation, the asset has to sit in a place that both the seller and any future buyer +can access, and no single account can be its owner. + +To solve the problem of shared data access, Sui offers the _shared_ ownership model. A shared +object belongs to the network: it can be read and modified by any account, and the rules of +interaction are defined by the module that implements the object. Typical uses for shared objects +are marketplaces, shared resources, escrows, and other scenarios where multiple accounts need +access to the same state. + +## Party Objects + +The newest ownership state, the _party_ object, sits between the two models above: like a +single-owner object, it has an owner - an address whose permission is required to use it - but, +like a shared object, transactions touching it are ordered by consensus. Today a party object is +always owned by a single address; the state is designed to eventually support more complex +configurations, with permissions split between multiple parties. + +Party objects trade away the speed of exclusive ownership for the flexibility of consensus +ordering - useful for assets that are frequently touched by high-traffic services, where many +independent transfers to and from the same owner may be in flight at once. For most applications, +they are an advanced option rather than the starting point: begin with single-owner objects, and +reach for party objects when a concrete need arises. + +> Party objects are listed here for the complete picture. Their transfer functions are covered in +> [Appendix C: Transfer Functions](./../appendix/transfer-functions#party), and the +> [`sui::party`](https://docs.sui.io/references/framework/sui/party) module documentation covers +> the details. ## Immutable (Frozen) State Sui also offers the _frozen object_ model, where an object becomes permanently read-only. These -immutable objects, while readable, cannot be modified or moved, providing a stable and constant -state accessible to all network participants. Frozen objects are ideal for public data, reference -materials, and other use cases where the state permanence is desirable. +immutable objects, while readable, cannot be modified, transferred, or deleted, providing a stable +and constant state accessible to all network participants. Frozen objects are ideal for public +data, reference materials, and other use cases where state permanence is desirable. ## Object Owner -The last ownership model in Sui is the _object owner_. In this model, an object is owned by another -object. This feature allows creating complex relationships between objects, storing large -heterogeneous collections, and implementing extensible and modular systems. Practically speaking, -since the transactions are initiated by accounts, the transaction still accesses the parent object, -but it can then access the child objects through the parent object. +The last ownership model in Sui is the _object owner_: an object owned by another object. This +feature allows creating complex relationships between objects, storing large heterogeneous +collections, and implementing extensible and modular systems. Since transactions are initiated by +accounts, a transaction accesses the parent object first, and reaches the child objects through it. A use case we love to mention is a game character. Alice can own the Hero object from a game, and the Hero can own items: also represented as objects, like a "Map", or a "Compass". Alice may take @@ -67,12 +79,18 @@ the "Map" from the "Hero" object, and then send it to Bob, or sell it on a marke owner, it becomes very natural to imagine how the assets can be structured and managed in relation to each other. +> There are two mechanisms behind parent-child relations, both covered later in the book: +> [Dynamic Fields](./../programmability/dynamic-fields) and +> [Transfer to Object](./../storage/transfer-to-object). + ## Summary - **Single Owner:** Objects are owned by a single account, granting exclusive control over the object. - **Shared State:** Objects can be shared with the network, allowing multiple accounts to read and modify the object. +- **Party:** Objects have a single owner but are sequenced through consensus - a newer, advanced + option. - **Immutable State:** Objects become permanently read-only, providing a stable and constant state. - **Object Owner:** Objects can own other objects, enabling complex relationships and modular systems. diff --git a/book/programmability/address-balances.md b/book/programmability/address-balances.md new file mode 100644 index 000000000..0fb2d799e --- /dev/null +++ b/book/programmability/address-balances.md @@ -0,0 +1,157 @@ +--- +description: "Address balances on Sui: hold fungible value directly at an address without a Coin object, send funds with send_funds, and withdraw them with a Withdrawal." +--- + +# Address Balances + +A [`Coin`](./balance-and-coin) is an object: to spend it, a transaction has to reference it by its +ID, fetch it, and pass it in. That works well for discrete assets, but it makes an account's funds +a set of individual objects that have to be tracked, merged, and split. _Address balances_ offer a +different model: fungible value held directly at an address, as a running total, with no object to +manage. + +Under the hood, the value lives in an onchain _accumulator_ keyed by the pair `(address, type)`. +The balance of `T` at an address is a single number that goes up when funds are sent to it and down +when they are withdrawn - much closer to how a bank account works than to a wallet full of coins. + +> Address balances are a recent addition to the Sui Framework. This section covers the core +> `send_funds` / `redeem_funds` API, withdrawing from an object, and the transaction-level rules +> that protect withdrawals from replay. + +## Sending Funds to an Address + +Any `Coin` or [`Balance`](./balance-and-coin#balance) can be deposited into an address balance +with `send_funds`. The value is consumed and credited to the recipient's balance of `T`: + +```move file=packages/samples/sources/programmability/address-balances.move anchor=pay + +``` + +`send_funds` is defined on both `Coin` and `Balance`. For a `Coin`, it turns the coin into a +`Balance` and adds it to the recipient's accumulator; there is no object left behind, and the +recipient does not need to "accept" anything - the balance simply increases. + +> The current value of an address balance can be read from Move with +> `balance::settled_funds_value`, given a reference to the system `AccumulatorRoot` object. As the +> name suggests, it reports the funds _settled_ as of the beginning of the current consensus +> commit - deposits made within the commit are not yet visible to it. + +## Withdrawing Funds + +Going the other way - taking value _out_ of an address balance - is deliberately more restricted. +You cannot read from an arbitrary address's balance and mint a coin from it; instead, a withdrawal +is represented by a `Withdrawal>` value, defined in the `sui::funds_accumulator` module +of the Sui Framework: + +```move +/// A permission to withdraw up to `limit` units of `T` from `owner`. +public struct Withdrawal has drop { + owner: address, + limit: u256, +} +``` + +A `Withdrawal` is an _authorization_, not the funds themselves. It records whose balance is being +drawn from (`owner`) and the maximum amount that may be taken (`limit`). It has `drop`, so an unused +one can simply be discarded. The transaction provides it - a `Withdrawal` for the transaction sender +is supplied as an input by the transaction builder, in the same spirit as the gas coin or a +[received object](./../storage/transfer-to-object). There is no constructor for it in user code. + +A transaction that spends from the sender's address balance therefore looks like this: the +`Withdrawal` comes in as an input, checked against the sender's balance at signing, and a command +turns it into a `Coin`: + +```text +// Spending 1_000 MIST from the sender's address balance +// Input 0: Withdrawal> { owner: sender, limit: 1_000 } +// Input 1: recipient address +0: sui::coin::redeem_funds(Input(0)); // -> Coin +1: TransferObjects([Result(0)], Input(1)); +``` + +Once a function has a `Withdrawal`, it redeems it into a real `Coin` with `redeem_funds`: + +```move file=packages/samples/sources/programmability/address-balances.move anchor=collect + +``` + +Redemption is where the amount is actually moved out of the accumulator. It can only be performed +from the module that defines the withdrawn type - this is enforced with the +[internal permit](./../move-basics/internal-permit) mechanism, which is exactly why `sui::coin` and +`sui::balance` (the modules that define `Coin` and `Balance`) are the ones exposing `redeem_funds`. + +## Inspecting and Splitting a Withdrawal + +Before redeeming, the `Withdrawal` can be inspected and divided. This is useful when a single +withdrawal needs to fund several operations: + +```move file=packages/samples/sources/programmability/address-balances.move anchor=split_join + +``` + +Splitting and joining a `Withdrawal` only moves the _limit_ around; no funds change hands until +`redeem_funds` is called. Joining requires both withdrawals to have the same `owner`, and aborts +otherwise. + +## Withdrawing from an Object + +The owner of a `Withdrawal` does not have to be an account - it can be an object. An object with an +address balance can produce a withdrawal from its own funds with `withdraw_funds_from_object`, +passing a mutable reference to its `UID`: + +```move file=packages/samples/sources/programmability/address-balances.move anchor=object_withdraw + +``` + +This lets any object - a shared vault, an escrow, a treasury - hold and pay out fungible value +without wrapping individual `Coin` objects. The withdrawal it produces is redeemed the same way as a +sender's - through `redeem_funds`. + +## Replay Protection and Parallel Execution + +Address balances also change how a transaction proves that it is unique and cannot be replayed. The +usual anchor is an [owned object](./../object/ownership#account-owner-or-single-owner): every object +carries a [version](./../object/object-model) that the system bumps on each change, so a signed +transaction referencing it can execute only once - after the version moves, the transaction no +longer matches. The gas coin normally provides this anchor for free. + +A transaction that has no owned-object input - for instance, one that pays gas straight from an +address balance, or whose inputs are only shared objects - has nothing to anchor it, so it must +carry the protection itself. Two fields of the transaction data cover this. SDKs set them when they +build such a transaction, so this is a matter of how the transaction is _constructed_ rather than +anything in Move code: + +- **Expiration (`ValidDuring`).** The transaction sets its expiration to + `TransactionExpiration::ValidDuring` with a `min_epoch` and a `max_epoch` spanning at most one + epoch (`max_epoch <= min_epoch + 1`). Bounding validity to a narrow epoch window bounds the window + in which the transaction could be replayed, taking the place of the version check that protects + owned objects. +- **Nonce.** The transaction includes a `nonce` - an arbitrary value whose only job is to make two + otherwise-identical transactions distinct. Unlike the nonces of account-based chains, it is not + sequential and has no gap problem; it simply lets transactions that would otherwise share a digest + coexist. + +These same properties are what keep such transactions parallelizable: with unique digests and the +[per-object ordering](./../object/fast-path-and-consensus#consensus-path) that Sui already uses, +non-conflicting withdrawals never have to wait on one another. + +## Summary + +- An _address balance_ is fungible value of type `T` held directly at an address in an onchain + accumulator, rather than as a `Coin` object; +- `coin.send_funds(recipient)` (or `balance.send_funds`) deposits value into an address balance, + consuming the coin; +- withdrawing requires a `Withdrawal>` - an authorization with an `owner` and a `limit` - + which the transaction provides for the sender, or an object provides for itself; +- `coin::redeem_funds` turns a `Withdrawal` into a `Coin`, and can only be called from the module + defining the type, via the [internal permit](./../move-basics/internal-permit) mechanism; +- a transaction with no owned-object input (paying gas from an address balance, or using only shared + objects) carries its own replay protection: a `ValidDuring` expiration bounded to one epoch, and a + `nonce` that makes its digest unique. + +## Further Reading + +- [sui::balance](https://docs.sui.io/references/framework/sui/balance) module documentation. +- [Using Address Balances](https://docs.sui.io/onchain-finance/asset-custody/address-balances/using-address-balances) + in the Sui Documentation. +- [Balance and Coin](./balance-and-coin) for the object-based side of fungible tokens. diff --git a/book/programmability/balance-and-coin.md b/book/programmability/balance-and-coin.md index 191ef7848..ae09aa68a 100644 --- a/book/programmability/balance-and-coin.md +++ b/book/programmability/balance-and-coin.md @@ -1,5 +1,381 @@ --- -description: "Balance and Coin in Sui Move: create fungible tokens, manage balances, and work with the Coin standard in smart contracts." +description: "Balance, Coin, and CoinRegistry in Sui Move: create fungible tokens with the Currency standard, manage supply with TreasuryCap, and store metadata onchain." --- -# Balance & Coin +# Balance and Coin + +Fungible tokens are the most common kind of digital asset: units of value that are interchangeable +with each other, like money. On Sui, the main abstraction for fungible tokens is +[`Coin`](https://docs.sui.io/references/framework/sui_sui/coin) - the object that wallets hold, +transactions take as inputs, and applications accept as payment. Owning "10 SUI" means owning a +`Coin` object with the value of 10 SUI. + +Two supporting types complete the standard - one layer below `Coin`, and one above: + +- [`Balance`](https://docs.sui.io/references/framework/sui_sui/balance) - the raw amount inside a + `Coin`: a plain value without an object ID, which applications use to store and accumulate funds; +- [`Currency`](https://docs.sui.io/references/framework/sui_sui/coin_registry) - a shared object + describing the coin type itself: its metadata, supply, and regulatory status. + +This section walks through all three, and shows how to create a currency with the +`sui::coin_registry` module - the standard way of doing it. + +## Balance + +The `Balance` type is defined in the `sui::balance` module. It is a plain value with the `store` +ability - not an object: it has no `UID` and no storage overhead of its own. This makes it the type +of choice for _keeping_ funds: whenever an application needs to store or accumulate value inside +its own types - a vault, a liquidity pool, an escrow - it embeds a `Balance`, not a `Coin`. + +```move +/// Storable balance - an inner struct of a Coin type. +/// Can be used to store coins which don't need the key ability. +public struct Balance has store { + value: u64, +} +``` + +The [phantom type parameter](./../move-basics/generics#phantom-type-parameters) `T` is what makes +one balance different from another: `Balance` and `Balance` are distinct, +non-interchangeable types, even though both store just a `u64`. + +`Balance` has no `copy`, no `drop`, and no public constructor for a non-zero value. A balance can +only be created by increasing the total supply of `T`, and can only disappear by decreasing it. +Everything in between - splitting, joining, storing - just moves the value around. This is the +[ownership](./../move-basics/ownership-and-scope) guarantee applied to money: no duplication, no +accidental loss. + +```move file=packages/samples/sources/programmability/balance-and-coin.move anchor=balance + +``` + +## Coin + +`Balance` cannot exist on its own in storage - it has to be wrapped in an object. The +`sui::coin::Coin` type is the standard wrapper: + +```move +/// A coin of type `T` worth `value`. +public struct Coin has key, store { + id: UID, + balance: Balance, +} +``` + +With `key` and `store`, a `Coin` is a full-fledged object: it can be owned by an account, +transferred, and passed into transactions as an input. The gas object used to pay for transactions +is a `Coin`. This gives the standard its rule of thumb: `Coin` at the boundary, `Balance` +inside. Funds enter an application as a `Coin`, are stored and accumulated as a `Balance`, and +leave as a `Coin` again. + +The API mirrors `Balance` - splitting, joining, and converting between the two: + +```move file=packages/samples/sources/programmability/balance-and-coin.move anchor=coin + +``` + +> The samples above conjure their `Coin` and `Balance` out of thin air with the test-only +> `coin::mint_for_testing` and `balance::create_for_testing` functions - the standard tools for +> testing coin-handling code, covered in +> [Using System Objects in Tests](./../testing/using-system-objects). + +In transactions, coins receive special treatment: the native `SplitCoins` and `MergeCoins` +[commands](./../concepts/what-is-a-transaction#commands) operate on coins directly, so a wallet can +prepare an exact payment - even split it off the gas coin - without calling any module functions. +This is why modules rarely need to expose split or merge functionality of their own. + +The `sui::coin` module also provides `coin::take` and `coin::put` helpers, which combine the +conversion and the split/join steps: `take` splits a `Coin` out of a `Balance`, and `put` merges a +`Coin` into a `Balance`. They come in handy when an application stores funds as a `Balance` and +sends them out as `Coin`s. + +> Coin objects are not the only way to hold fungible value: a newer mechanism keeps it directly at +> an address, as a running total with no object to manage. It builds on the types described here, +> and is covered in the [Address Balances](./address-balances) section. + +## Currency and the Coin Registry + +A single `Coin` says nothing about the token `T` itself: its name, its symbol, how many decimals +it uses, or how its supply is managed. This information is stored once per type in a `Currency` +object, and all currencies are tracked by the `CoinRegistry` - a system object with the reserved +address `0xc`: + +```move +/// System object found at address `0xc` that stores coin data for all +/// registered coin types. +public struct CoinRegistry has key { id: UID } +``` + +The `Currency` object holds everything there is to know about the coin type `T`: + +```move +/// Currency stores metadata such as name, symbol, decimals, icon_url and +/// description, as well as supply state (optional) and regulatory status. +public struct Currency has key { + id: UID, + /// Number of decimal places the coin uses for display purposes. + decimals: u8, + /// Human-readable name for the coin. + name: String, + /// Short symbol/ticker for the coin. + symbol: String, + /// Detailed description of the coin. + description: String, + /// URL for the coin's icon/logo. + icon_url: String, + /// Current supply state of the coin (fixed, burn-only, or unknown). + supply: Option>, + /// Regulatory status of the coin (regulated with deny cap or unknown). + regulated: RegulatedState, + /// ID of the treasury cap for this coin type, if registered. + treasury_cap_id: Option, + /// ID of the metadata capability for this coin type, if claimed. + metadata_cap_id: MetadataCapState, + /// Additional fields for extensibility. + extra_fields: VecMap, +} +``` + +Most of these fields get their own section on this page: the supply state, the regulatory status, +and the two capabilities are all covered below. One field, however, deserves attention right away: +`decimals`. Move has no fractional +numbers - the value of a `Coin` is a plain integer, counting the currency's smallest units, and +`decimals` tells clients where to put the decimal point _for display_. With `decimals = 8`, a +`Coin` with the value `100_000_000` is displayed as `1` coin; the native SUI currency has 9 +decimals, and its base unit even has a name of its own - MIST. Amounts in Move code - minting, +splitting, comparing - are always expressed in base units. + +The `coin_registry` module is _the_ way to create a currency: it replaced the original +`coin::create_currency` function, which stored metadata in a standalone `CoinMetadata` object (we +cover the differences [at the end of this section](#legacy-coin-metadata)). It offers two ways to +create a currency, both producing the same result: a shared `Currency` object with a +[derived address](https://docs.sui.io/references/framework/sui_sui/derived_object), so that the +metadata for any coin type can be found without knowing its object ID. + +### Creating a Currency in `init` + +The most common flow uses a [One-Time Witness](./one-time-witness) to guarantee that a currency for +the type can be created only once, in the [module initializer](./module-initializer): + +```move file=packages/samples/sources/programmability/balance-and-coin-2.move anchor=gold + +``` + +The `new_currency_with_otw` call returns two values: + +- `CurrencyInitializer` - a temporary value used to configure the currency before it is published. + It cannot be stored or dropped, so the transaction cannot succeed until it is consumed by the + `finalize` call (a technique we explore in the + [Hot Potato Pattern](./hot-potato-pattern) section); +- `TreasuryCap` - the [capability](./capability) that controls minting and burning, explored in + the [Supply and TreasuryCap](#supply-and-treasurycap) section below. + +The `finalize` call returns one more capability - the `MetadataCap`, which controls updates to +the currency metadata. However, in the OTW flow, `finalize` does not complete the registration. +Because `init` runs during publishing, before the `CoinRegistry` can be passed in as an argument, +the `Currency` object takes a detour: `finalize` transfers it to the registry's address, +where it waits for the second, closing step - `finalize_registration`: + +```move +/// The second step in the "otw" initialization of coin metadata, that takes in +/// the `Currency` that was transferred from init, and transforms it in to a +/// "derived address" shared object. +/// +/// Can be performed by anyone. +public fun finalize_registration( + registry: &mut CoinRegistry, + currency: Receiving>, + _ctx: &mut TxContext, +); +``` + +This function [receives](./../storage/transfer-to-object) the `Currency` sent to the registry +and re-creates it as a shared object with a derived address. Until it is called, the registration +is incomplete: the `Currency` is not shared, cannot be found at its derived address, and +cannot be passed into any function that reads or updates it. The call is permissionless - anyone +can make it, and indexers often do - but it should not be left to chance: + +> Treat `finalize_registration` as a mandatory part of the OTW flow, not as optional cleanup. The +> publisher should call it in a follow-up transaction right after publishing - only then is the +> currency fully registered and usable. + +### Creating a Currency Dynamically + +The second flow does not require an OTW and can be performed at any time after the package is +published - for example, in an application that creates currencies on demand. The +`new_currency` function takes the `CoinRegistry` directly, and the `Currency` is shared +immediately on `finalize`, with no extra registration step: + +```move file=packages/samples/sources/programmability/balance-and-coin-3.move anchor=doubloon + +``` + +### One Type, Two Shapes + +Both flows denominate the currency with a marker type `T`, but they demand different shapes from +it, matching how each flow proves that the currency is created only once: + +- `new_currency_with_otw` takes a `T` with `drop` - specifically, a + [One-Time Witness](./one-time-witness): a `drop`-only struct with no fields, named after its + module. The proof is the witness _value_ itself: it exists exactly once, is consumed by the + call, and can never be produced again - so neither can the currency. +- `new_currency` takes a _key-only_ `T` - `has key` and nothing else, with the single `id: UID` + field. No instance of `T` is passed, only the type argument, so there is no witness value to + prove anything. Instead, two checks stand in: `new_currency` is subject to the + [internal constraint](./../storage/internal-constraint) - like `sui::event::emit`, it can only be + called with a type defined in the calling module - and the registry aborts if a `Currency` has + already been registered. + +A key-only type cannot have `drop`, so the same type can never be used with both flows. + +## Supply and TreasuryCap + +The [Balance](#balance) section stated that value can only be created by increasing the total +supply of `T`, and can only disappear by decreasing it. The type that does both is `Supply`, +defined in `sui::balance` as the accounting counterpart of `Balance`: + +```move +module sui::balance; + +/// A Supply of T. Used for minting and burning. +public struct Supply has store { + value: u64, +} + +/// Increase supply by `value`, creating a new `Balance`. +public fun increase_supply(self: &mut Supply, value: u64): Balance; + +/// Destroy a `Balance`, decreasing the supply by its value. +public fun decrease_supply(self: &mut Supply, balance: Balance): u64; +``` + +These two functions are the only gate through which value enters and leaves circulation, so every +unit of `Balance` in existence is accounted for by the `Supply` - the number in the supply +always equals the sum of all balances of `T`. + +And just as `Coin` is the object form of a `Balance`, the `TreasuryCap` - the capability +returned by both creation flows - is the object form of a `Supply`: + +```move +module sui::coin; + +/// Capability allowing the bearer to mint and burn +/// coins of type `T`. Transferable +public struct TreasuryCap has key, store { + id: UID, + total_supply: Supply, +} +``` + +Owning the `TreasuryCap` _is_ owning the supply authority. Its `mint` and `burn` functions are thin +wrappers over the supply: `mint` increases it and wraps the new `Balance` into a `Coin`, `burn` +unwraps a `Coin` and decreases the supply by its value. As long as the `TreasuryCap` exists, the +current total can be read from it with `total_supply`. + +```move file=packages/samples/sources/programmability/balance-and-coin-3.move anchor=mint_burn + +``` + +Whoever owns the `TreasuryCap` controls the supply, so where the capability ends up is a design +decision: kept by the publisher for a managed supply, stored inside an application object for +programmatic minting, or given up entirely - as described next. + +> A `Supply` can also exist on its own: `balance::create_supply` turns a witness into a raw +> `Supply` - it is, in fact, the example we used to introduce the +> [Witness pattern](./witness-pattern) - and `treasury_into_supply` extracts the supply from a +> `TreasuryCap`. These are low-level tools: a currency created through the registry should keep +> its `TreasuryCap` intact, since the supply states described next operate on the capability. + +## Supply States + +By default, the supply of a currency is flexible - the `Currency` object records it as `Unknown` +and the `TreasuryCap` can mint and burn freely. The registry supports two irreversible transitions, +both consuming the `TreasuryCap`: + +- `make_supply_fixed` - the supply can never change again. The `Doubloon` example above uses this: + it mints the entire supply upfront and fixes it in the same call; +- `make_supply_burn_only` - no more minting, but anyone can burn coins with the + `coin_registry::burn` and `burn_balance` functions, which take the shared `Currency` object and + permanently decrease the supply. + +Both can be applied either during initialization (on the `CurrencyInitializer`) or later, on the +shared `Currency` object. Consuming the capability is not just ceremony: the transition unpacks +the `TreasuryCap` and moves its `Supply` _into_ the `Currency` object - which is why, from that +point on, the `Currency` itself tracks the total supply, readable onchain with `total_supply`. + +## Managing Metadata + +The name, symbol, description, and icon URL of a currency can be updated after creation with the +`set_name`, `set_symbol`, `set_description`, and `set_icon_url` functions - each requiring a +reference to the `MetadataCap`. Like the `TreasuryCap`, the `MetadataCap` can be deleted with +`delete_metadata_cap`, making the metadata immutable forever - or never claimed in the first +place: `finalize_and_delete_metadata_cap` finalizes the currency with immutable metadata from the +start. Either way, the deletion is recorded in the `Currency`, so the cap can never be claimed +again. + +## Reading a Currency + +A `Currency` is not only for its creator. As a shared object with a derived address, it can be +found for any coin type and passed - by immutable reference - into any function, and the registry +provides getters for every field: `decimals`, `name`, `symbol`, `description`, `icon_url`, the +supply checks `is_supply_fixed` and `is_supply_burn_only`, and the `treasury_cap_id`, +`metadata_cap_id`, and `deny_cap_id` functions to locate the currency's capabilities - or to verify +that they were deleted (the deny cap belongs to _regulated_ currencies, covered +[below](#regulated-currencies)). + +This turns coin metadata into something applications can rely on _on-chain_: a lending protocol +can require the supply of a collateral coin to be fixed, and the function below uses `decimals` to +accept deposits only in whole units of a currency: + +```move file=packages/samples/sources/programmability/balance-and-coin-4.move anchor=currency_reader + +``` + +## Regulated Currencies + +A currency can opt into regulation during initialization by calling `make_regulated` on the +`CurrencyInitializer`. This creates one more capability - `DenyCapV2` - whose owner maintains a +_deny list_: addresses that cannot use `Coin` as transaction inputs. The list itself lives in +the `DenyList` system object at the reserved address `0x403`, managed by the +[sui::deny_list](https://docs.sui.io/references/framework/sui/deny_list) module. Optionally, a +regulated currency can support a _global pause_, stopping all transfers of the coin type. This +feature exists for compliance-heavy assets like stablecoins; most currencies are created without +it. + +## Legacy Coin Metadata + +Before the `CoinRegistry`, currencies were created with `coin::create_currency`, which produced a +standalone `CoinMetadata` object instead of a `Currency`. This function is deprecated, but +plenty of currencies created with it are still live, and some applications still expect +`CoinMetadata` as an argument. The registry provides a bridge in both directions: + +- `migrate_legacy_metadata` registers an existing `CoinMetadata` in the registry, creating a + `Currency` for it; +- `borrow_legacy_metadata` produces a `CoinMetadata` view of a registry-native `Currency`, for + compatibility with older interfaces (returned within the same transaction via a hot potato). + +New code should always use the `coin_registry` flows. + +## Summary + +- `Coin` is the main abstraction for fungible tokens: an object that can be owned, transferred, + and passed into transactions; +- `Balance` is the unit of accounting inside a `Coin`: a non-object value that cannot be copied + or dropped, only moved, split, and joined - and the type applications embed to keep funds; +- `Currency` describes the coin type: metadata, supply state, and regulatory status. It is + created through the `CoinRegistry` system object, either with an OTW in `init` or dynamically - + and can be _read_ onchain by any module; +- `Supply` is the accounting authority: the only gate through which `Balance` value is created + and destroyed. `TreasuryCap` is its object form - it controls minting and burning, and can be + given up to fix the supply; +- `MetadataCap` controls metadata updates, and can be deleted to make them immutable; +- coin values are integers of base units; the `decimals` field of a `Currency` is display-only. + +## Further Reading + +- [Currency Standard](https://docs.sui.io/onchain-finance/fungible-tokens/currency) in Sui + Documentation. +- [sui::coin_registry](https://docs.sui.io/references/framework/sui_sui/coin_registry) module + documentation. +- [sui::coin](https://docs.sui.io/references/framework/sui_sui/coin) module documentation. +- [sui::balance](https://docs.sui.io/references/framework/sui_sui/balance) module documentation. diff --git a/book/programmability/bcs.md b/book/programmability/bcs.md index 2098d8eb6..c547e7b3d 100644 --- a/book/programmability/bcs.md +++ b/book/programmability/bcs.md @@ -1,5 +1,5 @@ --- -description: "BCS (Binary Canonical Serialization) in Move: encode and decode structured data for on-chain storage and cross-platform communication." +description: "BCS (Binary Canonical Serialization) in Move: encode and decode structured data for onchain storage and cross-platform communication." --- # Binary Canonical Serialization @@ -8,6 +8,13 @@ Binary Canonical Serialization (BCS) is a binary encoding format for structured originally designed in Diem, and became the standard serialization format for Move. BCS is simple, efficient, deterministic, and easy to implement in any programming language. +While serialization may sound like an advanced topic, BCS is everywhere on Sui: arguments of a +transaction are BCS-encoded, objects and events are stored as - and read offchain as - BCS bytes, +and messages signed and verified in smart contracts are usually BCS-serialized structs. Most of the +time the encoding is handled for you, but sooner or later an application needs to do it by hand: +decode a signed payload, parse raw bytes passed as a `vector` argument, or produce bytes that +match what an offchain client built. + > The full format specification is available in the > [BCS repository](https://github.com/zefchain/bcs). @@ -20,33 +27,52 @@ deterministic, meaning that the same data will always be serialized to the same > "BCS is not a self-describing format. As such, in order to deserialize a message, one must know > the message type and layout ahead of time" from the [README](https://github.com/zefchain/bcs) -Integers are stored in little-endian format, and variable-length integers are encoded using a -variable-length encoding scheme. Sequences are prefixed with their length as ULEB128, enumerations -are stored as the index of the variant followed by the data, and maps are stored as an ordered -sequence of key-value pairs. +The core rules are: + +- integers are stored in little-endian byte order; +- sequences (like [vectors](./../move-basics/vector)) are prefixed with their length, encoded as + ULEB128 - a compact, variable-length integer encoding; +- [enums](./../move-basics/enum-and-match) are stored as the index of the variant, followed by the + fields of that variant; +- maps are stored as ordered sequences of key-value pairs; +- structs are treated as a sequence of fields: the fields are serialized one after another, in the + order they are defined in the struct, with no names, types, or separators in between. + +To make this concrete, here is how a `User` value is laid out byte by byte: + +```move file=packages/samples/sources/programmability/bcs.move anchor=user_def + +``` -Structs are treated as a sequence of fields, and the fields are serialized in the order they are -defined in the struct. The fields are serialized using the same rules as the top-level data. +| Field | Value | Encoded bytes | +| ------------------------ | ----- | --------------------------------- | +| `age: u8` | `42` | `2A` | +| `is_active: bool` | `true`| `01` | +| `name: String` | `"Bob"` | `03 42 6F 62` (length + bytes) | +| `User` (all of the above)| | `2A 01 03 42 6F 62` | ## Using BCS -The [Sui Framework](./sui-framework) includes the [`sui::bcs`][sui-bcs] module for encoding and -decoding data. Encoding functions are native to the VM, and decoding functions are implemented in -Move. +Two modules implement BCS in Move: the [Standard Library](./../move-basics/standard-library) +provides `std::bcs` with a single native encoding function `to_bytes`, and the +[Sui Framework](./sui-framework) builds on top of it with the [`sui::bcs`][sui-bcs] module, which +re-exports `to_bytes` and adds decoding functions implemented in Move. In Sui code, importing +`sui::bcs` alone is enough for both encoding and decoding. ## Encoding -To encode data, use the `bcs::to_bytes` function, which converts data references into byte vectors. -This function supports encoding any types, including structs. +To encode data, use the `bcs::to_bytes` function, which converts a data reference into a byte +vector. This function supports encoding any type, including structs and enums. ```move module std::bcs; -public native fun to_bytes(t: &T): vector; +/// Return the binary representation of `v` in BCS (Binary Canonical +/// Serialization) format. +public native fun to_bytes(v: &MoveValue): vector; ``` -The following example shows how to encode a struct using BCS. The `to_bytes` function can take any -value and encode it as a vector of bytes. +The following example shows the encoding of primitive values: ```move file=packages/samples/sources/programmability/bcs.move anchor=encode @@ -54,7 +80,10 @@ value and encode it as a vector of bytes. ### Encoding a Struct -Structs encode similarly to simple types. Here is how to encode a struct using BCS: +A struct is encoded as nothing more than its fields, one after another. The example below encodes +the `User` value from the [Format](#format) section, checks the exact bytes from the table, and +then demonstrates the "sequence of fields" rule directly - concatenating the individually encoded +fields yields the same result: ```move file=packages/samples/sources/programmability/bcs.move anchor=encode_struct @@ -62,15 +91,27 @@ Structs encode similarly to simple types. Here is how to encode a struct using B ## Decoding -Because BCS is not a self-describing format, decoding requires prior knowledge of the data type. The -[`sui::bcs`][sui-bcs] module provides various functions to assist with this process. +Because BCS is not a self-describing format, decoding requires prior knowledge of the data type. +This is not just a formality - the same bytes are perfectly valid under different readings, and the +decoder has no way to detect a mismatch. The 6 bytes of the encoded `User` above can just as well be +read as a `u16` followed by a `vector`: + +```move file=packages/samples/sources/programmability/bcs.move anchor=not_self_describing + +``` + +The [`sui::bcs`][sui-bcs] module provides functions to assist with decoding: `peel_bool`, +`peel_u8` through `peel_u256`, and `peel_address` for primitive values, a `peel_vec_*` family and a +`peel_option_*` family for common containers, and macros for everything else. If the decoder runs +out of bytes - or the bytes do not form a valid value, such as a boolean byte other than `0` or `1` - +the call aborts. ### Wrapper API -BCS is implemented as a wrapper in Move. The decoder takes the bytes by value, and then allows the -caller to _peel off_ the data by calling different decoding functions, prefixed with `peel_*`. The -data is extracted from the bytes, and the remaining bytes are kept in the wrapper until the -`into_remainder_bytes` function is called. +The decoder is a wrapper around the bytes: the `bcs::new` function takes the bytes by value, and +then the caller _peels off_ values one by one, front to back, by calling the `peel_*` functions. +Whatever has not been decoded stays inside the wrapper, and can be taken back out with the +`into_remainder_bytes` function. ```move file=packages/samples/sources/programmability/bcs.move anchor=decode @@ -86,62 +127,73 @@ makes code a little bit more readable and helps to avoid unnecessary copying of ### Decoding Vectors While most of the primitive types have a dedicated decoding function, vectors need special handling, -which depends on the type of the elements. For vectors, first you need to decode the length of the -vector, and then decode each element in a loop. +which depends on the type of the elements. The underlying structure is always the same: first decode +the length of the vector, then decode each element in a loop. ```move file=packages/samples/sources/programmability/bcs.move anchor=decode_vector ``` -This functionality is provided by the library as a macro `peel_vec!`. It calls the inner expression -as many times as the vector length and aggregates the result into a single vector. +For everyday use, the library offers the `peel_vec!` macro, which performs the loop internally and +calls the given function once per element, as well as ready-made `peel_vec_*` functions for vectors +of primitive types: + +```move file=packages/samples/sources/programmability/bcs.move anchor=decode_vector_macro -```move -let u64_vec = bcs.peel_vec!(|bcs| bcs.peel_u64()); -let address_vec = bcs.peel_vec!(|bcs| bcs.peel_address()); - -// Caution: this is only possible if `MyStruct` is defined in the current module! -let my_struct = bcs.peel_vec!(|bcs| MyStruct { - user_addr: bcs.peel_address(), - age: bcs.peel_u8(), -}); ``` ### Decoding Option - - -[Option](./../move-basics/option) in Move is represented as a vector of either 0 or 1 element. To -read an option, you would treat it like a vector and check its length (first byte - either 1 or 0). +[Option](./../move-basics/option) is encoded as a single byte - `0` for _none_ and `1` for _some_ - +followed by the value, if present. The `peel_option!` macro reads the byte and evaluates the given +function only if the value is there; primitive types also have ready-made `peel_option_*` functions. ```move file=packages/samples/sources/programmability/bcs.move anchor=decode_option ``` -Like with [vector](#decoding-vectors), there is a wrapper macro `peel_option!` which checks the -variant index and evaluates the expression if the underlying value is _some_. +### Decoding Structs + +There is no way to automatically decode bytes into a Move struct - the [struct](../move-basics/struct) +can only be packed by its module, and the bytes carry no information about what they represent. To +parse bytes into a struct, peel each field and pack the type. The example below makes the full round +trip: it encodes a `User` value, decodes it back from the bytes, and checks that the result is +identical to the original. + +```move file=packages/samples/sources/programmability/bcs.move anchor=round_trip -```move -let u8_opt = bcs.peel_option!(|bcs| bcs.peel_u8()); -let bool_opt = bcs.peel_option!(|bcs| bcs.peel_bool()); ``` -### Decoding Structs +> The bytes contain no field names and no type tags, so the only thing that makes decoding correct +> is peeling the exact same types in the exact same order as they were encoded. Getting the order +> wrong does not necessarily abort - it may silently produce wrong values, as the +> [example above](#decoding) shows. + +### Decoding Enums -Structs are decoded field by field, and there is no way to automatically decode bytes into a Move -struct. To parse bytes into a struct, you need to decode each field and instantiate the type. +An [enum](./../move-basics/enum-and-match) value is encoded as the index of its variant, followed by +the fields of that variant. Decoding mirrors this: the `peel_enum_tag` function reads the variant +index, and a `match` expression on it decodes the corresponding fields: -```move file=packages/samples/sources/programmability/bcs.move anchor=decode_struct +```move file=packages/samples/sources/programmability/bcs.move anchor=decode_enum ``` ## Summary -Binary Canonical Serialization is an efficient binary format for structured data, ensuring -consistent serialization across platforms. The Sui Framework provides comprehensive tools for -working with BCS, allowing extensive functionality through built-in functions. +- BCS is the standard binary serialization format of Move: deterministic - the same value always + produces the same bytes. +- The format is not self-describing: the bytes carry no names or types, and the reader must know the + layout ahead of time. +- Structs and enums encode as their fields in declaration order; decoding must peel the same types + in the same order. +- Encoding is done with `bcs::to_bytes`; decoding with the `bcs::new` wrapper and the `peel_*` + family of functions and macros, which abort on malformed or truncated input. + +## Further Reading + +- [BCS specification](https://github.com/zefchain/bcs) - the full format description. +- [std::bcs](https://docs.sui.io/references/framework/std/bcs) and [sui::bcs][sui-bcs] module + documentation. -[sui-bcs]: https://docs.sui.io/references/framework/sui_sui/bcs +[sui-bcs]: https://docs.sui.io/references/framework/sui/bcs diff --git a/book/programmability/capability.md b/book/programmability/capability.md index 472aa09ab..2a6dc4c1d 100644 --- a/book/programmability/capability.md +++ b/book/programmability/capability.md @@ -33,21 +33,57 @@ application can have a setup phase where the admin account prepares the state of ``` +Notice that this `AdminCap` has only the `key` ability, unlike the one in the first example, which +also had `store`. The [abilities](./../move-basics/abilities-introduction) of a capability define +how it can move between accounts: with `key` and `store`, the capability can be freely transferred +with public transfer functions and stored inside other objects; with only `key`, it can be +transferred only by functions defined in its module, so the module can restrict - or completely +forbid - passing the capability on. As described in the +[Storage Functions](./../storage/storage-functions) section, this is the difference between +internal and public transfer. + +## Capabilities in the Sui Framework + +The capability pattern is not just a convention - the [Sui Framework](./sui-framework) itself is +built around it. Knowing the standard capabilities helps recognize the pattern in real code; here +are the ones you are most likely to encounter: + +- `sui::coin::TreasuryCap` - created together with a new currency, grants the right to mint and + burn coins of type `T`. Owning the `TreasuryCap` is owning the supply of the currency; we explore + it in the [Balance and Coin](./balance-and-coin) chapter; +- `sui::package::UpgradeCap` - created when a package is published, authorizes future upgrades of + the package. The owner of the `UpgradeCap` can also restrict future upgrades, or disable them + completely by making the capability immutable; +- `sui::kiosk::KioskOwnerCap` - grants the right to `place`, `take`, and `list` items in a + [Kiosk](https://docs.sui.io/standards/kiosk) - the trading primitive of Sui. While the `Kiosk` + object itself is shared and accessible to everyone, the "owner" operations on it require the + capability; +- `sui::transfer_policy::TransferPolicyCap` - grants the right to manage a `TransferPolicy`: + add and remove trading rules, and withdraw the collected fees. + +Two of these capabilities take a type parameter - a technique worth noting. By adding a +[generic](./../move-basics/generics) to the capability, the authority it grants is scoped to a +single type: a `TreasuryCap` controls the supply of `GOLD` and gives no rights over the +`SILVER` currency. + +The framework also features a more general form of authority - the `Publisher` object, which proves +authority over all types of a package. It is covered separately in the +[Publisher Authority](./publisher) chapter. + ## Address Check vs Capability -Utilizing objects as capabilities is a relatively new concept in blockchain programming. And in -other smart-contract languages, authorization is often performed by checking the address of the -sender. This pattern is still viable on Sui, however, overall recommendation is to use capabilities -for better security, discoverability, and code organization. +Utilizing objects as capabilities is a relatively new concept in blockchain programming. In other +smart-contract languages, authorization is often performed by checking the address of the sender. +This pattern is still viable on Sui, however, the overall recommendation is to use capabilities for +better security, discoverability, and code organization. -Let's look at how the `new` function that creates a user would look like if it was using the address -check: +Let's look at how the `new` function that creates a user would look if it used the address check: ```move file=packages/samples/sources/programmability/capability-3.move anchor=with_address ``` -And now, let's see how the same function would look like with the capability: +And now, let's see how the same function looks with the capability: ```move file=packages/samples/sources/programmability/capability-4.move anchor=with_capability @@ -66,8 +102,15 @@ Using capabilities has several advantages over the address check: their account (via a Wallet or Explorer), and know that they have the admin rights. This is less transparent with the address check. -However, the address approach has its own advantages. For example, if an address is multisig, and -transaction building gets more complex, it might be easier to check the address. Also, if there's a -central object of the application that is used in every function, it can store the admin address, -and this would simplify migration. The central object approach is also valuable for revocable -capabilities, where the admin can revoke the capability from the user. +However, the address approach has advantages of its own. One case is a _multisig_ address - an +address controlled by multiple parties, where a transaction is only valid if enough of them sign +it. If the admin rights of an application belong to a multisig address, checking the sender may be +simpler than building a transaction that presents a capability object owned by that address. + +Another case is an application with a central object - a config or a registry - that is already +passed into every function. Such an object can store the admin address as a regular field, and +checking it requires no extra inputs. The address is plain data, so it can be changed at runtime, +without a package upgrade. The same idea enables _revocation_: an owned capability, once +transferred, cannot be taken back from its owner, but an entry in a central registry - an address +or an ID of a previously issued capability - can be removed by the admin at any moment, instantly +revoking access. diff --git a/book/programmability/collections.md b/book/programmability/collections.md index 844362b3f..522eea280 100644 --- a/book/programmability/collections.md +++ b/book/programmability/collections.md @@ -1,62 +1,85 @@ --- -description: "Collection types in Sui Move: VecSet, VecMap, and other vector-based data structures from the Sui Framework." +description: + 'Vector-based collections in the Sui Framework: VecSet and VecMap, their operations and + constraints, and when to reach for dynamic collections instead.' --- # Collections -Collection types are a fundamental part of any programming language. They are used to store a -collection of data, such as a list of items. The `vector` type has already been covered in the -[vector section](./../move-basics/vector), and in this chapter we will cover the vector-based -collection types offered by the [Sui Framework](./sui-framework). +Storing groups of values is one of the most common needs in a program. The +[`vector`](./../move-basics/vector) type, covered in the Move Basics chapter, is the base building +block for it, and the [Sui Framework](./sui-framework) extends it with two collection types that +add structure on top: `VecSet`, which keeps its elements unique, and `VecMap`, which associates +keys with values. In this section we introduce all three in their most common role - as fields of +an object - and show the operations and constraints of each. ## Vector -While we have previously covered the `vector` type in the [vector section](./../move-basics/vector), -it is worth going over it again in a new context. This time we will cover the usage of the `vector` -type in objects and how it can be used in an application. +While the [vector section](./../move-basics/vector) presents the `vector` type as a standalone +value, in a real application it usually lives inside an object. A store that owns a list of books +is a vector in a field: ```move file=packages/samples/sources/programmability/collections.move anchor=vector ``` +Everything from the vector section applies here unchanged; the collection types below follow the +same pattern - plain struct values that can be placed in a field, passed around, and, unlike +[dynamic fields](./dynamic-fields) introduced later in this chapter, fully described by the type of +the object that holds them. + ## VecSet -`VecSet` is a collection type that stores a set of unique items. It is similar to a `vector`, but it -does not allow duplicate items. This property makes it useful for storing a collection of unique -items, such as a list of IDs or addresses. +`VecSet` is a collection that stores _unique_ items. Inserting a value that is already present +aborts, so the set is a natural fit for collections that must not contain duplicates, such as a +list of IDs or addresses. ```move file=packages/samples/sources/programmability/collections-2.move anchor=vec_set ``` -VecSet will fail on attempt to insert an item that already exists in the set. +The `contains` function answers membership questions, and the contents can be read back either by +reference, with `keys`, or taken out as a plain `vector` with `into_keys` - for example, to iterate +over them with the [vector macros](./../move-basics/vector#vector-macros). -## VecMap +> The element type of a `VecSet` must have the [`copy`](./../move-basics/copy-ability) and +> [`drop`](./../move-basics/drop-ability) abilities. This is true for primitive types and simple +> data structs, but rules out storing assets in a set. -`VecMap` is a collection type that stores a map of key-value pairs. It is similar to a `VecSet`, but -it allows you to associate a value with each item in the set. This makes it useful for storing a -collection of key-value pairs, such as a list of addresses and their balances, or a list of user IDs -and their associated data. +## VecMap -Keys in a `VecMap` are unique, and each key can only be associated with a single value. If you try -to insert a key-value pair with a key that already exists in the map, the old value will be replaced -with the new value. +`VecMap` is a collection of key-value pairs, where each key is unique and maps to a single value. +Reading a value back is the everyday operation of a map, and there are two ways to do it: the index +syntax `map[&key]` borrows a value and aborts if the key is missing, while `try_get` returns an +[`Option`](./../move-basics/option) and never aborts. ```move file=packages/samples/sources/programmability/collections-3.move anchor=vec_map ``` +Like `VecSet`, a `VecMap` aborts on an attempt to `insert` a key that is already present - it does +_not_ silently overwrite the old value. Replacing a value requires going through a mutable +reference, as the example above shows, or removing the old entry first. Keys of a `VecMap` must +have the [`copy`](./../move-basics/copy-ability) ability, while the value can be any type. + ## Limitations -Standard collection types are a great way to store typed data with guaranteed safety and -consistency. However, they are limited by the type of data they can store - the type system won't -allow you to store a wrong type in a collection; and they're limited in size - by the object size -limit. They will work for relatively small-sized sets and lists, but for larger collections you may -need to use a different approach. +Vector-based collections are strictly typed: a `VecSet
` holds addresses and nothing else, +which is exactly what you want most of the time, but makes them unsuitable for heterogeneous data. +They are also plain values stored inside the object, so they count toward the object size limit of +256KB, described in the [Building Against Limits](./../guides/building-against-limits) guide. -Another limitations on collection types is inability to compare them. Because the order of insertion -is not guaranteed, an attempt to compare a `VecSet` to another `VecSet` may not yield the expected -results. +In practice, a different limit matters sooner: every operation - `insert`, `contains`, `get` - +scans the underlying vector element by element, so the cost of each access grows with the size of +the collection. Vector-based collections shine when the number of elements is small and bounded - +tens or hundreds of entries. For large or unbounded collections, the Sui Framework provides +`Table`, `Bag`, and other object-backed types, which we cover in the +[Dynamic Collections](./dynamic-collections) section later in this chapter. + +Lastly, vector-based collections do not support equality comparison the way one might expect. +`VecSet` and `VecMap` keep their contents in insertion order, and the `==` operator compares the +underlying vectors element by element. As a result, two sets that contain the same elements, but +received them in a different order, are _not_ equal. > This behavior is caught by the linter and will emit a warning: _Comparing collections of type > 'sui::vec_set::VecSet' may yield unexpected result_ @@ -65,19 +88,31 @@ results. ``` -In the example above, the comparison will fail because the order of insertion is not guaranteed, and -the two `VecSet` instances may have different orders of elements. And the comparison will fail even -if the two `VecSet` instances contain the same elements. +In the example above, both sets contain the same elements - `1` and `2` - but they were inserted in +a different order. Since the comparison is order-sensitive, `set1 == set2` evaluates to `false`, and +the assertion aborts. Do not rely on `==` to compare vector-based collections, unless you can +guarantee that the elements were inserted in the same order. ## Summary -- Vector is a native type that allows storing a list of items. -- VecSet is built on top of vector and allows storing sets of unique items. -- VecMap is used to store key-value pairs in a map-like structure. -- Vector-based collections are strictly typed and limited by the object size limit and are best - suited for small-sized sets and lists. +- Vector is a native type that allows storing a list of items; inside an object it appears as a + regular field. +- VecSet is built on top of vector and stores unique items; inserting a duplicate aborts. +- VecMap stores key-value pairs with unique keys; inserting an existing key aborts, and values are + read with the index syntax or `try_get`. +- Vector-based collections are strictly typed, scan their contents linearly on every operation, and + are best suited for small, bounded sets and lists; larger collections call for + [dynamic collections](./dynamic-collections). ## Next Steps In the next section we will cover the [Wrapper Type Pattern](./wrapper-type-pattern) - a design pattern often used with collection types to extend or restrict their behavior. + +## Further Reading + +- [sui::vec_set][vec-set-framework] module documentation. +- [sui::vec_map][vec-map-framework] module documentation. + +[vec-set-framework]: https://docs.sui.io/references/framework/sui/vec_set +[vec-map-framework]: https://docs.sui.io/references/framework/sui/vec_map diff --git a/book/programmability/display.md b/book/programmability/display.md index 30e8eb49b..8ed6196c1 100644 --- a/book/programmability/display.md +++ b/book/programmability/display.md @@ -1,13 +1,13 @@ --- -description: "Object Display in Sui: define rich metadata templates for your objects using the Display standard for wallets and explorers." +description: "Object Display in Sui: define metadata templates for your objects with the Display Registry, and migrate Display from V1 to V2." --- # Object Display Objects on Sui are explicit in their structure and behavior and can be displayed in an understandable way. However, to support richer metadata for clients, there's a standard and -efficient way of "describing" them to the client - the `Display` object defined in the -[Sui Framework](./sui-framework). +efficient way of "describing" them to the client - the `Display` object, registered in the system +_Display Registry_ defined in the [Sui Framework](./sui-framework). ## Background @@ -28,30 +28,79 @@ does not have interfaces, it is not possible to know if an object has a specific ## Object Display To address these issues, Sui introduces a standard way of describing an object for display. Instead -of defining fields in the object struct, the display metadata is stored in a separate object, which -is associated with the type. This way, the display metadata is not duplicated, and it is easy to -extend and maintain. +of defining fields in the object struct, the display metadata is stored in a separate object - +`Display` - which is associated with the type `T`. This way, the display metadata is not +duplicated, and it is easy to extend and maintain. Another important feature of Sui Display is the ability to define templates and use object fields in -those templates. Not only it allows for a more flexible display, but it also frees the developer +those templates. Not only does it allow for a more flexible display, but it also frees the developer from the need to define the same fields with the same names and types in every object. > The Object Display is natively supported by the -> [Sui Full Node](https://docs.sui.io/guides/operator/sui-full-node), and the client can fetch the -> display metadata for any object if the object type has a Display associated with it. +> [Sui Full Node](https://docs.sui.io/operators/full-node/sui-full-node), and the client can fetch +> the display metadata for any object if the object type has a Display associated with it. -```move file=packages/samples/sources/programmability/display.move anchor=hero +## Display Registry + +For every type `T` there is exactly one `Display`, and it lives at a predictable address. Both +properties come from the _Display Registry_ - a system shared object located at the reserved address +`0xd` (see [Reserved Addresses](./../appendix/reserved-addresses)). When a display is created, its +object ID is +[derived](https://docs.sui.io/references/framework/sui_sui/derived_object) from the registry's `UID` +and the type `T`. As a result, anyone - including RPCs and other clients - can compute the ID of +`Display` offline and fetch it directly, without scanning events or querying historical data. + +```move +module sui::display_registry; + +/// The root of display, to enable derivation of addresses. +/// The address is system-generated at `0xd`. +public struct DisplayRegistry has key { id: UID } + +/// Holds the display values for the type `T`. +public struct Display has key { + id: UID, + /// All the (key,value) entries for a given display object. + fields: VecMap, + /// ID of the `DisplayCap` managing this display. `None` for + /// migrated V1 displays until the capability is claimed. + cap_id: Option, +} +/// The capability object that is used to manage the display. +public struct DisplayCap has key, store { id: UID } ``` -## Creator Privilege +The `Display` object itself is _shared_, and the authority over it is represented by a separate +owned object - the `DisplayCap` [capability](./capability). The holder of the capability can +`set`, `unset`, or `clear` the display fields at any time, and the changes apply globally without +the need to update every object. The capability can be transferred to another account, or built into +an application with custom metadata-management functionality. + +## Creating a Display + +A new `Display` is created with one of two functions, both taking a mutable reference to the +`DisplayRegistry` and returning the `Display` together with its `DisplayCap`: + +- `display_registry::new` - takes an [Internal Permit](./../move-basics/internal-permit), and + hence can only be called from the module that defines `T`; +- `display_registry::new_with_publisher` - takes the [Publisher](./publisher) object, for cases + when the display is created outside of the defining module. + +Because the registry is a shared object, it cannot be accessed in the +[module initializer](./module-initializer) - the display is created by a separate, one-time call +right after the package is published: + +```move file=packages/samples/sources/programmability/display.move anchor=hero + +``` -While the objects can be owned by accounts and may be a subject to -[True Ownership](./../object/ownership#account-owner-or-single-owner), the Display can be owned by -the creator of the object. This way, the creator can update the display metadata and apply the -changes globally without the need to update every object. The creator can also transfer Display to -another account or even build an application around the object with custom functionality to manage -the metadata. +The `set` calls define the template fields, and `share` finalizes the creation by sharing the +`Display` object; the `DisplayCap` is then transferred to the publisher, who keeps it to update the +fields later. Note that the function is defined as `entry` rather than `public`: a one-time setup +function is best kept out of the package's public API, so that a later upgrade can remove it - +[upgrade compatibility rules](https://docs.sui.io/develop/publish-upgrade-packages/upgrade) +freeze `public` function signatures, but not `entry` ones. ## Standard Fields @@ -67,42 +116,19 @@ The fields that are supported most widely are: - `project_url` - A link to a website associated with the object or creator. - `creator` - A string that indicates the object creator. -> Please, refer to the [Sui Documentation](https://docs.sui.io/guides/developer/objects/display) for the most +> Please refer to the [Sui Documentation](https://docs.sui.io/develop/objects/display) for the most > up-to-date list of supported fields. While there's a standard set of fields, the Display object does not enforce them. The developer can define any fields they need, and the client can use them as they see fit. Some applications may -require additional fields, and omit other, and the Display is flexible enough to support them. - -## Working with Display - -The `Display` object is defined in the `sui::display` module. It is a generic struct that takes a -phantom type as a parameter. The phantom type is used to associate the `Display` object with the -type it describes. The `fields` of the `Display` object are a `VecMap` of key-value pairs, where the -key is the field name and the value is the field value. The `version` field is used to version the -display metadata, and is updated on the `update_display` call. - -```move -module sui::display; - -public struct Display has key, store { - id: UID, - /// Contains fields for display. Currently supported - /// fields are: name, link, image and description. - fields: VecMap, - /// Version that can only be updated manually by the Publisher. - version: u16 -} -``` - -The [Publisher](./publisher) object is required to a new Display, since it serves as the proof of -ownership of type. +require additional fields and omit others, and the Display is flexible enough to support them. ## Template Syntax -Currently, Display supports simple string interpolation and can use struct fields (and paths) in its -templates. The syntax is trivial - `{path}` is replaced with the value of the field at the path. The -path is a dot-separated list of field names, starting from the root object in case of nested fields. +Every value in a Display is a _format string_ - a mix of literal text and expressions delimited by +`{` and `}`. The simplest expression is a field path: `{path}` is replaced with the value of the +field at that path, where the path is a dot-separated list of field names starting from the object +being displayed. To output a literal brace, double it - `{{` becomes `{`. ```move file=packages/samples/sources/programmability/display.move anchor=nested @@ -118,12 +144,110 @@ The Display for the type `LittlePony` above could be defined as follows: } ``` -## Multiple Display Objects +A field path is only the most basic expression. The full form of an expression has three parts - a +_chain_ that navigates into the data, an optional list of _fallbacks_ separated by `|`, and an +optional _transform_ prefixed with `:` that controls how the value is rendered: + +```text +{ chain | fallback | ... : transform } +``` + +The following sections walk through the parts of this syntax that come up most often. For the +complete grammar - literals, struct and enum values, derived-object access, and the exhaustive +transform list - see the +[Object Display Syntax](https://docs.sui.io/references/object-display-syntax) reference. + +### Vector and Map Indexing + +A chain can index into a `vector` or a `VecMap` with square brackets. Numeric indices always carry a +type suffix - `0u64`, not `0` - and another field's value can be used as the index: + +```text +{items[0u64]} first element of the `items` vector +{items[idx]} use the `idx` field's value as the index +{scores[6u32]} look up the key `6u32` in a VecMap, returns its value +``` + +### Dynamic Field Access + +Templates can reach beyond the object's own fields and load +[dynamic fields](./dynamic-fields) from storage. The `->` operator loads a dynamic field, `=>` loads +a [dynamic object field](./dynamic-object-fields), and the key goes in brackets: + +```text +{parent->['color']} dynamic field with the string key 'color' +{parent->['color'].x} read field `x` on the loaded value +{parent=>['hat']} dynamic object field (the value is a full object) +``` + +Because each load reads from storage, they are budgeted: a template may perform at most 8 object +loads by default, with `->` costing one and `=>` costing two. + +### Transforms + +By default a value is rendered as a human-readable string. A transform after `:` changes that - +useful for values that are not plain text, such as byte vectors or timestamps: + +| Transform | Effect | +| ----------------- | ---------------------------------------------------------------- | +| `str` _(default)_ | Human-readable string; UTF-8 for `String` and `vector`. | +| `hex` | Lowercase, zero-padded hexadecimal. | +| `base64` | Base64-encoded bytes; accepts `url` and `nopad` modifiers. | +| `bcs` | BCS-serialized value, then Base64-encoded - for aggregate types. | +| `json` | Structured JSON value; only when it is the whole format string. | +| `timestamp` (`ts`)| A numeric value read as Unix milliseconds, formatted ISO 8601. | +| `url` | Like `str`, but percent-encodes reserved URL characters. | + +```text +{amount:hex} render `amount` as hex +{created_at:ts} "2023-04-12T17:00:00Z" +{metadata:json} emit the whole struct as JSON +``` + +### Fallbacks + +If a chain evaluates to null - a missing field, an out-of-bounds index, or a `None` +[Option](./../move-basics/option) - the next chain after `|` is tried. A string literal in single +quotes makes a convenient default: + +```text +{display_name | name | 'Anonymous'} +``` + +If every alternative is null, the whole format string evaluates to null and the field is omitted from +the result. + +## Migrating from V1 to V2 + +The registry-backed Display described on this page is the second version of the standard - _Display +V2_. The original one - V1, implemented in the `sui::display` module - predates the registry: V1 +`Display` objects were owned rather than shared, could only be created with the `Publisher` +object, and were discovered through events. Any number of V1 displays could exist for the same type, +and full nodes used the most recently updated one. V2 replaces event-based discovery with derivation +from the registry, and reduces "any number of displays" to exactly one per type. + +Existing V1 displays were migrated to V2 automatically by a system migration: for every type with a +V1 display, there is already a shared `Display` with the same fields and with `cap_id` set to +`none`. To manage such a display, the creator claims its `DisplayCap` in one of two ways: + +- `claim` - consumes the legacy V1 `Display` object as the proof of authority over the type, + destroying it in the process; +- `claim_with_publisher` - uses the [Publisher](./publisher) object instead; the leftover V1 object + can then be destroyed with `delete_legacy`. + +```move file=packages/samples/sources/programmability/display.move anchor=migrate + +``` -There's no restriction to how many `Display` objects can be created for a specific `T`. However, -the most recently updated `Display` will be used by the full node. +For a V1 display that was created after the system migration took place, the +`display_registry::migrate_v1_to_v2` function performs the migration directly: it creates the V2 +`Display`, copies the fields from the legacy object, destroys it, and returns the new display +together with its capability. ## Further Reading -- [Sui Object Display](https://docs.sui.io/guides/developer/objects/display) is Sui Documentation +- [Object Display](https://docs.sui.io/develop/objects/display) in the Sui Documentation +- [Object Display Syntax](https://docs.sui.io/references/object-display-syntax) - the full template + language reference - [Publisher](./publisher) - the representation of the creator +- [Internal Permit](./../move-basics/internal-permit) - the authorization used to create a display diff --git a/book/programmability/dynamic-collections.md b/book/programmability/dynamic-collections.md index 052920120..02b631fa1 100644 --- a/book/programmability/dynamic-collections.md +++ b/book/programmability/dynamic-collections.md @@ -4,7 +4,7 @@ description: "Dynamic collections in Sui: Bag, Table, ObjectBag, ObjectTable, an # Dynamic Collections -[Sui Framework](./sui-framework) offers a variety of collection types that build on the +The [Sui Framework](./sui-framework) offers a variety of collection types that build on the [dynamic fields](./dynamic-fields) and [dynamic object fields](./dynamic-object-fields) concepts. These collections are designed to be a safer and more understandable way to store and manage dynamic fields and objects. @@ -17,15 +17,37 @@ offer. ## Common Concepts -All of the collection types share the same set of methods, which are: +All five collections follow the same shape: a struct with the `key` and `store` abilities, holding +its own `UID` and a `size` counter. The entries are attached to that `UID` as dynamic fields. This +is why creating a collection requires a mutable reference to the +[transaction context](./transaction-context) - a fresh `UID` has to be derived from it - and why a +collection is typically stored as a field of another object, as the examples below show. -- `add` - adds a field to the collection -- `remove` - removes a field from the collection +All of the collection types share the same set of core methods: + +- `new` - creates a new, empty collection +- `add` - adds a field to the collection ([LinkedTable](#linkedtable) uses `push_front` and + `push_back` instead) +- `remove` - removes a field from the collection and returns the value - `borrow` - borrows a field from the collection - `borrow_mut` - borrows a mutable reference to a field from the collection - `contains` - checks if a field exists in the collection - `length` - returns the number of fields in the collection - `is_empty` - checks if the `length` is 0 +- `destroy_empty` - destroys the collection, aborting if it still contains fields + +The last method is what makes collections safer than raw dynamic fields: because collections track +their size, they cannot be destroyed while non-empty, which rules out +[orphaned fields](./dynamic-fields#orphaned-dynamic-fields). The flip side of this protection is +that a collection whose values cannot be dropped has to be emptied entry by entry before it can be +destroyed - and since the number of dynamic fields accessed per transaction is +[limited](./../guides/building-against-limits), dismantling a large collection may take more than +one transaction. + +Another property, inherited from dynamic fields, is that the keys are not discoverable onchain: +to access an entry, the code has to know its key. Offchain tooling can still list all entries, as +they are stored as dynamic fields on the collection's `UID`. The only collection that can be +iterated onchain is [LinkedTable](#linkedtable). All collection types support index syntax for `borrow` and `borrow_mut` methods. If you see square brackets in the examples, they are translated into `borrow` and `borrow_mut` calls. @@ -44,9 +66,11 @@ collection types. ## Bag -Bag, as the name suggests, acts as a "bag" of heterogeneous values. It is a simple, non-generic type -that can store any data. Bag will never allow orphaned fields, as it tracks the number of fields and -can't be destroyed if it's not empty. +Bag, as the name suggests, acts as a "bag" of heterogeneous values. It is a simple, non-generic +type built on [dynamic fields](./dynamic-fields), and it can store any data. Bag is the right +choice when a single container has to hold values of different types - for example, a game +character carrying items of various kinds, or a user profile storing unrelated settings side by +side. ```move module sui::bag; @@ -61,7 +85,7 @@ public struct Bag has key, store { _See [full documentation for sui::bag][bag-framework] module._ -Due to Bag storing any types, the extra methods it offers is: +Since Bag stores values of any type, it offers one extra method: - `contains_with_type` - checks if a field exists with a specific type @@ -80,14 +104,22 @@ Using the Bag: ## ObjectBag Defined in the `sui::object_bag` module. Identical to [Bag](#bag), but uses -[dynamic object fields](./dynamic-object-fields) internally. Can only store objects as values. +[dynamic object fields](./dynamic-object-fields) internally. Can only store objects as values, and +in exchange keeps them discoverable by their IDs in offchain tooling. Use it for the same +heterogeneous scenarios as Bag when the stored values are assets that should remain visible in +wallets and explorers - such as an inventory of NFTs of different types. + +Like dynamic object fields, ObjectBag offers the `value_id` function, which returns the `ID` of a +stored object without specifying its type. _See [full documentation for sui::object_bag][object-bag-framework] module._ ## Table -Table is a typed dynamic collection that has a fixed type for keys and values. It is defined in the -`sui::table` module. +Table is a typed dynamic collection that has a fixed type for keys and values. It is built on +[dynamic fields](./dynamic-fields) and defined in the `sui::table` module. Table is the go-to +collection for large uniform registries: user records, balances, or configuration entries keyed by +an address or a name - like the `UserRegistry` in the example below. ```move module sui::table; @@ -102,6 +134,11 @@ public struct Table has key, s _See [full documentation for sui::table][table-framework] module._ +Since the type of the values is fixed, Table offers one extra method: + +- `drop` - destroys the table even if it is not empty; only available when the value type has the + [drop](./../move-basics/drop-ability) ability + Used as a struct field: ```move file=packages/samples/sources/programmability/dynamic-collections.move anchor=table_struct @@ -117,14 +154,36 @@ Using the Table: ## ObjectTable Defined in the `sui::object_table` module. Identical to [Table](#table), but uses -[dynamic object fields](./dynamic-object-fields) internally. Can only store objects as values. +[dynamic object fields](./dynamic-object-fields) internally. Can only store objects as values, and +in exchange keeps them discoverable by their IDs in offchain tooling. Use it when a registry +stores whole objects of the same type - for example, user profile objects keyed by the owner's +address - and each of them should stay individually discoverable. + +Like dynamic object fields, ObjectTable offers the `value_id` function, which returns the `ID` of +a stored object without specifying its type. _See [full documentation for sui::object_table][object-table-framework] module._ +Storing objects requires the value type to have the `key` and `store` abilities: + +```move file=packages/samples/sources/programmability/dynamic-collections.move anchor=object_table_struct + +``` + +Using the ObjectTable: + +```move file=packages/samples/sources/programmability/dynamic-collections.move anchor=object_table_usage + +``` + ## LinkedTable -It is defined in the `sui::linked_table` module, similar to [Table](#table) but the values are linked together, -allowing for ordered insertion and removal. +Defined in the `sui::linked_table` module. Built on [dynamic fields](./dynamic-fields), similar to +[Table](#table), but the entries are linked together, allowing insertion at either end, ordered +removal, and onchain iteration. This makes it the choice +for anything that must be enumerated or processed in order onchain: queues and waitlists, +leaderboards, or registries whose entries have to be listed - like the `AdminRegistry` in the +example below. ```move module sui::linked_table; @@ -143,14 +202,18 @@ public struct LinkedTable has key, sto _See [full documentation for sui::linked_table][linked-table-framework] module._ -Since the values stored in LinkedTable are linked together, it has unique methods for adding and deleting. +Since the entries in LinkedTable are linked together, adding an entry requires stating where it +goes, so instead of `add` it has: - `push_front` - inserts a key-value pair at the front of the table - `push_back` - inserts a key-value pair at the back of the table -- `remove` - removes a key-value pair by key and returns the value - `pop_front` - removes the front of the table, returns the key and value - `pop_back` - removes the back of the table, returns the key and value +Additionally, the `front`, `back`, `prev`, and `next` methods return the keys of neighboring +entries, making it possible to iterate over the table onchain. Like [Table](#table), LinkedTable +offers the `drop` method for value types with the [drop](./../move-basics/drop-ability) ability. + Used as a struct field: ```move file=packages/samples/sources/programmability/dynamic-collections.move anchor=linked_table_struct @@ -163,13 +226,50 @@ Using the LinkedTable: ``` +## Pricing + +Collections inherit the pricing of the primitives they are built on. Creating a collection adds an +object with a `UID` to storage; each entry is priced as a +[dynamic field](./dynamic-fields#dynamic-fields-vs-fields), or - in the Object-variants - as a +[dynamic object field](./dynamic-object-fields#pricing-differences), with its higher, two-object +cost per entry. + +## Choosing a Collection Type + +A short decision guide: + +- The key and value types are fixed and known - use [Table](#table); if the values vary in type, + use [Bag](#bag); +- The values are objects that should stay visible to wallets and explorers - take the + [ObjectTable](#objecttable) / [ObjectBag](#objectbag) variant; +- The collection has to be iterated onchain or preserve insertion order - use + [LinkedTable](#linkedtable), the only one of the five that links its entries; +- The collection is small, bounded, and needs to be embedded or compared as a plain value - the + vector-based [collections](./collections) from the earlier section may be a better fit than a + dynamic one. + +> One more thing to keep in mind: the entries of a dynamic collection live outside of the struct +> itself. Serializing a `Table` (for example, with [BCS](./bcs)) or comparing two tables only +> takes the `id` and `size` fields into account - never the contents. + ## Summary -- [Bag](#bag) - a simple collection that can store any type of data. -- [ObjectBag](#objectbag) - a collection that can store only objects. -- [Table](#table) - a typed dynamic collection that has a fixed type for keys and values. -- [ObjectTable](#objecttable) - same as Table, but can only store objects. -- [LinkedTable](#linkedtable) - similar to Table but the values are linked together. +- [Bag](#bag) - a simple collection that can store any type of data; fits containers of + heterogeneous values, such as inventories. +- [ObjectBag](#objectbag) - same as Bag, but can only store objects; fits heterogeneous assets + that should stay visible in wallets and explorers. +- [Table](#table) - a typed dynamic collection that has a fixed type for keys and values; fits + large uniform registries. +- [ObjectTable](#objecttable) - same as Table, but can only store objects; fits registries of + same-type objects that should stay individually discoverable. +- [LinkedTable](#linkedtable) - similar to Table but the entries are linked together; fits queues + and anything iterated onchain. + +## Next Steps + +This section concludes the tour of dynamic fields and the collections built on top of them. In the +next section we will move on to design patterns, starting with the +[Witness](./witness-pattern) pattern. ## Further Reading @@ -184,9 +284,3 @@ Using the LinkedTable: [linked-table-framework]: https://docs.sui.io/references/framework/sui/linked_table [bag-framework]: https://docs.sui.io/references/framework/sui/bag [object-bag-framework]: https://docs.sui.io/references/framework/sui/object_bag - - - - diff --git a/book/programmability/dynamic-fields.md b/book/programmability/dynamic-fields.md index 4a0379431..e59bcf745 100644 --- a/book/programmability/dynamic-fields.md +++ b/book/programmability/dynamic-fields.md @@ -4,23 +4,24 @@ description: "Dynamic fields in Sui: attach heterogeneous key-value data to obje # Dynamic Fields -Sui Object model allows objects to be attached to other objects as _dynamic fields_. The behavior is -similar to how a `Map` works in other programming languages. However, unlike a `Map` which in Move -would be strictly typed (we have covered it in the [Collections](./collections) section), dynamic -fields allow attaching objects of any type. A similar approach from the world of frontend -development would be a JavaScript Object type which allows storing any type of data dynamically. +The Sui Object Model allows attaching extra data to objects at runtime as _dynamic fields_. The +behavior is similar to how a `Map` works in other programming languages. However, unlike a `Map`, +which in Move would be strictly typed (we have covered it in the [Collections](./collections) +section), dynamic fields allow attaching values of any type. A similar approach from the world of +frontend development would be a JavaScript Object type which allows storing any type of data +dynamically. > There's no limit to the number of dynamic fields that can be attached to an object. Thus, dynamic -> fields can be used to store large amounts of data that don't fit into the object limit size. +> fields can be used to store large amounts of data that don't fit into the object size limit. -Dynamic Fields allow for a wide range of applications, from splitting data into smaller parts to -avoid [object size limit](./../guides/building-against-limits) to attaching objects as a part of -application logic. +Dynamic fields allow for a wide range of applications, from splitting data into smaller parts to +avoid the [object size limit](./../guides/building-against-limits) to attaching objects as a part +of application logic. ## Definition -Dynamic Fields are defined in the `sui::dynamic_field` module of the -[Sui Framework](./sui-framework). They are attached to object's `UID` via a _name_, and can be +Dynamic fields are defined in the `sui::dynamic_field` module of the +[Sui Framework](./sui-framework). They are attached to an object's `UID` via a _name_, and can be accessed using that name. There can be only one field with a given name attached to an object. ```move @@ -29,7 +30,7 @@ module sui::dynamic_field; /// Internal object used for storing the field and value public struct Field has key { /// Determined by the hash of the object ID, the field name - /// value and it's type, i.e. hash(parent.id || name || Name) + /// value and its type, i.e. hash(parent.id || name || Name) id: UID, /// The value for the name of this field name: Name, @@ -43,20 +44,15 @@ As the definition shows, dynamic fields are stored in an internal `Field` object The `Field` object contains the field name and the value bound to it. The constraints on the `Name` and `Value` type parameters define the abilities that the key and value must have. +_See [full documentation for sui::dynamic_field][dynamic-field-framework] module._ + ## Usage The methods available for dynamic fields are straightforward: a field can be added with `add`, -removed with `remove`, replaced with `replace`, and read with `borrow` and `borrow_mut`. -Additionally, the `exists_` method can be used to check if a field exists (for stricter checks with -type, there is an `exists_with_type` method). - -The `replace` function swaps the existing value of a field for a new one and returns the old value, -without requiring a separate `remove` and `add` call. For example: - -```move -// Replace the current hat and get the old one back -let old_hat = df::replace(&mut character.id, b"hat_key", Hat { color: 0xFF0000 }); -``` +removed with `remove`, and read with `borrow` and `borrow_mut`. Additionally, the `exists_` method +can be used to check if a field exists (for stricter checks with type, there is an +`exists_with_type` method), and `remove_if_exists` removes a field if it is present, returning an +[`Option`](./../move-basics/option) with the value. ```move file=packages/samples/sources/programmability/dynamic-fields.move anchor=usage @@ -64,7 +60,7 @@ let old_hat = df::replace(&mut character.id, b"hat_key", Hat { color: 0xFF0000 } In the example above, we define a `Character` object and two different types of accessories that could never be put together in a vector. However, dynamic fields allow us to store them together in -a single object. Both objects are attached to the `Character` via a `vector` (bytestring +a single object. Both objects are attached to the `Character` via a `vector` (a byte string literal), and can be accessed using their respective names. As you can see, when we attached the accessories to the Character, we passed them _by value_. In @@ -90,25 +86,6 @@ In this example we showed how different types can be used for both _name_ and th dynamic field. The `String` is attached via a `vector` name, the `u64` is attached via a `u32` name, and the `bool` is attached via a `bool` name. Anything is possible with dynamic fields! -## Orphaned Dynamic Fields - -> To prevent orphaned dynamic fields, please, use [Dynamic Collection Types](./dynamic-collections) -> such as `Bag` as they track the dynamic fields and won't allow unpacking if there are attached -> fields. - -The `object::delete()` function, which is used to delete a UID, does not track the dynamic fields, -and cannot prevent dynamic fields from becoming orphaned. Once the parent UID is deleted, the -dynamic fields are not automatically deleted, and they become orphaned. This means that the dynamic -fields are still stored in the blockchain, but they will never become accessible again. - -```move file=packages/samples/sources/programmability/dynamic-fields.move anchor=orphan_fields - -``` - -Orphaned objects are not a subject to storage rebate, and the storage fees will remain unclaimed. -One way to avoid orphaned dynamic fields during unpacking of an object is to return the `UID` and -store it somewhere temporarily until the dynamic fields are removed and handled properly. - ## Custom Type as a Field Name In the examples above, we used primitive types as field names since they have the required set of @@ -128,13 +105,13 @@ Two field names that we defined above are `AccessoryKey` and `MetadataKey`. The ``` -As you can see, custom types do work as field names but as long as they can be _constructed_ by the -module, in other words - if they are _internal_ to the module and defined in it. This limitation on +As you can see, custom types work as field names as long as they can be _constructed_ by the +module - in other words, if they are _internal_ to the module and defined in it. This limitation on struct packing can open up new ways in the design of the application. -This approach is used in the Object Capability pattern, where an -application can authorize a foreign object to perform operations in it while not exposing the -capabilities to other modules. +This approach is used in the [Object Capability](./object-capability) pattern, where an application +can authorize a foreign object to perform operations in it while not exposing the capabilities to +other modules. ## Exposing UID @@ -142,7 +119,7 @@ capabilities to other modules. Mutable access to `UID` is a security risk. Exposing `UID` of your type as a mutable reference can lead to unwanted modifications or removal of the object's dynamic fields. Additionally, it affects -the Transfer to Object and +[Transfer to Object](./../storage/transfer-to-object) and [Dynamic Object Fields](./dynamic-object-fields). Make sure to understand the implications before exposing the `UID` as a mutable reference. @@ -169,6 +146,25 @@ specific fields. ``` +## Orphaned Dynamic Fields + +> To prevent orphaned dynamic fields, please use [Dynamic Collection Types](./dynamic-collections) +> such as `Bag` as they track the dynamic fields and won't allow unpacking if there are attached +> fields. + +The `object::delete()` function, which is used to delete a UID, does not track the dynamic fields, +and cannot prevent dynamic fields from becoming orphaned. Once the parent UID is deleted, the +dynamic fields are not automatically deleted, and they become orphaned. This means that the dynamic +fields are still stored in the blockchain, but they will never become accessible again. + +```move file=packages/samples/sources/programmability/dynamic-fields.move anchor=orphan_fields + +``` + +Orphaned objects are not subject to the storage rebate, and the storage fees will remain unclaimed. +One way to avoid orphaned dynamic fields during unpacking of an object is to return the `UID` and +store it somewhere temporarily until the dynamic fields are removed and handled properly. + ## Dynamic Fields vs Fields Dynamic Fields are more expensive than regular fields, as they require additional storage and costs @@ -184,12 +180,28 @@ transaction. ## Applications -Dynamic Fields can play a crucial role in applications of any complexity. They open up a variety of +Dynamic fields can play a crucial role in applications of any complexity. They open up a variety of different use cases, from storing heterogeneous data to attaching objects as part of the application logic. They allow for certain [upgradeability practices](./../guides/upgradeability-practices) based on the ability to define them _later_ and change the type of the field. +## Summary + +- Dynamic fields attach values to an object's `UID` under a _name_; both the name and the value + can be of almost any type, including types defined in other modules. +- Attached values are owned by the parent object and can only be accessed through it. +- Custom types used as field names can only be constructed by the defining module, which protects + the fields from external access. +- Deleting the parent `UID` does not remove its dynamic fields - the fields left behind become + inaccessible _orphans_. + ## Next Steps In the next section we will cover [Dynamic Object Fields](./dynamic-object-fields) and explain how -they differ from dynamic fields, and what are the implications of using them. \ No newline at end of file +they differ from dynamic fields, and what are the implications of using them. + +## Further Reading + +- [sui::dynamic_field][dynamic-field-framework] module documentation. + +[dynamic-field-framework]: https://docs.sui.io/references/framework/sui/dynamic_field \ No newline at end of file diff --git a/book/programmability/dynamic-object-fields.md b/book/programmability/dynamic-object-fields.md index ad33cdbde..fdab661f0 100644 --- a/book/programmability/dynamic-object-fields.md +++ b/book/programmability/dynamic-object-fields.md @@ -4,25 +4,26 @@ description: "Dynamic object fields in Sui: attach objects as fields that remain # Dynamic Object Fields -> This section expands on the [Dynamic Fields](./dynamic-fields). Please, read it first to -> understand the basics of dynamic fields. +> This section expands on [Dynamic Fields](./dynamic-fields). Please read it first to understand +> the basics of dynamic fields. Another variation of dynamic fields is _dynamic object fields_, which have certain differences from regular dynamic fields. In this section, we will cover the specifics of dynamic object fields and explain how they differ from regular dynamic fields. -> General recommendation is to avoid using dynamic object fields in favor of (just) dynamic fields, +> The general recommendation is to avoid using dynamic object fields in favor of (just) dynamic fields, > especially if there's no need for direct discovery through the ID. The extra costs of dynamic > object fields may not be justified by the benefits they provide. ## Definition -Dynamic Object Fields are defined in the `sui::dynamic_object_fields` module in the +Dynamic Object Fields are defined in the `sui::dynamic_object_field` module in the [Sui Framework](./sui-framework). They are similar to dynamic fields in many ways, but unlike them, dynamic object fields have an extra constraint on the `Value` type. The `Value` must have a combination of `key` and `store`, not just `store` as in the case of dynamic fields. -They're less explicit in their framework definition, as the concept itself is more abstract: +The module definition is smaller than that of dynamic fields - only the field _name_ gets a +wrapper type, while the value is stored as-is: ```move module sui::dynamic_object_field; @@ -35,9 +36,11 @@ public struct Wrapper has copy, drop, store { } ``` -Unlike `Field` type in the [Dynamic Fields](./dynamic-fields#definition) section, the `Wrapper` type +Unlike the `Field` type in the [Dynamic Fields](./dynamic-fields#definition) section, the `Wrapper` type only stores the name of the field. The value is the object itself, and is _not wrapped_. +_See [full documentation for sui::dynamic_object_field][dynamic-object-field-framework] module._ + The constraints on the `Value` type become visible in the methods available for dynamic object fields. Here's the signature for the `add` function: @@ -53,13 +56,12 @@ public fun add( ) { /* implementation omitted */ } ``` -The rest of the methods which are identical to the ones in the -[Dynamic Fields](./dynamic-fields#usage) section have the same constraints on the `Value` type. -Let's list them for reference: +The rest of the methods are identical to the ones in the +[Dynamic Fields](./dynamic-fields#usage) section, and carry the same constraint on the `Value` +type. Let's list them for reference: - `add` - adds a dynamic object field to the object - `remove` - removes a dynamic object field from the object -- `replace` - replaces the value of an existing dynamic object field, returning the old value - `borrow` - borrows a dynamic object field from the object - `borrow_mut` - borrows a mutable reference to a dynamic object field from the object - `exists_` - checks if a dynamic object field exists @@ -68,31 +70,54 @@ Let's list them for reference: Additionally, there is an `id` method which returns the `ID` of the `Value` object without specifying its type. -## Usage & Differences with Dynamic Fields +## Usage and Differences with Dynamic Fields The main difference between dynamic fields and dynamic object fields is that the latter allows storing _only objects_ as values. This means that you can't store primitive types like `u64` or -`bool`. It may be considered a limitation, if not for the fact that dynamic object fields are _not -wrapped_ into a separate object. +`bool`. In exchange for this restriction, the attached object is _not wrapped_ into a separate +object: it keeps its ID and stays visible to offchain tooling. -> The relaxed requirement for wrapping keeps the object available for off-chain discovery via its -> ID. However, this property may not be outstanding if wrapped object indexing is implemented, -> making the dynamic object fields a redundant feature. +> This is the property to weigh when choosing between the two: a value attached as a regular +> dynamic field is wrapped into a `Field` object and disappears from ID-based queries, while a +> value attached as a dynamic object field remains discoverable by its ID in wallets and explorers. ```move file=packages/samples/sources/programmability/dynamic-object-fields.move anchor=usage ``` +In the example above, the `Accessory` has both `key` and `store`, so it can be attached as a +dynamic object field. The `Metadata`, however, only has `store`, so it can only be attached as a +regular dynamic field. Both kinds of fields coexist on the same `UID` - even under similar names - +because the internal `Wrapper` type prevents key collisions between the two modules. Lastly, the +example demonstrates the `id` function, which returns the `ID` of the attached object without +requiring its type - something only possible because the object keeps its identity. + ## Pricing Differences -Dynamic Object Fields come a little more expensive than dynamic fields. Because of their internal -structure, they require 2 objects: the Wrapper for Name and the Value. Because of this, the cost of -adding and accessing object fields (loading 2 objects compared to 1 for dynamic fields) is higher. +Dynamic object fields are a little more expensive than dynamic fields. Because of their internal +structure, a single dynamic object field is stored as two objects: an internal field storing the +name, and the value object itself. As a result, the cost of adding and accessing dynamic object +fields (loading 2 objects compared to 1 for dynamic fields) is higher. + +## Summary + +- Dynamic object fields require the value to be an object (`key` + `store`) and, unlike regular + dynamic fields, keep the attached object discoverable by its ID in wallets and explorers. +- The methods mirror those of dynamic fields, with an extra `id` function that returns the `ID` of + the attached object without specifying its type. +- Dynamic object fields are more expensive than dynamic fields, so prefer the latter unless + ID-based discovery is required. ## Next Steps -Both dynamic field and dynamic object fields are powerful features which allow for innovative +Both dynamic fields and dynamic object fields are powerful features which allow for innovative solutions in applications. However, they are relatively low-level and require careful handling to avoid orphaned fields. In the next section, we will introduce a higher-level abstraction - [Dynamic Collections](./dynamic-collections) - which can help with managing dynamic fields and -objects more effectively. \ No newline at end of file +objects more effectively. + +## Further Reading + +- [sui::dynamic_object_field][dynamic-object-field-framework] module documentation. + +[dynamic-object-field-framework]: https://docs.sui.io/references/framework/sui/dynamic_object_field \ No newline at end of file diff --git a/book/programmability/epoch-and-time.md b/book/programmability/epoch-and-time.md index 856fe4453..af9908971 100644 --- a/book/programmability/epoch-and-time.md +++ b/book/programmability/epoch-and-time.md @@ -4,39 +4,42 @@ description: "Access time in Sui Move: use epochs for operational periods and Cl # Epoch and Time -Sui has two ways of accessing the current time: `Epoch` and `Time`. The former represents -operational periods in the system and changed roughly every 24 hours. The latter represents the -current time in milliseconds since the Unix Epoch. Both can be accessed freely in the program. +Sui has two ways of accessing the current time: the _epoch_ and the `Clock` object. The former +represents operational periods in the system and changes roughly every 24 hours. The latter gives +the current time in milliseconds since the Unix Epoch. Both can be accessed freely in the program. ## Epoch Epochs are used to separate the system into operational periods. During an epoch the validator set -is fixed, however, at the epoch boundary, the validator set can be changed. Epochs play a crucial -role in the consensus algorithm and are used to determine the current validator set. They are also -used as measurement in the staking mechanism. +is fixed; at the epoch boundary, it can change. Epochs play a crucial role in the consensus +algorithm and are used as a unit of measurement in the staking mechanism. -Epoch can be read from the [transaction context](./transaction-context): +The current epoch can be read from the [transaction context](./transaction-context): ```move file=packages/samples/sources/programmability/epoch-and-time.move anchor=epoch ``` -It is also possible to get the unix timestamp of the epoch start: +It is also possible to get the Unix timestamp (in milliseconds) of the epoch start: ```move file=packages/samples/sources/programmability/epoch-and-time.move anchor=epoch_start ``` +Both values are embedded in the transaction itself, so reading them is free and does not require +access to any object. + Normally, epochs are used in staking and system operations, however, in custom scenarios they can be used to emulate 24h periods. They are critical if an application relies on the staking logic or needs to know the current validator set. ## Time -For a more precise time measurement, Sui provides the `Clock` object. It is a system object that is -updated during checkpoints by the system, which stores the current time in milliseconds since the -Unix Epoch. The `Clock` object is defined in the `sui::clock` module and has a reserved address -`0x6`. +For a more precise time measurement, Sui provides the `Clock` object. It is a system object, +updated by a system transaction on every consensus commit - roughly every quarter of a second - +which stores the current time in milliseconds since the Unix Epoch. The `Clock` object is defined +in the `sui::clock` module and has a +[reserved address](./../appendix/reserved-addresses) `0x6`. Clock is a shared object, but a transaction attempting to access it mutably will fail. This limitation allows parallel access to the `Clock` object, which is important for maintaining @@ -63,17 +66,39 @@ public struct Clock has key { } ``` -There is only one public function available in the `Clock` module - `timestamp_ms`. It returns the -current time in milliseconds since the Unix Epoch. +For regular use, the module exposes a single function - `timestamp_ms`. It returns the current +time in milliseconds since the Unix Epoch. ```move file=packages/samples/sources/programmability/epoch-and-time.move anchor=clock ``` +The `Clock` comes with a few useful guarantees: within a single transaction, `timestamp_ms` always +returns the same value, and across transactions the value never decreases. However, because the +clock is only updated on consensus commits, transactions executed close to each other may see an +identical timestamp. + ## Testing -The `Clock` module provides a number of methods for use in testing. +Since the real `Clock` is only updated by the system, the module provides test-only functions to +create a clock, set its value, and destroy it: ```move file=packages/samples/sources/programmability/epoch-and-time.move anchor=test -``` \ No newline at end of file +``` + +## Summary + +- The current epoch and its start timestamp are read from the + [transaction context](./transaction-context) - free and available in every transaction; an + epoch lasts roughly 24 hours. +- The `Clock` object at the reserved address `0x6` gives the time in milliseconds, updated on + every consensus commit; it can only be accessed immutably. +- Within a transaction the `Clock` value never changes, and across transactions it never + decreases. +- In tests, use `create_for_testing`, `set_for_testing`, `increment_for_testing`, and + `destroy_for_testing` to control the clock. + +## Further Reading + +- [sui::clock](https://docs.sui.io/references/framework/sui/clock) module documentation. \ No newline at end of file diff --git a/book/programmability/events.md b/book/programmability/events.md index 883766bc3..4a2eda501 100644 --- a/book/programmability/events.md +++ b/book/programmability/events.md @@ -1,24 +1,32 @@ --- -description: "Emit and subscribe to events in Sui Move: notify off-chain listeners about on-chain activity in your smart contracts." +description: "Emit and test events in Sui Move: notify offchain listeners about onchain activity in your smart contracts." --- # Events -Events are a way to notify off-chain listeners about on-chain events. They are used to emit -additional information about the transaction that is not stored - and, hence, can't be accessed - -on-chain. Events are emitted by the `sui::event` module located in the -[Sui Framework](./sui-framework). +Onchain storage keeps the _current_ state of the application: objects, their fields, and their +owners. What it does not keep is the history of actions that led to this state. A marketplace +module stores listed items, but once an item is sold and the object changes hands, there is no +onchain trace of the purchase - the price paid, the time of the sale, or the parties involved. +Applications, however, often need exactly that: an activity feed, a trading history, or analytics. -> Any custom type with the [copy](./../move-basics/copy-ability) and -> [drop](./../move-basics/drop-ability) abilities can be emitted as an event. Sui Verifier requires -> the type to be internal to the module. +_Events_ are the mechanism for this. An event is a piece of data attached to the result of a +successful transaction and stored offchain. Emitting an event does not modify any objects and +costs no storage fees; instead, events are indexed by full nodes, and offchain services can query +or subscribe to them. Events are the main way for a Move program to communicate with the outside +world. + +## Definition + +Events are emitted with the `emit` function defined in the [`sui::event`][event-framework] module +of the [Sui Framework](./sui-framework): ```move module sui::event; -/// Emit a custom Move event, sending the data off-chain. +/// Emit a custom Move event, sending the data offchain. /// -/// Used for creating custom indexes and tracking on-chain +/// Used for creating custom indexes and tracking onchain /// activity in a way that suits a specific application the most. /// /// The type `T` is the main way to index the event, and can contain @@ -26,25 +34,82 @@ module sui::event; public native fun emit(event: T); ``` +An event can be any custom type with the [copy](./../move-basics/copy-ability) and +[drop](./../move-basics/drop-ability) abilities. Additionally, the Sui Verifier requires the type +to be [_internal to the module_](./../storage/internal-constraint) that emits it: it is impossible to emit a type defined in another +module, and, even though they satisfy the `copy + drop` requirement, +[primitive types](./../move-basics/primitive-types) cannot be emitted either. This rule makes the +event type an unforgeable label - an `ItemPurchased` event can only ever originate from the module +that declares it. + ## Emitting Events -Events are emitted using the `emit` function in the `sui::event` module. The function takes a single -argument - the event to be emitted. The event data is passed by value, +To emit an event, define a struct for it and pass an instance of the struct to `event::emit`. The +event data is passed by value and sent offchain as part of the transaction result: ```move file=packages/samples/sources/programmability/events.move anchor=emit ``` -The Sui Verifier requires the type passed to the `emit` function to be _internal to the module_. So -emitting a type from another module will result in a compilation error. Primitive types, although -they match the _copy_ and _drop_ requirement, are not allowed to be emitted as events. +The type of the event serves as the primary filter for offchain queries - services subscribe to +`ItemPurchased` events by naming the type. This suggests a simple design principle: emit a +dedicated type per action, and name it after the action that happened, in past tense - +`ItemPurchased`, `AuctionStarted`, `ConfigUpdated`. Inside the event, include the values an +indexer would need to make sense of the action without fetching anything else: the IDs of the +objects involved, amounts, and the relevant addresses. + +Note that events are attached to a _successful_ transaction: if the transaction aborts after the +`emit` call, no events are recorded. ## Event Structure -Events are a part of the transaction result and are stored in the _transaction effects_. As such, -they natively have the `sender` field which is the address which sent the transaction. So adding a -"sender" field to the event is not necessary. Similarly, event metadata contains the timestamp. But -it is important to note that the timestamp is relative to the node and may vary a little from node -to node. +Events become part of the _transaction effects_, and the system attaches metadata to each of them: + +- the _sender_ - the address that signed the transaction; +- the _transaction digest_ - linking the event to the transaction that emitted it; +- the _timestamp_ - the time of the checkpoint that finalized the transaction, shared by all + events of that transaction; +- the _type signature_ of the event, including the package and module that emitted it. + +Because the sender and the transaction digest are always present in the metadata, there is no need +to duplicate them in the event fields. A `sender: address` field in an event struct is redundant, +unless the "logical" sender differs from the transaction signer (for example, in a sponsored +transaction executed on behalf of a user). + +It is important to understand that events are a one-way channel. Emitted events are not stored +onchain and cannot be read back by Move code - not in the same transaction, and not in any later +one. If a value needs to be accessed by the program, it belongs in an object; if it needs to be +seen by the outside world, it belongs in an event. + +## Testing Events + +Because events are the interface between the application and its offchain services, it is +important to test that the right events are emitted with the right values. The `sui::event` module +provides two test-only functions for this: `num_events`, returning the number of events emitted so +far in the test, and `events_by_type`, returning a vector of all emitted events of type `T`. + +```move file=packages/samples/sources/programmability/events.move anchor=test + +``` + +Since event structs are internal to the module, tests placed in the same module (or in a test +module of the same package with appropriate accessors) can inspect their fields directly. + +## Summary + +- Events attach application-defined data to the transaction result; they are indexed offchain and + are the main way to notify the outside world about onchain activity. +- Any custom type with `copy` and `drop` can be an event, but it must be internal to the emitting + module - this makes the event type an unforgeable label. +- Event metadata already contains the sender, the transaction digest, and a timestamp; event + fields should carry action-specific data, such as object IDs and amounts. +- Events cannot be read back by Move code - they are a one-way channel. +- Use `num_events` and `events_by_type` to test emitted events. + +## Further Reading + +- [sui::event][event-framework] module documentation. +- [Using Events](https://docs.sui.io/guides/developer/sui-101/using-events) in the Sui + Documentation - querying and subscribing to events offchain. - +[event-framework]: https://docs.sui.io/references/framework/sui/event diff --git a/book/programmability/hot-potato-pattern.md b/book/programmability/hot-potato-pattern.md index 09b2d3c0e..507ee556d 100644 --- a/book/programmability/hot-potato-pattern.md +++ b/book/programmability/hot-potato-pattern.md @@ -4,7 +4,7 @@ description: "The Hot Potato pattern in Move: a struct with no abilities that mu # Pattern: Hot Potato -A case in the abilities system - a struct without any abilities - is called _hot potato_. It cannot +A special case in the abilities system - a struct without any abilities - is called _hot potato_. It cannot be stored (not as [an object](./../storage/key-ability) nor as [a field in another struct](./../storage/store-ability)), it cannot be [copied](./../move-basics/copy-ability) or [discarded](./../move-basics/drop-ability). Hence, once @@ -58,27 +58,32 @@ a [dynamic field](./dynamic-fields). ### Flash Loans -Canonical example of the hot potato pattern is flash loans. A flash loan is a loan that is borrowed -and repaid in the same transaction. The borrowed funds are used to perform some operations, and the +The canonical example of the hot potato pattern is the flash loan - a loan that is borrowed and +repaid in the same transaction. The borrowed funds are used to perform some operations, and the repaid funds are returned to the lender. The hot potato pattern ensures that the borrowed funds are returned to the lender. An example usage of this pattern may look like this: ```move -// Borrow the funds from the lender. -let (asset_a, potato) = lender.borrow(amount); +// Borrow the funds from the lender; the `potato` obligates us to repay. +let (funds, potato) = lender.borrow(amount); // Perform some operations with the borrowed funds. -let asset_b = dex.trade(loan); -let proceeds = another_contract::do_something(asset_b); +let asset = dex.trade(funds); +let proceeds = another_contract::do_something(asset); -// Keep the commission and return the rest to the lender. -let pay_back = proceeds.split(amount, ctx); -lender.repay(pay_back, potato); +// Repay the loan and keep the profit. +let payback = proceeds.split(amount, ctx); +lender.repay(payback, potato); transfer::public_transfer(proceeds, ctx.sender()); ``` +> An outstanding hot potato also affects what the rest of the transaction is allowed to do: values +> entangled with it cannot be passed to non-`public` `entry` functions until the potato is +> consumed. The exact rules - with a worked flash-loan example - are described in +> [Entry Functions](./../move-advanced/entry-functions). + ### Variable-path Execution The hot potato pattern can be used to introduce variation in the execution path. For example, if @@ -102,11 +107,6 @@ define ways to interact with the hot potato, for example, stamp it with a type s extract some information from it. This way, the hot potato can be passed between different modules, and even different packages within the same transaction. - - ### Usage in the Sui Framework The pattern is used in various forms in the Sui Framework. Here are some examples: @@ -117,14 +117,18 @@ The pattern is used in various forms in the Sui Framework. Here are some example which can only be consumed if all conditions are met. - [sui::token][token-framework] - in the Closed Loop Token system, an `ActionRequest` carries the information about the performed action and collects approvals similarly to `TransferRequest`. +- [sui::package][package-framework] - the `UpgradeTicket` and `UpgradeReceipt` guarding the + [package upgrade](./package-upgrades) flow are hot potatoes: an authorized upgrade must be + performed and committed within the same transaction. -[borrow-framework]: https://docs.sui.io/references/framework/sui-framework/borrow -[transfer-policy-framework]: https://docs.sui.io/references/framework/sui-framework/transfer_policy -[token-framework]: https://docs.sui.io/references/framework/sui-framework/token +[borrow-framework]: https://docs.sui.io/references/framework/sui/borrow +[package-framework]: https://docs.sui.io/references/framework/sui/package +[transfer-policy-framework]: https://docs.sui.io/references/framework/sui/transfer_policy +[token-framework]: https://docs.sui.io/references/framework/sui/token ## Summary -- A hot potato is a struct without abilities, it must come with a way to create and destroy it. +- A hot potato is a struct without abilities; its module must provide ways to create and destroy it. - Hot potatoes are used to ensure that some action is taken before the transaction ends, similar to a callback. - Most common use cases for hot potato are borrowing, flash loans, variable-path execution, and diff --git a/book/programmability/index.md b/book/programmability/index.md index 83d61c7f2..1158cf84a 100644 --- a/book/programmability/index.md +++ b/book/programmability/index.md @@ -6,9 +6,39 @@ description: "Advanced Sui programmability: patterns, events, dynamic fields, ca In previous chapters we've covered [the basics of Move](./../move-basics) and [Sui Storage Model](./../storage). Now it's time to dive deeper into the advanced topics of Sui -programmability. +programmability. This chapter introduces more complex concepts, practices, and features of Move and +Sui that are essential for building more sophisticated applications. -This chapter introduces more complex concepts, practices and features of Move and Sui that are -essential for building more sophisticated applications. It is intended for developers who are -already familiar with the basics of Move and Sui, and are looking to expand their knowledge and -skills. +The sections are ordered so that each builds on what came before, but they also form a few mostly +independent threads, and it is fine to follow the one you need right now: + +- **The execution environment** - what a program can learn about the transaction it runs in and + the system around it, and how it communicates with the outside world: + [Sui Framework](./sui-framework), [Transaction Context](./transaction-context), + [Module Initializer](./module-initializer), [Epoch and Time](./epoch-and-time), + [Events](./events), and [Binary Canonical Serialization](./bcs). + +- **Storage at scale** - from simple vector-based collections to dynamic fields, a primitive that + attaches arbitrary data to objects and lifts static type and size limits: + [Collections](./collections), [Wrapper Type](./wrapper-type-pattern), + [Dynamic Fields](./dynamic-fields), [Dynamic Object Fields](./dynamic-object-fields), and + [Dynamic Collections](./dynamic-collections). + +- **Patterns of authority** - Move's answer to access control: from owned objects acting as + permissions to guarantees backed by the system, and features built on top of them: + [Capability](./capability), [Witness](./witness-pattern), + [One Time Witness](./one-time-witness), [Publisher](./publisher), [Display](./display), and + [Hot Potato](./hot-potato-pattern). + +- **Assets and funds** - fungible value and the two ways to hold it, as objects and as balances + attached directly to an address: [Balance and Coin](./balance-and-coin) and + [Address Balances](./address-balances). + +- **Code evolution** - what happens after the code ships: publishing new versions of a package, + protecting shared state from old versions, and migrating data: + [Package Upgrades](./package-upgrades). + +> Many code samples in this chapter are written as [tests](./../move-basics/testing), and use +> test-only helpers from the framework: `tx_context::dummy()` creates a placeholder transaction +> context, and `std::unit_test::destroy` consumes any value at the end of a test. We cover testing +> techniques in detail in the [Testing](./../testing) chapter. diff --git a/book/programmability/module-initializer.md b/book/programmability/module-initializer.md index dc69c898d..c254b374f 100644 --- a/book/programmability/module-initializer.md +++ b/book/programmability/module-initializer.md @@ -5,14 +5,13 @@ description: "The init function in Move: run one-time setup code when a module i # Module Initializer A common use case in many applications is to run certain code just once when the package is -published. Imagine a simple store module that needs to create the main Store object upon its +published. Imagine a simple shop module that needs to create the main `Shop` object upon its publication. In Sui, this is achieved by defining an `init` function within the module. This function will automatically be called when the module is published. -> All of the modules' `init` functions are called during the publishing process. Currently, this -> behavior is limited to the publish command and does not extend to package upgrades. -> -> +> The `init` function of every module in the package is called during the publishing process. This +> behavior is limited to the publish command and does not extend to package upgrades - a module +> added in an upgrade will not have its `init` called. ```move file=packages/samples/sources/programmability/module-initializer.move anchor=main @@ -24,45 +23,61 @@ In the same package, another module can have its own `init` function, encapsulat ``` -## `init` Features +## The `init` Rules -The function is called on publish, if it is present in the module and follows the rules: +The function is called on publish if it is present in the module and follows these rules: -- The function has to be named `init`, be private and have no return values. -- Takes one or two arguments: [One Time Witness](./one-time-witness) (optional) and - [TxContext](./transaction-context). With `TxContext` always being the last argument. +- The function must be named `init`, be private, and have no return values; +- it cannot be declared as `entry` and cannot have type parameters; +- it takes one or two arguments: an optional [One Time Witness](./one-time-witness) and the + [TxContext](./transaction-context), with `TxContext` always being the last argument. ```move -fun init(ctx: &mut TxContext) { /* ... */} +fun init(ctx: &mut TxContext) { /* ... */ } fun init(otw: OTW, ctx: &mut TxContext) { /* ... */ } ``` -TxContext can also be passed as immutable reference: `&TxContext`. However, practically speaking, it -should always be `&mut TxContext` since the `init` function can't access the on-chain state and to -create new objects it requires the mutable reference to the context. +These rules are not a convention - they are enforced by the bytecode verifier. A function named +`init` that violates any of them fails verification, and the package cannot be published. -```move -fun init(ctx: &TxContext) { /* ... */} -fun init(otw: OTW, ctx: &TxContext) { /* ... */ } -``` +`TxContext` can also be taken as an immutable reference `&TxContext`, but in practice it should +always be `&mut TxContext`: the `init` function cannot access the onchain state, so creating new +objects is the whole point of it - and that requires a mutable reference to the context. ## Trust and Security -While `init` function can be used to create sensitive objects once, it is important to know that the -same object (e.g. `StoreOwnerCap` from the first example) can still be created in another function. -Especially given that new functions can be added to the module during an upgrade. So the `init` -function is a good place to set up the initial state of the module, but it is not a security measure -on its own. +While the `init` function can be used to create sensitive objects once, it is important to know +that the same object (e.g. `ShopOwnerCap` from the first example) can still be created in another +function - especially since new functions can be added to the module during an upgrade. The `init` +function is a good place to set up the initial state of the module, but it is not a security +measure on its own. There are ways to guarantee that the object was created only once, such as the -[One Time Witness](./one-time-witness). And there are ways to limit or disable the upgrade of the -module, which we will cover in the Package Upgrades chapter. +[One Time Witness](./one-time-witness). And there are ways to limit or disable package upgrades, +described in +[Custom Upgrade Policies](https://docs.sui.io/concepts/sui-move-concepts/packages/custom-policies) +in the Sui Documentation. + +## Testing the Initializer + +The `init` function is called by the runtime and cannot be invoked in a transaction. However, it +is a regular function in every other sense, so [tests](./../move-basics/testing) placed in the same +module can call it directly: + +```move file=packages/samples/sources/programmability/module-initializer.move anchor=test + +``` + +For an `init` function that takes a [One Time Witness](./one-time-witness), the witness value can +be created in tests with the test-only `sui::test_utils::create_one_time_witness` function. And in +scenario-based tests, described in the [Test Scenario](./../testing/test-scenario) section, the +objects created by `init` can also be inspected after the call. ## Next Steps As follows from the definition, the `init` function is guaranteed to be called only once when the -module is published. So it is a good place to put the code that initializes module's objects and -sets up the environment and configuration. +module is published. So it is a good place to put the code that initializes the module's objects +and sets up the environment and configuration. For example, if there's a [Capability](./capability) which is required for certain actions, it should be created in the `init` function. In the next chapter we will talk about the `Capability` diff --git a/book/programmability/one-time-witness.md b/book/programmability/one-time-witness.md index d674c4543..8120fbdc9 100644 --- a/book/programmability/one-time-witness.md +++ b/book/programmability/one-time-witness.md @@ -4,22 +4,66 @@ description: "One Time Witness (OTW) in Sui Move: a type guaranteed to be instan # One Time Witness -While regular [Witness](./witness-pattern) is a great way to statically prove the ownership of a -type, there are cases where we need to ensure that a Witness is instantiated only once. And this is -the purpose of the One Time Witness (OTW). +While the regular [Witness](./witness-pattern) is a great way to statically prove the ownership of +a type, there are cases where we need to ensure that a witness is instantiated only once - and this +is the purpose of the One Time Witness (OTW). - +## Background + +To see the problem the OTW solves, let's try to build a simple generic coin implementation with the +tools we already have. A `TreasuryCap` controls the supply of a coin of type `T`, and creating +one requires a [witness](./witness-pattern) of `T`: + +```move +module book::simple_coin; + +/// Controls the supply of the Coin. +public struct TreasuryCap has key, store { + id: UID, + total_supply: u64, +} + +/// Create a new `TreasuryCap` with a witness. +/// Vulnerable: nothing prevents the caller from creating +/// multiple `TreasuryCap`s with the same witness! +public fun new(_witness: T, ctx: &mut TxContext): TreasuryCap { + TreasuryCap { id: object::new(ctx), total_supply: 0 } +} +``` + +The regular witness proves that the calling module owns the type `T`, but it proves nothing about +_how many times_ the witness has been - or will be - constructed. A dishonest developer can simply +call `new` twice and keep a second treasury for themselves: + +```move +module book::simple_coin_cheater; + +/// The Coin witness... used twice. >_< +public struct MOVE has drop {} + +fun init(ctx: &mut TxContext) { + let treasury = book::simple_coin::new(MOVE {}, ctx); + let secret_treasury = book::simple_coin::new(MOVE {}, ctx); + + transfer::public_transfer(treasury, ctx.sender()); + transfer::public_transfer(secret_treasury, ctx.sender()); +} +``` + +For anyone deciding whether to trust a coin built this way, there is a whole list of conditions to +audit: that only one `TreasuryCap` exists for the given `T`, that the module has no backdoor to +issue more, and that a future upgrade cannot add one. None of these conditions can be checked from +within Move code - verifying them requires trust in the author, and careful (and repeated) review +of the source. + +To remove the need for this trust, Sui introduces the One Time Witness - a witness that the system +itself guarantees to be instantiated exactly once, checkable at runtime. ## Definition -The OTW is a special type of Witness that can be used only once. It cannot be manually created and -it is guaranteed to be unique per module. Sui Adapter treats a type as an OTW if it follows these -rules: +The OTW is a special type of witness that can be used only once. It cannot be manually created and +it is guaranteed to be unique per module. The Sui execution environment treats a type as an OTW if +it follows these rules: 1. Has only `drop` ability. 2. Has no fields. @@ -39,120 +83,26 @@ module, the OTW is guaranteed to be instantiated only once. ## Enforcing the OTW -To check if a type is an OTW, `sui::types` module of the [Sui Framework](./sui-framework) offers a -special function `is_one_time_witness` that can be used to check if the type is an OTW. +To check if a type is an OTW, the `sui::types` module of the [Sui Framework](./sui-framework) +offers a special function `is_one_time_witness`. This is the runtime counterpart of the rules +above: a library function that expects an OTW should call it to make sure the received witness is +the real, one-time one, and not a regular type with the `drop` ability. ```move file=packages/samples/sources/programmability/one-time-witness.move anchor=usage ``` - - - +This single `assert!` is what fixes the coin example from the [Background](#background) section: +if `simple_coin::new` required an OTW instead of a regular witness, the second call in the cheater +module would fail, because the OTW instance exists only once - in the first call. ## Summary -The OTW pattern is a great way to ensure that a type is used only once. Most of the developers -should understand how to define and receive the OTW, while the OTW checks and enforcement is mostly -needed in libraries and frameworks. For example, the `sui::coin` module requires an OTW in the +The OTW pattern is a great way to ensure that a type is used only once. Most developers only need +to know how to define and receive an OTW, while the checks and enforcement are mostly the concern +of libraries and frameworks. For example, the `sui::coin` module requires an OTW in the `coin::create_currency` method, therefore enforcing that the `coin::TreasuryCap` is created only -once. +once - solving exactly the problem we described in the [Background](#background) section. OTW is a powerful tool that lays the foundation for the [Publisher](./publisher) object, which we will cover in the next section. - - diff --git a/book/programmability/package-upgrades.md b/book/programmability/package-upgrades.md new file mode 100644 index 000000000..4fb0adc22 --- /dev/null +++ b/book/programmability/package-upgrades.md @@ -0,0 +1,204 @@ +--- +description: + 'Package upgrades on Sui: how new versions are published, what the UpgradeCap is, how to make a + package immutable, and how to version and migrate shared state.' +--- + +# Package Upgrades + +As we mentioned in the [Packages](./../concepts/packages) concept, published packages are +_immutable_ - the bytecode stored onchain can never be modified or deleted. Yet real applications +need to evolve: bugs get fixed, features get added, and dependencies move forward. Sui reconciles +these two requirements with _package upgrades_ - a way to publish a new version of a package while +keeping every previous version intact. + +This section explains the mechanics: what an upgrade can and cannot change, the `UpgradeCap` object +that authorizes upgrades, and - most importantly - what upgrades mean for the state your package +has already created. For design advice on writing upgrade-friendly code, see the +[Upgradeability Practices](./../guides/upgradeability-practices) guide. + +## An Upgrade Is a New Package + +An upgrade does not touch the published bytecode. Instead, it publishes the new version of the code +at a _new address_ and records it as the successor of the previous version. Both versions - in +fact, all versions ever published - remain onchain side by side: + +``` +0xAAA... <- version 1, published +0xBBB... <- version 2, upgrade of 0xAAA +0xCCC... <- version 3, upgrade of 0xBBB, the latest version +``` + +This has a consequence that is easy to miss: **old versions of a package remain callable**. An +upgrade does not redirect anyone - a transaction can still call functions of version 1 directly, +and packages that depend on version 1 keep calling version 1 until they upgrade their own +dependency. Publishing a fix does not, by itself, stop the buggy version from being used. We will +return to this point when we talk about [state](#upgrades-and-state). + +Types, however, are not duplicated across versions. A struct keeps the identity of the package +version that first _defined_ it: a `Counter` type from version 1 is exactly the same type in +version 2, and objects created before the upgrade are fully compatible with the new code. A type +first added in version 2 belongs to version 2, and so on. + +## What Can Change + +An upgraded package must stay _compatible_ with the previous version, so that existing callers and +dependent packages don't break. Under the default - most permissive - upgrade policy, an upgrade +can: + +- change the implementation of any function; +- add new modules, functions, and types; +- change, add, or remove `public(package)`, private, and non-public + [`entry`](./../move-advanced/entry-functions) functions; +- change dependencies. + +And it can not: + +- remove a module; +- change or remove the signature of a `public` function; +- change or remove an existing type definition - fields, abilities, and type parameters of every + struct and enum are frozen forever, whether public or not. + +In short: public signatures and data layouts are permanent, implementations are not. This is why +the [Upgradeability Practices](./../guides/upgradeability-practices) guide recommends keeping +`public` surface minimal and structs thin - every `public` function and every struct field is a +commitment for the lifetime of the package. + +## The `UpgradeCap` + +When a package is published, the `Publish` command returns an `UpgradeCap` - an object defined in +the `sui::package` module of the [Sui Framework](./sui-framework). It is a classic +[capability](./capability): whoever owns it can upgrade the package, and no one else can. + +```move +module sui::package; + +/// Capability controlling the ability to upgrade a package. +public struct UpgradeCap has key, store { + id: UID, + /// (Mutable) ID of the package that can be upgraded. + package: ID, + /// (Mutable) The number of upgrades that have been applied + /// successively to the original package. Initially 0. + version: u64, + /// What kind of upgrades are allowed. + policy: u8, +} +``` + +The `package` field always points at the latest version - only the latest version of a package can +be upgraded, so the chain of versions never forks. The upgrade itself is a three-step dance inside +a single transaction: `authorize_upgrade` takes the `UpgradeCap` and returns an `UpgradeTicket`; +the `Upgrade` transaction command consumes the ticket, verifies and publishes the new bytecode, and +returns an `UpgradeReceipt`; finally, `commit_upgrade` applies the receipt back to the +`UpgradeCap`. Both the ticket and the receipt are [hot potatoes](./hot-potato-pattern) - they +cannot be stored or dropped, so an authorized upgrade cannot be left half-finished. In practice the +whole flow is built for you by the `sui client upgrade` CLI command. + +The `policy` field stores the most permissive kind of upgrade the capability allows. It starts at +_compatible_ - the default policy described [above](#what-can-change) - and can be restricted to +_additive_ (only new functionality can be added, existing code is frozen) or _dependency-only_ +(only dependencies can be changed). Restriction is a one-way street: `only_additive_upgrades` and +`only_dep_upgrades` can tighten the policy, but nothing can loosen it back. And because +`authorize_upgrade` is a regular public function taking the `UpgradeCap`, the capability can be +wrapped in a custom object to enforce arbitrary upgrade rules - a timelock, a multisig, or a vote. + +## Making a Package Immutable + +The final restriction is giving up upgrades altogether. Deleting the `UpgradeCap` makes the package +truly immutable - no one will ever be able to publish a new version: + +```move +/// Discard the `UpgradeCap` to make a package immutable. +public entry fun make_immutable(cap: UpgradeCap) { + let UpgradeCap { id, package: _, version: _, policy: _ } = cap; + id.delete(); +} +``` + +This is irreversible, and that is exactly the point: it is the strongest guarantee a package can +offer. Users and dependent packages know the code they reviewed is the code that will run forever. +The trade-off is equally permanent - no bug can ever be fixed. Immutability is a common choice for +small foundational libraries, and a dangerous one for evolving applications. + +## Upgrades and State + +Objects are stored outside of packages, and an upgrade does not touch them: a shared object created +by version 1 is just as accessible to version 1 as it is to version 2. Combined with the fact that +old versions remain callable, this leads to the central problem of upgrades: **without explicit +versioning, the old code keeps full access to the state**. If version 2 fixes a bug in a function +that mutates a shared object, an attacker can simply keep calling the version 1 function - on the +very same object. + +The solution is to version the state itself. The object carries a `version` field, the package +carries a `VERSION` constant, and every function that touches the object first checks that the two +match: + +```move file=packages/samples/sources/programmability/package-upgrades.move anchor=versioned + +``` + +Constants are baked into the bytecode, so each published version compares the object against its +own number: version 1 bytecode checks for `1`, version 2 bytecode checks for `2`. As long as the +object's field says `1`, the old code keeps working and the new code aborts - and the moment the +field is bumped to `2`, the situation flips: every call into the old version aborts with +`EVersionMismatch`, and only the latest code can proceed. Bumping the version is how the old +package is _decommissioned_. + +## Migrating State + +The version bump - the _migration_ - can be performed in two ways, and the choice depends on how +much state there is and who can reach it. + +The straightforward way is an _eager_ migration: right after the upgrade, the holder of an admin +[capability](./capability) calls a `migrate` function which bumps the version of the shared object +in a single transaction: + +```move file=packages/samples/sources/programmability/package-upgrades.move anchor=migrate + +``` + +Eager migration is a clean cut-over and is the right choice when the state is a handful of shared +objects the publisher controls. It falls short when it can't reach everything: an application may +have thousands of objects, or the objects may be _owned_ by users - and only the owner can send a +transaction touching an owned object. + +For these cases there is _lazy_ migration: instead of migrating everything up front, each object is +migrated the first time the new code touches it. This is also the answer to a limitation we saw +[earlier](#what-can-change) - struct layouts can never change, so how does state evolve at all? By +keeping the base object thin and storing the actual content in a [dynamic field](./dynamic-fields), +which can be swapped for a new shape at any time: + +```move file=packages/samples/sources/programmability/package-upgrades-2.move anchor=lazy + +``` + +Version 1 of this package attached a `ConfigV1` to the object; version 2 defines a richer +`ConfigV2` and quietly replaces the old value on first access. No coordinated migration is needed - +objects upgrade themselves as they are used, whether there are ten of them or ten million, owned or +shared. + +## Summary + +- An upgrade publishes a new version of a package at a new address; all previous versions stay + onchain and _remain callable_. +- Compatibility rules protect callers: implementations can change and new code can be added, but + `public` function signatures and type definitions are permanent. +- The `UpgradeCap` is the capability authorizing upgrades; its policy can be restricted one way - + from compatible to additive to dependency-only - and deleting it via `make_immutable` makes the + package immutable forever. +- State is not part of the package: without explicit versioning, old versions keep full access to + shared objects. A `version` field checked against a package `VERSION` constant decommissions old + code. +- Migrations can be _eager_ - an admin bumps the version right after the upgrade - or _lazy_ - + each object migrates on first access, which also allows evolving the shape of the state through + dynamic fields. + +## Further Reading + +- [Upgradeability Practices](./../guides/upgradeability-practices) guide on designing + upgrade-friendly packages. +- [Package Upgrades](https://docs.sui.io/concepts/sui-move-concepts/packages/upgrade) in the Sui + documentation. +- [Custom Upgrade Policies](https://docs.sui.io/concepts/sui-move-concepts/packages/custom-policies) + in the Sui documentation. diff --git a/book/programmability/publisher.md b/book/programmability/publisher.md index db8d1875a..8ffa5ff1c 100644 --- a/book/programmability/publisher.md +++ b/book/programmability/publisher.md @@ -4,10 +4,10 @@ description: "The Publisher object in Sui: prove package authority to configure # Publisher Authority -In application design and development, it is often needed to prove publisher authority. This is -especially important in the context of digital assets, where the publisher may enable or disable -certain features for their assets. The Publisher Object is an object, defined in the -[Sui Framework](./sui-framework), that allows the publisher to prove their _authority over a type_. +Applications often need to prove _who published a type_. This is especially important in the +context of digital assets, where the publisher may enable or disable certain features for their +assets. The Publisher object, defined in the [Sui Framework](./sui-framework), is what allows the +publisher to prove their _authority over a type_. ## Definition @@ -26,19 +26,20 @@ public struct Publisher has key, store { } ``` -> If you're not familiar with the One Time Witness, you can read more about it -> [here](./one-time-witness). - Here's a simple example of claiming a `Publisher` object in a module: ```move file=packages/samples/sources/programmability/publisher.move anchor=publisher ``` +> For the common claim-and-transfer flow, the `sui::package` module also provides a shorthand - +> `package::claim_and_keep` - which claims the `Publisher` object and transfers it to the sender in +> one call. + ## Usage -The Publisher object has two functions associated with it which are used to prove the publisher's -authority over a type: +The Publisher object has two functions associated with it - `from_module` and `from_package` - +which check whether a type was defined in the module or package this `Publisher` stands for: ```move file=packages/samples/sources/programmability/publisher.move anchor=use_publisher @@ -54,20 +55,23 @@ system configurations, it can also be used to manage the application's state. ``` -However, Publisher misses some native properties of [Capabilities](./capability), such as type -safety and expressiveness. The signature for the `admin_action` is not very explicit, can be called -by anyone else. And due to `Publisher` object being standard, there is now a risk of unauthorized -access if the `from_module` check is not performed. So it's important to be cautious when using the -`Publisher` object as an admin role. +However, the Publisher object lacks some of the native properties of +[Capabilities](./capability), such as type safety and expressiveness. The signature of +`admin_action` says nothing about the required authority - the function can be called by anyone +holding _any_ `Publisher` object, so the authorization must be checked inside the function body. +And since every published package produces a `Publisher`, forgetting the `from_module` check opens +the action to every publisher on the network. For these reasons, it is important to be cautious +when using the `Publisher` object as an admin role. ## Role on Sui -Publisher is required for certain features on Sui. [Object Display](./display) can be created only -by the Publisher, and TransferPolicy - an important component of the Kiosk system - also requires -the Publisher object to prove ownership of the type. +Publisher is required for certain features on Sui. [Object Display](./display) can be created with +the Publisher when it is set up outside of the module defining the type, and TransferPolicy - an +important component of the Kiosk system - also requires the Publisher object to prove ownership of +the type. ## Next Steps -In the next chapter we will cover the first feature that requires the Publisher object - Object +In the next section we will cover the first feature that can use the Publisher object - Object Display - a way to describe objects for clients, and standardize metadata. A must-have for user-friendly applications. diff --git a/book/programmability/randomness.md b/book/programmability/randomness.md index 6d79afe7e..541bfc8f8 100644 --- a/book/programmability/randomness.md +++ b/book/programmability/randomness.md @@ -1,55 +1,232 @@ --- -description: "On-chain randomness in Sui: generate secure random values in Move smart contracts using the Random shared object." +description: + 'Onchain randomness in Sui: generate secure random values in Move smart contracts using the + Random shared object.' --- -# Randomness +# Onchain Randomness - +- [Onchain Randomness](https://docs.sui.io/guides/developer/advanced/randomness-onchain) guide in + Sui documentation. +- [sui::random](https://docs.sui.io/references/framework/sui/random) module documentation. diff --git a/book/programmability/sui-framework.md b/book/programmability/sui-framework.md index 0182b3d96..c5ed58df8 100644 --- a/book/programmability/sui-framework.md +++ b/book/programmability/sui-framework.md @@ -5,8 +5,9 @@ description: "The Sui Framework: built-in modules for storage, coins, display, c # Sui Framework Sui Framework is a default dependency set in the [Package Manifest](./../concepts/manifest). It -depends on the [Standard Library](./../move-basics/standard-library) and provides Sui-specific -features, including the interaction with the storage, and Sui-specific native types and modules. +depends on the [Standard Library](./../move-basics/standard-library) and provides the Sui-specific +functionality: storage operations, native types, and the modules the rest of this chapter is built +on. _For convenience, we grouped the modules in the Sui Framework into multiple categories. But they're still part of the same framework._ @@ -26,8 +27,8 @@ still part of the same framework._ | [sui::clock](https://docs.sui.io/references/framework/sui/clock) | Defines the `Clock` type and its methods | [Epoch and Time](./epoch-and-time) | | [sui::dynamic_field](https://docs.sui.io/references/framework/sui/dynamic_field) | Implements methods to add, use and remove dynamic fields | [Dynamic Fields](./dynamic-fields) | | [sui::dynamic_object_field](https://docs.sui.io/references/framework/sui/dynamic_object_field) | Implements methods to add, use and remove dynamic object fields | [Dynamic Object Fields](./dynamic-object-fields) | -| [sui::event](https://docs.sui.io/references/framework/sui/event) | Allows emitting events for off-chain listeners | [Events](./events) | -| [sui::package](https://docs.sui.io/references/framework/sui/package) | Defines the `Publisher` type and package upgrade methods | [Publisher](./publisher), Package Upgrades | +| [sui::event](https://docs.sui.io/references/framework/sui/event) | Allows emitting events for offchain listeners | [Events](./events) | +| [sui::package](https://docs.sui.io/references/framework/sui/package) | Defines the `Publisher` type and package upgrade methods | [Publisher](./publisher) | | [sui::display](https://docs.sui.io/references/framework/sui/display) | Implements the `Display` object and ways to create and update it | [Display](./display) |
@@ -48,6 +49,21 @@ still part of the same framework._ +## Coins and Assets + +
+ +| Module | Description | Chapter | +| ---------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------- | +| [sui::balance](https://docs.sui.io/references/framework/sui/balance) | The `Balance` type - the underlying store of value | [Balance and Coin](./balance-and-coin) | +| [sui::coin](https://docs.sui.io/references/framework/sui/coin) | The `Coin` type - a transferable fungible asset | [Balance and Coin](./balance-and-coin) | +| [sui::sui](https://docs.sui.io/references/framework/sui/sui) | The SUI coin type | [Balance and Coin](./balance-and-coin) | +| [sui::pay](https://docs.sui.io/references/framework/sui/pay) | Helper functions for splitting and merging coins | - | +| [sui::deny_list](https://docs.sui.io/references/framework/sui/deny_list) | Deny list for regulated coin types | - | +| [sui::token](https://docs.sui.io/references/framework/sui/token) | The closed-loop token standard | - | + +
+ ## Utilities
@@ -57,22 +73,25 @@ still part of the same framework._ | [sui::bcs](https://docs.sui.io/references/framework/sui/bcs) | Implements the BCS encoding and decoding functions | [Binary Canonical Serialization](./bcs) | | [sui::borrow](https://docs.sui.io/references/framework/sui/borrow) | Implements the borrowing mechanic for borrowing by _value_ | [Hot Potato](./hot-potato-pattern) | | [sui::hex](https://docs.sui.io/references/framework/sui/hex) | Implements the hex encoding and decoding functions | - | +| [sui::random](https://docs.sui.io/references/framework/sui/random) | The `Random` object and secure onchain randomness | [Randomness](./randomness) | | [sui::types](https://docs.sui.io/references/framework/sui/types) | Provides a way to check if the type is a One-Time-Witness | [One Time Witness](./one-time-witness) |
+The framework also contains modules not covered in this book: the commerce primitives +([sui::kiosk](https://docs.sui.io/references/framework/sui/kiosk), +[sui::transfer_policy](https://docs.sui.io/references/framework/sui/transfer_policy)), a set of +cryptographic functions ([sui::hash](https://docs.sui.io/references/framework/sui/hash), +[sui::ed25519](https://docs.sui.io/references/framework/sui/ed25519), +[sui::bls12381](https://docs.sui.io/references/framework/sui/bls12381), and others), and assorted +utilities such as [sui::url](https://docs.sui.io/references/framework/sui/url) and +[sui::versioned](https://docs.sui.io/references/framework/sui/versioned). Refer to the +[framework documentation](https://docs.sui.io/references/framework) for the full list. + ## Exported Addresses Sui Framework exports two named addresses: `sui = 0x2` and `std = 0x1` from the std dependency. -```toml -[addresses] -sui = "0x2" - -# Exported from the MoveStdlib dependency -std = "0x1" -``` - ## Implicit Imports Just like with [Standard Library](./../move-basics/standard-library#implicit-imports), some of the @@ -90,56 +109,3 @@ types that are available without explicit `use` import: The source code of the Sui Framework is available in the [Sui repository](https://github.com/MystenLabs/sui/tree/main/crates/sui-framework/packages/sui-framework/sources). - - diff --git a/book/programmability/transaction-context.md b/book/programmability/transaction-context.md index 5596175c0..2002b2496 100644 --- a/book/programmability/transaction-context.md +++ b/book/programmability/transaction-context.md @@ -1,12 +1,12 @@ --- -description: "TxContext in Sui Move: access sender address, transaction digest, epoch, and generate unique IDs in your smart contracts." +description: "TxContext in Sui Move: access sender address, transaction digest, epoch, gas price, and generate unique IDs in your smart contracts." --- # Transaction Context -Every transaction has the execution context. The context is a set of predefined variables that are -available to the program during execution. For example, every transaction has a sender address, and -the transaction context contains a variable that holds the sender address. +Every transaction is executed in a _transaction context_. The context is a set of predefined +values available to the program during execution, such as the sender address, the current epoch, +or the transaction digest. The transaction context is available to the program through the `TxContext` struct. The struct is defined in the [`sui::tx_context`][tx-context-framework] module and contains the following fields: @@ -34,8 +34,13 @@ public struct TxContext has drop { } ``` -Transaction context cannot be constructed manually or directly modified. It is created by the system -and passed to the function as a reference in a transaction. Any function called in a +> While the struct still declares its original fields, current versions of the framework no longer +> read most of them directly - the getter functions forward to native functions implemented in the +> Sui execution environment. The fields are kept for compatibility, and `TxContext` is best thought +> of as an opaque handle to the execution environment. + +Transaction context cannot be constructed manually or directly modified. It is created by the +system and passed to the function as a reference in a transaction. Any function called in a [Transaction](./../concepts/what-is-a-transaction) has access to the context and can pass it into the nested calls. @@ -43,33 +48,52 @@ the nested calls. ## Reading the Transaction Context -With only exception of the `ids_created`, all of the fields in the `TxContext` have getters. The -getters are defined in the `sui::tx_context` module and are available to the program. The getters -don't require `&mut` because they don't modify the context. +The `sui::tx_context` module provides a getter for each of the values available in the context. +None of the getters require a mutable reference, since reading the context does not modify it: + +- `sender()` - the address that signed the transaction; +- `digest()` - a reference to the 32-byte digest (hash) of the current transaction, unique per + transaction; +- `epoch()` - the current [epoch](./epoch-and-time) number; +- `epoch_timestamp_ms()` - the timestamp of the moment the epoch started, in milliseconds; +- `sponsor()` - the address of the transaction sponsor, or `None` if the transaction was not + sponsored; +- `gas_price()` - the gas price submitted with the current transaction; +- `reference_gas_price()` - the reference gas price of the current epoch. ```move file=packages/samples/sources/programmability/transaction-context.move anchor=reading ``` -## Mutability +> The transaction digest is a hash of the transaction inputs, and while it is unique per +> transaction, it should never be used as a source of randomness - it is known before the +> transaction is executed, and can be manipulated by the sender. -The `TxContext` is required to create new objects (or just `UID`s) in the system. New UIDs are -derived from the transaction digest, and for the digest to be unique, there needs to be a changing -parameter. Sui uses the `ids_created` field for that. Every time a new UID is created, the -`ids_created` field is incremented by one. This way, the digest is always unique. +The `sponsor()` getter is related to _sponsored transactions_ - transactions where a third party, +the sponsor, pays the gas fees on behalf of the user. In a sponsored transaction, `sender()` still +returns the address of the user, so sender-based logic behaves the same whether or not the +transaction is sponsored. -Internally, it is represented as the `derive_id` function: +These getters are the complete public interface for reading the context. Other values, such as the +transaction's gas budget, are intentionally not exposed to the program. -```move -native fun derive_id(tx_hash: vector, ids_created: u64): address; -``` +## Mutability + +Some operations require the context to be passed as a mutable reference - `&mut TxContext`. The +most important of them is the creation of new objects: every object on Sui must have a globally +unique `UID`. Fresh UIDs are derived from the transaction digest and a counter of IDs created so +far in this transaction - the `ids_created` field. Each time a new UID is requested, the counter is +incremented, which guarantees that every derived address is unique. Because the counter has to +change, the operation requires a mutable reference to the context. + +We cover object creation in detail in the [UID and ID](./../storage/uid-and-id) section. ## Generating Unique Addresses -The underlying `derive_id` function can also be utilized in your program to generate unique -addresses. The function itself is not exposed, but a wrapper function `fresh_object_address` is -available in the `sui::tx_context` module. It may be useful if you need to generate a unique -identifier in your program. +The same derivation mechanism can be used directly in your program to generate unique addresses. +The `sui::tx_context` module exposes the `fresh_object_address` function for that, which may be +useful if an application needs a unique identifier - for example, to use as a key in a +[dynamic field](./dynamic-fields) or an offchain index. ```move module sui::tx_context; @@ -77,10 +101,30 @@ module sui::tx_context; /// Create an `address` that has not been used. As it is an object address, it will never /// occur as the address for a user. /// In other words, the generated address is a globally unique object ID. -public fun fresh_object_address(ctx: &mut TxContext): address { - let ids_created = ctx.ids_created; - let id = derive_id(*&ctx.tx_hash, ids_created); - ctx.ids_created = ids_created + 1; - id +public fun fresh_object_address(ctx: &mut TxContext): address; +``` + +## Transaction Context in Tests + +Since `TxContext` cannot be constructed in regular code, [tests](./../move-basics/testing) would +not be able to call any function that expects it. For this scenario the framework provides +test-only constructors: the simplest of them is `tx_context::dummy()`, which returns a context +with placeholder values. You will see it in code samples throughout this book: + +```move +#[test] +fun test_some_action() { + let ctx = &mut tx_context::dummy(); + // pass `ctx` into functions that expect `&mut TxContext` } ``` + +For tests that need specific values - a certain sender, epoch, or gas price - the module provides +more test-only constructors, as well as helpers to simulate epoch changes. They are covered in the +[Simulating Transaction Context](./../testing/transaction-context) section. For multi-transaction +scenarios and access to objects in storage, use the `sui::test_scenario` module, described in the +[Test Scenario](./../testing/test-scenario) section. + +## Further Reading + +- [sui::tx_context][tx-context-framework] module documentation. diff --git a/book/programmability/witness-pattern.md b/book/programmability/witness-pattern.md index d3123db6c..5b2e50606 100644 --- a/book/programmability/witness-pattern.md +++ b/book/programmability/witness-pattern.md @@ -4,9 +4,9 @@ description: "The Witness pattern in Move: prove type ownership through struct i # Pattern: Witness -Witness is a pattern of proving an existence by constructing a proof. In the context of programming, -witness is a way to prove a certain property of a system by providing a value that can only be -constructed if the property holds. +Witness is a pattern of proving a fact by constructing evidence of it. In the context of +programming, a witness is a way to prove a certain property of a system by providing a value that +can only be constructed if the property holds. ## Witness in Move @@ -16,40 +16,22 @@ constructing it. This is one of the most important patterns in Move, and it is w generic type instantiation and authorization. Practically speaking, for the witness to be used, there has to be a function that expects a witness -as an argument. In the example below it is the `new` function that expects a witness of the `T` type -to create a `Instance` instance. +as an argument. In the example below it is the `new` function that expects a witness of the `T` +type to create an `Instance`. -> It is often the case that the witness struct is not stored, and for that the function may require -> the [Drop](./../move-basics/drop-ability) ability for the type. +> The witness is usually discarded rather than stored, which is why such functions often require +> the witness type to have the [drop](./../move-basics/drop-ability) ability. -```move -module book::witness; - -/// A struct that requires a witness to be created. -public struct Instance { t: T } +```move file=packages/samples/sources/programmability/witness-pattern-2.move anchor=main -/// Create a new instance of `Instance` with the provided T. -public fun new(witness: T): Instance { - Instance { t: witness } -} ``` The only way to construct an `Instance` is to call the `new` function with an instance of the type `T`. This is a basic example of the witness pattern in Move. A module providing a witness often has a matching implementation, like the module `book::witness_source` below: -```move -module book::witness_source; - -use book::witness::{Self, Instance}; - -/// A struct used as a witness. -public struct W {} +```move file=packages/samples/sources/programmability/witness-pattern-3.move anchor=source -/// Create a new instance of `Instance`. -public fun new_instance(): Instance { - witness::new(W {}) -} ``` The instance of the struct `W` is passed into the `new_instance` function to create an @@ -65,12 +47,13 @@ to do so. module sui::balance; /// A Supply of T. Used for minting and burning. +/// Wrapped into a `TreasuryCap` in the `Coin` module. public struct Supply has store { value: u64, } -/// Create a new supply for type T with the provided witness. -public fun create_supply(_w: T): Supply { +/// Create a new supply for type T. +public fun create_supply(_: T): Supply { Supply { value: 0 } } @@ -93,21 +76,46 @@ the supply. ```move module sui::balance; -const EOverflow: u64 = 0; +const EOverflow: u64 = 1; -/// Storable balance. +/// Storable balance - an inner struct of a Coin type. +/// Can be used to store coins which don't need the key ability. public struct Balance has store { value: u64, } /// Increase supply by `value` and create a new `Balance` with this value. public fun increase_supply(self: &mut Supply, value: u64): Balance { - assert!(value < (std::u64::max_value!() - self.value), EOverflow); + assert!(value <= (std::u64::max_value!() - self.value), EOverflow); self.value = self.value + value; Balance { value } } ``` +This is how new currencies are typically created on Sui: the `TreasuryCap` - the +[capability](./capability) described earlier in this chapter - is a wrapper around the `Supply`, +instantiated with a witness. + +## Authorization with Witness + +Instantiating a type is not the only use for a witness: any function can require one, making the +call available only to the module that defines `T`. The module below implements a generic +`RegulatedCoin`, in which the privileged operations - `mint`, `burn`, and `transfer` - require a +witness, while the shared functionality - `join` - is available to everyone: + +```move file=packages/samples/sources/programmability/witness-pattern.move anchor=regulated_coin + +``` + +A module that defines a witness type and calls `mint` gets its own regulated currency: it alone +decides how - and whether - to expose minting, burning, and transfers of its coins, while the base +module implements the logic shared by all such currencies. + +This use of a witness is close to the [Capability](./capability) pattern, with an important +difference: a capability is an object, so it authorizes whoever owns it - an account; a witness can +only be constructed by the module defining it, so it authorizes code. Authorization with a witness +is decided at the time the code is written, requires no storage, and cannot be transferred. + ## One Time Witness While a struct can be created any number of times, there are cases where a struct should be @@ -115,6 +123,11 @@ guaranteed to be created only once. For this purpose, Sui provides the "One-Time special witness that can only be used once. We explain it in more detail in the [next section](./one-time-witness). +> The standard library also provides a ready-made form of this proof: the +> [Internal Permit](./../move-basics/internal-permit). An `internal::Permit` proves that the +> call was authorized by the module defining `T` - without the library having to design a custom +> witness type or require `drop` on `T` itself. + ## Summary - Witness is a pattern of proving a certain property by constructing a proof. diff --git a/book/programmability/wrapper-type-pattern.md b/book/programmability/wrapper-type-pattern.md index ab4ef605e..2e9f2978e 100644 --- a/book/programmability/wrapper-type-pattern.md +++ b/book/programmability/wrapper-type-pattern.md @@ -1,8 +1,10 @@ --- -description: "The Wrapper type pattern in Move: create restricted or extended versions of existing types by wrapping them in new structs." +description: + 'The Wrapper type pattern in Move: create restricted or extended versions of existing types by + wrapping them in new structs.' --- -# Pattern: Wrapper type +# Pattern: Wrapper Type Sometimes, there's a need to create a new type that behaves similarly to an existing type but with certain modifications or restrictions. For example, you might want to create a @@ -14,7 +16,8 @@ after they’ve been inserted. The wrapper type pattern is an effective way to a The wrapper type pattern is a design pattern in which you create a new type that wraps an existing type. The wrapper type is distinct from the original but can be converted to and from it. -Often, it is implemented as a positional struct with a single field. +Often, it is implemented as a [positional struct](./../move-basics/struct.md#positional-structs) +with a single field. ```move file=packages/samples/sources/programmability/wrapper-type-pattern.move anchor=main @@ -43,7 +46,7 @@ The wrapper type pattern offers several benefits: ## Disadvantages -The wrapper type pattern is powerful in two scenarios—when you want to limit the behavior of an +The wrapper type pattern is powerful in two scenarios - when you want to limit the behavior of an existing type while providing a custom interface to the same data structure, and when you want to extend the behavior of an existing type. However, it does have some limitations: @@ -56,6 +59,6 @@ extend the behavior of an existing type. However, it does have some limitations: The wrapper type pattern is very useful, particularly when used in conjunction with collection types, as demonstrated in the previous section. In the next section, we will cover -[Dynamic Fields](./dynamic-fields) — an important primitive that enables +[Dynamic Fields](./dynamic-fields) - an important primitive that enables [Dynamic Collections](./dynamic-collections), a way to store large collections of data in a more flexible, albeit more expensive, way. diff --git a/book/sidebar.yml b/book/sidebar.yml index 0c39edf38..552cac26b 100644 --- a/book/sidebar.yml +++ b/book/sidebar.yml @@ -58,6 +58,8 @@ bookSidebar: id: move-basics/address - label: Expressions id: move-basics/expression + - label: Functions + id: move-basics/function - label: Structs id: move-basics/struct - label: Abilities Introduction @@ -78,12 +80,6 @@ bookSidebar: id: move-basics/control-flow - label: Enums and Match id: move-basics/enum-and-match - - label: Constants - id: move-basics/constants - - label: Aborting Execution - id: move-basics/assert-and-abort - - label: Functions - id: move-basics/function - label: Struct Methods id: move-basics/struct-methods - label: Visibility Modifiers @@ -92,15 +88,20 @@ bookSidebar: id: move-basics/ownership-and-scope - label: 'Ability: Copy' id: move-basics/copy-ability + - label: Constants + id: move-basics/constants + - label: Aborting Execution + id: move-basics/assert-and-abort - label: References id: move-basics/references - label: Generics id: move-basics/generics + - label: Macro Functions + id: move-basics/macros + - label: Internal Permit + id: move-basics/internal-permit - label: Type Reflection id: move-basics/type-reflection - # - label: "5.26 Macros: Introduction" - # type: doc - # id: move-basics/type-parameters - label: Testing id: move-basics/testing - type: category @@ -117,7 +118,7 @@ bookSidebar: id: object/object-model - label: Ownership id: object/ownership - - label: Fast Path & Consensus + - label: Fast Path and Consensus id: object/fast-path-and-consensus - type: category label: Using Objects @@ -125,16 +126,14 @@ bookSidebar: id: storage/index type: doc items: - - label: Internal Constraint - id: storage/internal-constraint - label: 'Ability: Key' id: storage/key-ability - label: 'Ability: Store' id: storage/store-ability + - label: Internal Constraint + id: storage/internal-constraint - label: Storage Functions id: storage/storage-functions - # - label: Transfer Restrictions - # id: storage/transfer-restrictions - label: UID and ID id: storage/uid-and-id - label: Receiving as Object @@ -180,20 +179,20 @@ bookSidebar: id: programmability/display - label: Events id: programmability/events - # - label: Balance & Coin - # id: programmability/balance-and-coin - # type: doc + - label: Balance and Coin + id: programmability/balance-and-coin + - label: Address Balances + id: programmability/address-balances - label: 'Pattern: Hot Potato' id: programmability/hot-potato-pattern + - label: Package Upgrades + id: programmability/package-upgrades # - label: '8.18 Pattern: Request' # id: programmability/request-pattern # type: doc # - label: '8.19 Pattern: Object Capability' # id: programmability/object-capability-pattern # type: doc - # - label: '8.20 Package Upgrades' - # id: programmability/package-upgrades - # type: doc # - label: '8.21 Transaction Blocks' # id: programmability/transaction-block # type: doc @@ -203,9 +202,8 @@ bookSidebar: # - label: '8.23 Cryptography and Hashing' # id: programmability/cryptography-and-hashing # type: doc - # - label: '8.24 Randomness' - # id: programmability/cryptography-and-hashing - # type: doc + - label: Randomness + id: programmability/randomness - label: BCS id: programmability/bcs - type: category @@ -234,6 +232,8 @@ bookSidebar: # id: testing/writing-test-macros - label: Extending Modules id: testing/extend-foreign-module + - label: Running Lints + id: testing/linting - label: Coverage Reports id: testing/coverage - label: Gas Profiling @@ -246,6 +246,8 @@ bookSidebar: items: - label: Compilation Modes id: move-advanced/modes + - label: Entry Functions + id: move-advanced/entry-functions - type: category label: Guides enumerate: false @@ -260,6 +262,8 @@ bookSidebar: id: guides/better-error-handling - label: Code Quality Checklist id: guides/code-quality-checklist + - label: Using Move Registry + id: guides/using-move-registry # - label: Open-sourcing Libraries # id: guides/open-sourcing-libraries # - label: Creating an NFT Collection diff --git a/book/storage/index.md b/book/storage/index.md index 4ab4b47e4..4dbe45d86 100644 --- a/book/storage/index.md +++ b/book/storage/index.md @@ -4,13 +4,32 @@ description: "Learn how to use Sui objects in Move: storage abilities, transfer # Using Objects -In the [Object Model][object-model] chapter, we introduced the shift in the Move language from an -account-based model to an object-based one. This chapter takes a closer look at that model. You’ll -learn how objects work and how to put them to use in your Sui applications. +The [Object Model][object-model] chapter introduced objects conceptually: the unit of storage, +with an identity, an owner, and an ownership state that shapes execution. This chapter turns those +concepts into code. You will learn how to define an object type, how to create and destroy +objects, and how to move them between ownership states - transfer, freeze, and share. -If you haven’t read the [Object Model][object-model] chapter yet, we recommend starting there before -continuing. +The sections build on each other and are meant to be read in order: -[object-model]: ./../object +- [Ability: Key](./key-ability) - the ability that turns a struct into an object; +- [Ability: Store](./store-ability) - the ability that permits a type to be stored inside objects, + and controls who can operate on the object; +- [Sui Verifier: Internal Constraint](./internal-constraint) - the bytecode-level rule that + reserves critical operations for the module defining the type; +- [Storage Functions](./storage-functions) - the operations that place objects into storage: + transfer, freeze, and share; +- [UID and ID](./uid-and-id) - the identity of every object, and its lifecycle; +- [Receiving as Object](./transfer-to-object) - the mechanism that lets objects own other objects. + +> Two types from the [Sui Framework](./../programmability/sui-framework) appear in almost every +> example of this chapter: `UID` - the unique identifier stored in every object - and `TxContext` - +> a special value describing the current transaction, available to any function as its last +> argument. Both are covered in depth later ([UID and ID](./uid-and-id) in this chapter, +> [Transaction Context](./../programmability/transaction-context) in the next one); to get +> started, it is enough to know that `object::new(ctx)` uses the transaction context to produce a +> fresh, unique `UID`. - +If you haven’t read the [Object Model][object-model] chapter yet, we recommend starting there +before continuing. + +[object-model]: ./../object diff --git a/book/storage/internal-constraint.md b/book/storage/internal-constraint.md index 1d20204c6..0bbce8531 100644 --- a/book/storage/internal-constraint.md +++ b/book/storage/internal-constraint.md @@ -4,19 +4,24 @@ description: "The Sui Verifier internal constraint: why storage operations requi # Sui Verifier: Internal Constraint -The Sui Bytecode Verifier enforces a set of rules on Move bytecode to ensure the safety of critical -storage operations. One of these rules is the _internal constraint_. It requires that the caller of -a function with a type parameter `T` must be the _defining module_ of that type. In other words, T -must be _internal_ to the module making the call. - -This rule is not (yet) part of the Move language itself, which can make it feel opaque. Still, it’s -an important rule to understand, especially when working with storage-related operations on Sui. - -Let’s look at an example from the [Sui Framework][sui-framework]. The emit function in the -[`sui::event`][event] module requires its type parameter `T` to be _internal_ to the caller: +In the [Internal Permit](./../move-basics/internal-permit) section, we introduced _internal type +parameters_: type parameters that only accept types defined in the calling module. There, +`std::internal::permit()` used the rule to produce a proof value. On Sui, the same rule +protects a handful of critical framework functions _directly_ - no permit value involved - and the +component enforcing it is the _Sui Verifier_. + +The Sui Verifier is a set of bytecode-level checks that run on top of regular Move verification, +both at compilation and when a package is published onchain. Most of its rules formalize what +this chapter has already described - such as the `id: UID` first-field requirement from the +[key ability](./key-ability) section. The _internal constraint_ is the rule that matters most for +what comes next: a function marked with it can only be called with a type parameter `T` that is +_internal_ - defined in the calling module. + +Let's look at the classic example - the `emit` function from the `sui::event` module (covered in +detail in the [Events](./../programmability/events) section), which requires its type parameter to +be internal to the caller: ```move -// An actual example of a function that enforces `internal` on `T`. module sui::event; // Sui Verifier will emit an error at compilation if this function is @@ -24,44 +29,44 @@ module sui::event; public native fun emit(event: T); ``` -Here’s a correct call to `emit`. The type `A` is defined inside the module `exercise_internal`, so -it’s internal and valid: - -```move -// Defines type `A`. -module book::exercise_internal; - -use sui::event; +Here is a correct call to `emit`. The type `A` is defined in the same module that makes the call, +so the constraint is satisfied: -/// Type defined in this module, so it's internal here. -public struct A has copy, drop {} +```move file=packages/samples/sources/storage/internal-constraint.move anchor=main -// This works because `A` is defined locally. -public fun call_internal() { - event::emit(A {}) -} ``` -But if you try to call `emit` with a type defined elsewhere, the verifier rejects it. For example, -this function, when added to the same module, fails because it tries to use the `TypeName` type from -the [Standard Library][move-stdlib]: +But calling `emit` with a type defined elsewhere - for example, the `TypeName` type from the +[Standard Library](./../move-basics/standard-library) - is rejected: ```move // This one fails! public fun call_foreign_fail() { use std::type_name; - event::emit(type_name::get()); - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid event. + event::emit(type_name::with_defining_ids()); + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid event. // Error: `sui::event::emit` must be called with a type // defined in the current module. } ``` -Internal constraints only apply to certain functions in the [Sui Framework][sui-framework]. We’ll -return to this concept several times throughout the book. +The effect is the same authority rule we established for [struct fields](./../move-basics/struct#field-visibility) +and saw generalized by `Permit`: the module that defines a type decides what happens with it. For +`emit`, it means only the defining module can emit events of its type; for the +[storage functions](./storage-functions) in the next section, it means the defining module fully +governs how its objects enter storage - unless it opts out by adding the +[`store`](./store-ability) ability. + +## Summary + +- The Sui Verifier is a set of bytecode-level rules checked at compilation and on publish. +- The internal constraint restricts a function's type parameter to types defined in the calling + module. +- It applies to a handful of critical framework functions: `event::emit`, and the restricted + storage functions covered in the [next section](./storage-functions). + +## Further Reading -[sui-framework]: ./../programmability/sui-framework.md -[move-stdlib]: ./../move-basics/standard-library.md -[event]: ./../programmability/events.md -[reflection]: ./../move-basics/type-reflection.md +- [Internal Permit](./../move-basics/internal-permit) - the same rule, available to any library + through `std::internal`. diff --git a/book/storage/key-ability.md b/book/storage/key-ability.md index 0d85d3a05..9e5d8f686 100644 --- a/book/storage/key-ability.md +++ b/book/storage/key-ability.md @@ -4,68 +4,66 @@ description: "The key ability in Move makes a struct an object that can be store # Ability: Key -In the [Basic Syntax][basic-syntax] chapter, we already covered two out of four abilities: -[Drop][drop-ability] and [Copy][copy-ability]. They affect the behavior of a value in a scope and -are not directly related to storage. Now it is time to cover the `key` ability, which allows a -struct to be _stored_. - -Historically, the `key` ability was created to mark a type as a _key in storage_. A type with the -`key` ability could be stored at the top level in global storage and could be _owned_ by an account -or address. With the introduction of the [Object Model][object-model], the `key` ability became the +In the [Move Basics][basic-syntax] chapter, we covered two of the four abilities - +[Drop][drop-ability] and [Copy][copy-ability]. They affect the behavior of a value within a scope, +and are not related to storage. Now it is time to cover the `key` ability - the ability that +allows a struct to become a unit of storage. + +Historically, the `key` ability was created to mark a type as a _key in global storage_. A type +with the `key` ability could be stored at the top level and could be _owned_ by an account or +address. With the introduction of the [Object Model][object-model], the `key` ability became the defining ability for _objects_. -> Later in the book, we will refer to any struct with the `key` ability as an Object. +> Throughout the book, we refer to any struct with the `key` ability as an _object_. ## Object Definition -A struct with the `key` ability is considered _an object_ and can be used in storage functions. The -Sui Verifier requires the first field of the struct to be named `id` and to have the type `UID`. -Additionally, it requires all fields to have the `store` ability — we’ll explore it in detail [on -the next page][store-ability]. - -```move -/// `User` object definition. -public struct User has key { - id: UID, // required by Sui Bytecode Verifier - name: String, // field types must have `store` -} - -/// Creates a new instance of the `User` type. -/// Uses the special struct `TxContext` to derive a Unique ID (UID). -public fun new(name: String, ctx: &mut TxContext): User { - User { - id: object::new(ctx), // creates a new UID - name, - } -} +A struct with the `key` ability is an object, and can be used in +[storage functions](./storage-functions). Two layers of rules apply to its definition: + +- The Move language requires every field of a `key` struct to have the [`store`][store-ability] + ability - we explore `store` on the next page; +- The Sui Verifier additionally requires the first field of the struct to be named `id` and to + have the type `UID`. + +```move file=packages/samples/sources/storage/key-ability.move anchor=user + ``` +The `new` function creates the object. A fresh `UID` can only be produced by `object::new`, which +takes a mutable reference to the [transaction context](./../programmability/transaction-context) - +so every newly created object gets an identifier that has never existed on the network before. We +look closer at the `UID` type and its guarantees in the [UID and ID](./uid-and-id) section. + ## Relation to `copy` and `drop` -`UID` is a type that does not have the [`drop`][drop-ability] or [`copy`][copy-ability] abilities. -Since it is required as a field of any type with the `key` ability, this means that types with `key` -can never have `drop` or `copy`. +`UID` is a type that has neither [`drop`][drop-ability] nor [`copy`][copy-ability]. Since every +object is required to have a `UID` field, and a struct can only have an ability its fields +support, this means that objects can never have `drop` or `copy`. Every object is +non-discardable and non-copyable by construction - which is exactly what the +[asset properties](./../object/digital-assets) demand. This property can be leveraged in [ability constraints][generics]: requiring `drop` or `copy` -automatically excludes `key`, and conversely, requiring `key` excludes types with `drop` or `copy`. +automatically excludes objects, and conversely, requiring `key` excludes types with `drop` or +`copy`. ## Types with the `key` Ability -Due to the `UID` requirement for types with `key`, none of the native types in Move can have the -`key` ability, nor can any of the types in the [Standard Library][standard-library]. The `key` -ability is present only in some [Sui Framework][sui-framework] types and in custom types. +Due to the `UID` requirement, none of the native types in Move can have the `key` ability, nor can +any of the types in the [Standard Library][standard-library]. The `key` ability is present only in +some [Sui Framework][sui-framework] types and in custom types. ## Summary -- The `key` ability defines an object -- The first field of an object must be `id` with type `UID` -- Fields of a `key` type must the have [`store`][store-ability] ability -- Objects cannot have [`drop`][drop-ability] or [`copy`][copy-ability] +- The `key` ability defines an object. +- The Sui Verifier requires the first field of an object to be `id` of type `UID`. +- The Move language requires all fields of a `key` struct to have [`store`][store-ability]. +- Objects can never have [`drop`][drop-ability] or [`copy`][copy-ability]. ## Next Steps -The `key` ability defines objects in Move and forces the fields to have `store`. In the next section -we cover the `store` ability to later explain how [storage operations](./storage-functions.md) work. +The `key` ability defines objects and forces all fields to have `store`. In the next section, we +look at the `store` ability itself - and at the second, less obvious role it plays for objects. ## Further Reading @@ -73,7 +71,7 @@ we cover the `store` ability to later explain how [storage operations](./storage [drop-ability]: ./../move-basics/drop-ability [copy-ability]: ./../move-basics/copy-ability -[store-ability]: ./store-ability.md +[store-ability]: ./store-ability [generics]: ./../move-basics/generics#constraints-on-type-parameters [sui-framework]: ./../programmability/sui-framework [standard-library]: ./../move-basics/standard-library diff --git a/book/storage/storage-functions.md b/book/storage/storage-functions.md index db793916e..d1b735dd0 100644 --- a/book/storage/storage-functions.md +++ b/book/storage/storage-functions.md @@ -4,90 +4,107 @@ description: "Sui storage functions: transfer, share, freeze, and receive object # Storage Functions -The module that defines main storage operations is `sui::transfer`. It is implicitly imported in all -packages that depend on the [Sui Framework](./../programmability/sui-framework), so, like other -implicitly imported modules (e.g. `std::option` or `std::vector`), it does not require adding a use -statement. +The module that defines the main storage operations is `sui::transfer`. It is implicitly imported +in all packages that depend on the [Sui Framework](./../programmability/sui-framework), so, like +other implicitly imported modules (e.g. `std::option` or `std::vector`), it does not require a +`use` statement. -> For quick reference, [Appendix C: Transfer Functions](./../appendix/transfer-functions.md) -> contains a list of all storage functions and object states. +> For quick reference, [Appendix C: Transfer Functions](./../appendix/transfer-functions) contains +> a list of all storage functions and object states. ## Overview -The `transfer` module provides functions to perform storage operations for each of the -[ownership types](./../object/ownership). +The `transfer` module provides a function for each of the +[ownership states](./../object/ownership) an object can be placed into: -1. [Transfer](#transfer) - send an object to an address, put it into _address owned_ state; -2. [Freeze](#freeze) - put an object into _immutable_ state, so it becomes a _public constant_ and - can never change. -3. [Share](#share) - put an object into a _shared_ state, so it is available to everyone; +1. [Transfer](#transfer) - send an object to an address, putting it into the _address owned_ + state; +2. [Freeze](#freeze) - put an object into the _immutable_ state, making it a _public constant_ + that can never change; +3. [Share](#share) - put an object into the _shared_ state, available to everyone. -The `transfer` module is a go-to for most of the storage operations, except a special case with -[Dynamic Fields](./../programmability/dynamic-fields) which are covered in the next chapter. +The `transfer` module is the go-to for most storage operations. Two special cases are covered +separately: [Dynamic Fields](./../programmability/dynamic-fields) - attaching data to objects - in +the next chapter, and [receiving objects sent to other objects](./transfer-to-object) at the end +of this one. ## Ownership and References: a Quick Recap -In the [Ownership and Scope](./../move-basics/ownership-and-scope) and -[References](./../move-basics/references) chapters, we covered the basics of ownership and -references in Move. It is important that you understand these concepts when using storage functions. -Here is a quick recap of the most important points: +Storage functions build directly on the semantics from the +[Ownership and Scope](./../move-basics/ownership-and-scope) and +[References](./../move-basics/references) sections. All of them take the object _by value_: the +object is moved into the function, the caller loses it - and, as we are about to see, it ends up +in storage, in its new state. This is the resource model at work: an object is never copied into +storage, it is _placed_ there, and the previous owner provably gives it up. A function that only +needs to read or update an object, on the other hand, takes it by reference (`&T` or `&mut T`) and +leaves the ownership state untouched. -- The _move_ semantics in Move means that the value is _moved_ from one scope to another. In other - words, if an instance of a type is passed to a function _by value_, it is _moved_ to the function - scope and can't be accessed in the caller scope anymore. -- To maintain the ownership of the value, you can pass it _by reference_. Either by _immutable - reference_ `&T` or _mutable reference_ `&mut T`. Then the value is _borrowed_ and can be accessed - in the callee scope, however the owner stays the same. +## Internal Rule in Transfer Functions -```move -/// Moved by value -public fun take(value: T) { /* value is moved here! */ abort } +Each storage operation comes in two forms: _internal_ and _public_. The internal functions - +`transfer`, `share_object`, `freeze_object` - enforce the +[internal constraint](./internal-constraint) from the previous section: they can only be called in +the module that defines the type of the object. The public versions - prefixed with `public_` - +lift that restriction, but require the type to have [`store`](./store-ability) in addition to +`key`: -/// For immutable reference, value stays in parent scope. -public fun borrow(value: &T) { /* value is borrowed here! can be read */ abort } +```move +/// Internal: can only be called in the module that defines `T`. +public fun transfer(obj: T, recipient: address); -/// For mutable reference, value stays in parent scope but can be mutated. -public fun borrow_mut(value: &mut T) { /* value is mutably borrowed here! */ abort } +/// Public: callable from any module, but requires `T` to have `store`. +public fun public_transfer(obj: T, recipient: address); ``` - - -## Internal Rule in Transfer Functions +Together, the two forms implement the rule we previewed in the +[store ability](./store-ability#relation-to-key) section: storage of a `key`-only object is fully +governed by its defining module, while `store` opens the object up to storage operations performed +by any module - and by the owner directly, in a transaction. -Storage operations can only be performed on objects, and come in two forms: _internal_ and _public_. -Internal, or sometimes called _restricted_, transfer functions can be performed on [`key`][key]-only -types, and - comes with the name - enforce [internal constraint](./internal-constraint.md). Public -versions can be called on any object that has `key` and [`store`][store]. Hence, `key`-only types' -storage is fully governed by their defining module, and `store` allows calling public transfer -functions in other modules. +To see every combination at once, suppose module `book::transfer_a` defines two objects - `ObjectK` +with `key` and `ObjectKS` with `key + store` - and module `book::transfer_b` tries to transfer +them: ```move -/// T: internal, can be called only in the module which defines the `T`. -public fun transfer(obj: T, recipient: address); +/// Imports the `ObjectK` and `ObjectKS` types from `transfer_a` and attempts +/// to implement different `transfer` functions for them. +module book::transfer_b; -/// No requirement for `T` to be internal to the caller, but requires `store`. -public fun public_transfer(obj: T, recipient: address); -``` +// The types are not internal to this module! +use book::transfer_a::{ObjectK, ObjectKS}; + +// Fails! `ObjectK` is not internal to this module. +public fun transfer_k(k: ObjectK, to: address) { + transfer::transfer(k, to); +} + +// Fails! `ObjectKS` is not internal to this module either - +// `store` does not affect the internal functions. +public fun transfer_ks(ks: ObjectKS, to: address) { + transfer::transfer(ks, to); +} -In the example above, the `transfer` function can only be called from the module that defines the -`T`, and has a type constraint `T: key`. While `public_transfer` - clearly indicated in the name - -can be called from any module, but requires `T` to have `key` and `store`. +// Fails! `public_transfer` requires `store`, and `ObjectK` does not have it. +public fun public_transfer_k(k: ObjectK, to: address) { + transfer::public_transfer(k, to); +} + +// Works! `ObjectKS` has `store`, and the function is public. +public fun public_transfer_ks(ks: ObjectKS, to: address) { + transfer::public_transfer(ks, to); +} +``` -Knowing this rule is critical for understanding application design in Move. Choice between making -object publicly transferable (`key` and `store`) and keeping it internal (`key`-only) may -drastically affect application logic and further development. +The same matrix applies to `share_object`/`public_share_object` and +`freeze_object`/`public_freeze_object`. Knowing this rule is critical for understanding +application design in Move: the choice between making an object publicly transferable +(`key + store`) and keeping it internal (`key`-only) drastically affects the guarantees the +application can give about its assets. ## Transfer -The `transfer::transfer` function is a function used to transfer an object to an address. Its -signature is as follows, only accepts a type with the [`key` ability](./key-ability.md) and an -[address](./../move-basics/address.md) of the recipient. Note that the object is passed into the -function _by value_, therefore it is _moved_ to the function scope and then moved to the recipient -address. +The `transfer::transfer` function sends an object to an address, making that address its sole +owner: ```move module sui::transfer; @@ -99,246 +116,157 @@ public fun transfer(obj: T, recipient: address); public fun public_transfer(obj: T, recipient: address); ``` -### Transfer Example - -In the following example, you can see how it can be used in a module that defines and sends an -object to the transaction sender. - -```move -module book::transfer_to_sender; - -/// A struct with `key` is an object. The first field is `id: UID`! -public struct AdminCap has key { id: UID } - -/// `init` function is a special function that is called when the module -/// is published. It is a good place to do a setup for an application. -fun init(ctx: &mut TxContext) { - // Create a new `AdminCap` object, in this scope. - let admin_cap = AdminCap { id: object::new(ctx) }; +In the following example, a module creates an object representing the application's admin rights +and sends it to the publisher of the module: - // Transfer the object to the transaction sender. - transfer::transfer(admin_cap, ctx.sender()); -} +```move file=packages/samples/sources/storage/storage-functions.move anchor=admin_cap -/// Transfers the `AdminCap` object to the `recipient`. Thus, the recipient -/// becomes the owner of the object, and only they can access it. -public fun transfer_admin_cap(cap: AdminCap, recipient: address) { - transfer::transfer(cap, recipient); -} ``` -When the module is published, the `init` function will get called, and the `AdminCap` object which -we created in it will be _transferred_ to the transaction sender. The `ctx.sender()` function -returns the sender address for the current transaction. +When the module is published, the `init` function is called, and the `AdminCap` object created in +it is _transferred_ to the transaction sender - `ctx.sender()` returns the sender address of the +current transaction. (The `init` function is covered in detail in the +[Module Initializer](./../programmability/module-initializer) section.) -Once the `AdminCap` has been transferred to the sender, for example, to `0xa11ce`, the sender, and -only the sender, will be able to access the object. This type of ownership is called _address -ownership_. +From that point, say the sender was `0xa11ce`, the object is in the _address owned_ state: only +`0xa11ce` can use it in a transaction - by reference or by value, including transferring it +further with the `transfer_admin_cap` function above. -> Address owned objects are a subject to _true ownership_ - only owner address can access them. This -> is a fundamental concept in the Sui storage model. +> Address-owned objects are subject to _true ownership_ - only the owner address can access them. +> This is a fundamental concept in the Sui storage model, introduced in the +> [Ownership](./../object/ownership#account-owner-or-single-owner) section. ### Public Transfer -Let's extend the example with a function that uses `AdminCap` to authorize a mint of a new object -and its transfer to an address: +Let's extend the example with a function that uses the `AdminCap` to authorize minting of a new +object and transferring it to any address: + +```move file=packages/samples/sources/storage/storage-functions.move anchor=mint_and_transfer -```move -/// Some `Gift` object that the admin can `mint_and_transfer` to an address. -public struct Gift has key, store { id: UID } - -/// Creates a new `Gift` object and transfers it to the `recipient`. -public fun mint_and_transfer( - _: &AdminCap, recipient: address, ctx: &mut TxContext -) { - let gift = Gift { id: object::new(ctx) }; - transfer::public_transfer(gift, recipient); -} ``` -The `mint_and_transfer` function is a _public_ function that "could" be called by anyone, but it -requires a reference to an `AdminCap` as the first argument. Without it, the function will not be -callable. This is a simple and very explicit way to restrict access to privileged functions called -_[Capability](./../programmability/capability)_. Because the `AdminCap` object is _address owned_, -only `0xa11ce` will be able to call the `mint_and_transfer` function. +The `mint_and_transfer` function "could" be called by anyone - it is public - but it requires an +`AdminCap` reference as its first argument, and the `AdminCap` object is owned by `0xa11ce` +exclusively. So in practice only `0xa11ce` can mint. This simple and explicit way of gating access +to a function is the _[Capability pattern](./../programmability/capability)_, one of the +cornerstones of Sui application design. -Unlike `AdminCap` where we restricted transferability as well as usability by adding only `key` -ability, `Gift` has a `key` and `store` combination, which means, that whoever owns a `Gift` can -freely call `transfer::public_transfer` and send it to anyone else. Without `store`, in our current -implementation, `Gift` would've been _"soulbound"_ meaning that the happy owner of the `Gift` would -not be able to do anything with it. +Note the difference between the two objects in this example. `AdminCap` is `key`-only: the module +keeps full control over it, and if the module exposed no `transfer_admin_cap` function, the admin +rights would be _soulbound_ - impossible to give away. `Gift` has `key + store`: it is sent with +`public_transfer`, and whoever owns a `Gift` can freely transfer it onward in their own +transactions, without any help from this module. ### Quick Recap -- `transfer` function is used to send an object to an address; -- The object becomes _address owned_ and can only be accessed by the recipient; -- _Address owned_ object can be used by reference or by value, including being transferred to - another address; -- _Public_ version of it is `public_transfer` and requires `store` -- Functions can be gated by requiring an object to be passed as an argument, creating a - _capability_. +- `transfer` sends an object to an address, making it _address owned_; +- Only the owner can use an address-owned object - by reference or by value; +- Requiring a `key`-only object as an argument gates a function to the object's owner - the + _Capability_ pattern; +- `public_transfer` is the public form: callable anywhere, requires `key + store`. ## Freeze -The `transfer::freeze_object` function is a function that is used to put an object into an -_immutable_ state. Once an object is _frozen_, it can never change, and it can be accessed by anyone -by immutable reference. - -The function signature is as follows, only accepts a type with the [`key` ability](./key-ability). -Just like all other storage functions, it takes the object _by value_. The public version of this -function is `public_freeze_object`, and requires `T` to have `store`. +The `transfer::freeze_object` function puts an object into the _immutable_ state. Once an object +is _frozen_, it can never change, and anyone can access it by immutable reference: ```move module sui::transfer; -// Make object immutable and allow anyone to read it. +// Make the object immutable and allow anyone to read it. public fun freeze_object(obj: T); // Public version of the `freeze_object` function. public fun public_freeze_object(obj: T); ``` -Let's extend the previous example and add a function that allows the admin to create a `Config` -object and freeze it: +Let's extend the running example with a `Config` object that the admin creates and freezes: -```move -/// Some `Config` object that the admin can `create_and_freeze`. -public struct Config has key { - id: UID, - message: String -} +```move file=packages/samples/sources/storage/storage-functions.move anchor=config -/// Creates a new `Config` object and freezes it. -public fun create_and_freeze( - _: &AdminCap, - message: String, - ctx: &mut TxContext -) { - let config = Config { - id: object::new(ctx), - message - }; - - // Freeze the object so it becomes immutable. - transfer::freeze_object(config); -} - -/// Returns the message from the `Config` object. -/// Can access the object by immutable reference! -public fun message(c: &Config): String { c.message } ``` -Config is an object that has a `message` field, and the `create_and_freeze` function creates a new -`Config` and freezes it. Once the object is frozen, it can be accessed by anyone by immutable -reference. The `message` function is a public function that returns the message from the `Config` -object. Config is now publicly available by its ID, and the message can be read by anyone. +Once `create_and_freeze` is called, the `Config` becomes publicly available by its ID, and the +`message` function can be called by anyone - on a frozen object, immutable references are free for +the taking. -> Function definitions are not connected to object's state. It is possible to define a function that -> takes a mutable reference to a type that is always frozen. However, it will not be callable on a -> frozen object. +Function definitions are not tied to the object's state, so it is perfectly legal to _define_ +functions that take a frozen type by mutable reference or by value - they just cannot be _called_ +with a frozen object: -The `message` function in the example above can be called on an immutable `Config` object. However, -two functions shown below are not callable on a frozen object: +```move file=packages/samples/sources/storage/storage-functions.move anchor=frozen_uncallable -```move -// === These can't be called on a frozen object! === +``` -/// The function can be defined, but it won't be callable on a frozen object. -/// Only immutable references are allowed. -public fun message_mut(c: &mut Config): &mut String { &mut c.message } +The same applies to `delete_config`, defined below in the [Share](#share) section: it takes +`Config` by value, and a frozen `Config` can never be passed to it. Freezing is _permanent_: +a frozen object cannot be modified, transferred, deleted - or unfrozen. -/// Deletes the `Config` object, takes it by value. -/// Can't be called on a frozen object! -public fun delete_config(c: Config) { - let Config { id, message: _ } = c; - id.delete() -} -``` +### Owned → Frozen -To summarize: +Since the `freeze_object` signature accepts any object by value, it can receive an object created +in the same scope, but also an object the sender _owns_. Single Owner → Immutable conversion is +possible! For example, an owner of a `Gift` can decide to preserve it forever: -- `transfer::freeze_object` function is used to put an object into an _immutable_ state; -- Once an object is _frozen_, it can never be changed, deleted or transferred, and it can be - accessed by anyone by immutable reference; -- _Public_ version of the `freeze_object` function is `public_freeze_object` and requires the `T` to - have `store`. +```move file=packages/samples/sources/storage/storage-functions.move anchor=freeze_gift -## Owned -> Frozen +``` -Since the `transfer::freeze_object` signature accepts any type with the `key` ability, it can take -an object that was created in the same scope, but it can also take an object that was owned by an -account. This means that the `freeze_object` function can be used to _freeze_ an object that was -_transferred_ to the sender. For security concerns, we would not want to freeze the `AdminCap` -object - it would be a security risk, since anyone would be able to access it. However, we can -freeze the `Gift` object that was minted and transferred to the recipient: +For obvious security reasons, this is also something to keep in mind in the other direction: an +`AdminCap` must never be frozen - a frozen capability would be readable by everyone, and every +function gated by `&AdminCap` would become callable by anyone. Which, once again, shows the value +of the `key`-only pattern: `AdminCap` has no `store`, so external code has no way to freeze it, +and the module simply does not expose a freezing function. -> Single Owner -> Immutable conversion is possible! +### Quick Recap -```move -/// Freezes the `Gift` object so it becomes immutable. -/// Gift has `key` and `store`, so `public_freeze_object` can be used! -public fun freeze_gift(gift: Gift) { - transfer::public_freeze_object(gift); -} -``` +- `freeze_object` puts an object into the _immutable_ state - permanently; +- A frozen object is readable by anyone via immutable reference, and can never be modified, + transferred, or deleted; +- Owned objects can be frozen - including by their owner in a transaction, if the object has + `store`; +- `public_freeze_object` is the public form: callable anywhere, requires `key + store`. ## Share -The `transfer::share_object` function is a function used to put an object into a _shared_ state. -Once an object is _shared_, it can be accessed by anyone by a mutable reference (hence, immutable -too). The function signature is as follows, only accepts a type with the -[`key` ability](./key-ability): +The `transfer::share_object` function puts an object into the _shared_ state, where anyone can +access it by mutable (and hence also immutable) reference: ```move module sui::transfer; -/// Put an object to a Shared state - can be accessed mutably and immutably. +/// Put the object into the shared state - accessible to everyone. public fun share_object(obj: T); -/// Public version of `share_object` function. +/// Public version of the `share_object` function. public fun public_share_object(obj: T); ``` -Like other transfer functions, `share_object` has its _public_ version which requires `T` to have -`store`. +```move file=packages/samples/sources/storage/storage-functions.move anchor=share -Once an object is _shared_, it is publicly available as a mutable reference. +``` -## Special Case: Shared Object Deletion +Unlike `freeze_object`, which accepts both new and owned objects, `share_object` has a runtime +restriction: **only an object created in the same transaction can be shared**. An attempt to share +an object that already exists in the owned state aborts with `ESharedNonNewObject`. There is no +Owned → Shared conversion: the decision to make an object shared has to be made at its creation. +And like freezing, sharing is one-way - once shared, an object stays shared for the rest of its +life, with a single exception, which we look at next. -While the shared object can't normally be taken by value, there is one special case where it can - -if the function that takes it deletes the object. This is a special case in the Sui storage model, -and it is used to allow the deletion of shared objects. To show how it works, we will create a -function that creates and shares a Config object and then another one that deletes it: +### Special Case: Shared Object Deletion -```move -/// Creates a new `Config` object and shares it. -public fun create_and_share(message: String, ctx: &mut TxContext) { - let config = Config { - id: object::new(ctx), - message - }; - - // Share the object so it becomes shared. - transfer::share_object(config); -} -``` +While a shared object can't normally be taken by value, there is one special case where it can - +if the function that takes it _deletes_ it. This is a special case in the Sui storage model, made +to allow cleaning up shared state. Let's add a function that deletes the shared `Config`: -The `create_and_share` function creates a new `Config` object and shares it. The object is now -publicly available as a mutable reference. Let's create a function that deletes the shared object: +```move file=packages/samples/sources/storage/storage-functions.move anchor=delete_shared -```move -/// Deletes the `Config` object, takes it by value. -/// Can be called on a shared object! -public fun delete_config(c: Config) { - let Config { id, message: _ } = c; - id.delete() -} ``` -The `delete_config` function takes the `Config` object by value and deletes it, and the Sui Verifier -would allow this call. However, if the function returned the `Config` object back or attempted to -`freeze` or `transfer` it, the Sui Verifier would reject the transaction. +The `delete_config` function takes the `Config` by value and destroys it completely - unpacking +the struct and deleting the `UID` - and the Sui Verifier allows this call. However, if the +function returned the `Config`, or attempted to `transfer` or `freeze` it, the transaction would +be rejected: ```move // Won't work! @@ -347,21 +275,48 @@ public fun transfer_shared(c: Config, to: address) { } ``` -To summarize: +The rule: a shared object taken by value must be deleted in the same transaction. + +### Quick Recap + +- `share_object` puts an object into the _shared_ state, accessible to everyone by mutable + reference; +- Only an object created in the same transaction can be shared - there is no Owned → Shared + conversion; +- Sharing is permanent, with one exception: a shared object may be taken by value in order to be + _deleted_; +- `public_share_object` is the public form: callable anywhere, requires `key + store`. + +## Party Transfer + +The `transfer` module also provides `party_transfer` and `public_party_transfer`, which place an +object into the [party state](./../object/ownership#party-objects) - single-owner access with +consensus ordering. Party objects are an advanced, newer feature, and we leave them out of the +running example; the function signatures are listed in +[Appendix C](./../appendix/transfer-functions#party), and the details are covered in the +[`sui::party`](https://docs.sui.io/references/framework/sui/party) module documentation. -- `share_object` function is used to put an object into a _shared_ state; -- Once an object is _shared_, it can be accessed by anyone by a mutable reference; -- Shared objects can be deleted, but they can't be transferred or frozen; -- _Public_ version of the `share_object` function is `public_share_object` and requires the `T` to - have `store`. +## Summary + +| Function | Resulting state | Reversible? | Public version | +| ---------------- | --------------- | ------------------------------------------------------- | ----------------------- | +| `transfer` | Address owned | Yes - transfer away | `public_transfer` | +| `freeze_object` | Immutable | No | `public_freeze_object` | +| `share_object` | Shared | Only by deletion | `public_share_object` | +| `party_transfer` | Party | [Depends on permissions](./../appendix/transfer-functions#party) | `public_party_transfer` | + +- Every storage function takes the object _by value_ - placing an object into storage consumes it; +- Internal versions require the type to be defined in the calling module; `public_*` versions + require `store` instead. ## Next Steps -Now that you know main features of the `transfer` module, you can start building more complex -applications on Sui that involve storage operations. In the next chapter, we will cover the -[Store Ability](./store-ability) which allows storing data inside objects and relaxes transfer -restrictions which we barely touched on here. And after that we will cover the -[UID and ID](./uid-and-id) types which are the most important types in the Sui storage model. +Now that you know the main features of the `transfer` module, you can start building applications +that involve storage operations. In the next section we cover the [UID and ID](./uid-and-id) +types - the identity of every object - and after that, [Receiving as Object](./transfer-to-object), +the mechanism behind objects owning other objects. + +## Further Reading -[key]: ./key-ability.md -[store]: ./store-ability.md +- [`sui::transfer`](https://docs.sui.io/references/framework/sui/transfer) module documentation. +- [Appendix C: Transfer Functions](./../appendix/transfer-functions). diff --git a/book/storage/store-ability.md b/book/storage/store-ability.md index 5db1835d5..9b001a165 100644 --- a/book/storage/store-ability.md +++ b/book/storage/store-ability.md @@ -4,66 +4,70 @@ description: "The store ability in Move allows types to be used as fields in obj # Ability: Store -The [`key` ability][key-ability] requires all fields to have `store`, which defines what the `store` -ability means: it is the ability to serve as a field of an Object. A struct with -[`copy`][copy-ability] or [`drop`][drop-ability] but without `store` can never be _stored_. A type -with `key` but without `store` cannot be wrapped - used as a field—in another object, and is -constrained to always remain at the top level. +The [`key` ability][key-ability] requires all fields to have `store`, and that requirement is the +best way to understand what `store` means: it is the ability to be _stored_ - to end up inside an +object in the blockchain state. A struct with [`copy`][copy-ability] or [`drop`][drop-ability] but +without `store` can live only during the transaction that creates it; it can never be persisted. ## Definition -The `store` ability allows a type to be used as a field in a struct with the `key` ability. - -```move -// hidden-block-start -use std::string::String; - -// hidden-block-end -/// Extra metadata with `store`; all fields must have `store` as well! -public struct Metadata has store { - bio: String, -} - -/// An object for a single user record. -public struct User has key { - id: UID, - name: String, // String has `store` - age: u8, // All integers have `store` - metadata: Metadata, // Another type with the `store` ability -} +The `store` ability allows a type to be used as a field of a struct with the `key` ability - +directly, or nested any number of levels deep. Like with other abilities, the rule applies +recursively: a struct can only have `store` if all of its fields have `store`. + +```move file=packages/samples/sources/storage/store-ability.move anchor=definition + ``` ## Relation to `copy` and `drop` -All three non-`key` abilities can be used in any combination. +`store` is independent of `copy` and `drop`: the three non-`key` abilities can be combined freely, +and none of them implies another. A type may be copyable but not storable, storable but neither +copyable nor droppable, and so on - each combination is valid and has its uses. ## Relation to `key` -An object with the `store` ability can be _stored_ in other objects. +An _object_ can also have the `store` ability, and for objects it plays a double role: + +- An object with `store` can be _wrapped_: used as a field of another object. An object without + `store` is constrained to always remain at the top level of storage. +- `store` acts as a _public_ modifier on the object: it permits calling the public + [storage functions](./storage-functions) - `public_transfer`, `public_share_object`, and + `public_freeze_object` - from _any_ module. Without `store`, storage operations on the object + are reserved for its defining module, which keeps full control over how the object moves. -> While not a language or verifier feature, `store` acts as a _public_ modifier on a struct, -> allowing calling _public_ [transfer functions](./storage-functions.md) which do not have an -> [internal constraint](./internal-constraint.md). +The second role is not a language feature but a convention of the [Sui Framework][sui-framework], +enforced through the [internal constraint](./internal-constraint) - the topic of the next section. +Whether to give an object `store` is one of the most consequential design decisions in a Sui +application, and we return to it in [Storage Functions](./storage-functions#internal-rule-in-transfer-functions). ## Types with the `store` Ability All native types (except references) in Move have the `store` ability. This includes: -- [bool](./../move-basics/primitive-types.md#booleans) -- [unsigned integers](./../move-basics/primitive-types.md#integer-types) -- [vector](./../move-basics/vector.md) -- [address](./../move-basics/address.md) +- [bool](./../move-basics/primitive-types#booleans) +- [unsigned integers](./../move-basics/primitive-types#integer-types) +- [`vector`](./../move-basics/vector) when `T` has `store` +- [address](./../move-basics/address) All of the types defined in the standard library have the `store` ability as well. This includes: -- [Option](./../move-basics/option.md) -- [String](./../move-basics/string.md) and [ASCII String](./../move-basics/string.md) -- [TypeName](./../move-basics/type-reflection.md) +- [`Option`](./../move-basics/option) when `T` has `store` +- [String](./../move-basics/string) and [ASCII String](./../move-basics/string#ascii-strings) +- [TypeName](./../move-basics/type-reflection) + +## Summary + +- `store` allows a type to be persisted - used as a field of an object, at any nesting depth. +- For objects, `store` additionally unlocks _wrapping_ and the public storage functions. +- `store` is independent of `copy` and `drop`; container types have it conditionally on their + contents. ## Further Reading - [Type Abilities](./../../reference/abilities) in the Move Reference. -[key-ability]: ./key-ability.md -[drop-ability]: ./../move-basics/drop-ability.md -[copy-ability]: ./../move-basics/copy-ability.md +[key-ability]: ./key-ability +[drop-ability]: ./../move-basics/drop-ability +[copy-ability]: ./../move-basics/copy-ability +[sui-framework]: ./../programmability/sui-framework diff --git a/book/storage/transfer-restrictions.md b/book/storage/transfer-restrictions.md deleted file mode 100644 index 3c15e1cec..000000000 --- a/book/storage/transfer-restrictions.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -draft: true -description: "Restricted vs public transfer in Sui Move: how the store ability controls whether objects can be transferred outside their module." ---- - - - -# Restricted and Public Transfer - -Storage Operations that we described in the [previous sections](./storage-functions) are restricted -by default - they can only be called in the module defining the object. In other terms, the type -must be _internal_ to the module to be used in storage operations. This restriction is implemented -in the Sui Verifier and is enforced at the bytecode level. - -However, to allow objects to be transferred and stored in other modules, these restrictions can be -relaxed. The `sui::transfer` module offers a set of _public\_\*_ functions that allow calling -storage operations in other modules. The functions are prefixed with `public_` and are available to -all modules and transactions. - -## Public Storage Operations - -The `sui::transfer` module provides the following public functions. They are almost identical to the -ones we already covered, but can be called from any module. - -```move -module sui::transfer; - -/// Public version of the `transfer` function. -public fun public_transfer(object: T, to: address) {} - -/// Public version of the `share_object` function. -public fun public_share_object(object: T) {} - -/// Public version of the `freeze_object` function. -public fun public_freeze_object(object: T) {} -``` - -To illustrate the usage of these functions, consider the following example: module A defines an -ObjectK with `key` and ObjectKS with `key + store` abilities, and module B tries to implement a -`transfer` function for these objects. - -> In this example we use `transfer::transfer`, but the behavior is identical for `share_object` and -> `freeze_object` functions. - -```move -/// Defines `ObjectK` and `ObjectKS` with `key` and `key + store` -/// abilities respectively -module book::transfer_a; - -public struct ObjectK has key { id: UID } -public struct ObjectKS has key, store { id: UID } -``` - -```move -/// Imports the `ObjectK` and `ObjectKS` types from `transfer_a` and attempts -/// to implement different `transfer` functions for them -module book::transfer_b; - -// types are not internal to this module -use book::transfer_a::{ObjectK, ObjectKS}; - -// Fails! ObjectK is not `store`, and ObjectK is not internal to this module -public fun transfer_k(k: ObjectK, to: address) { - transfer::transfer(k, to); -} - -// Fails! ObjectKS has `store` but the function is not public -public fun transfer_ks(ks: ObjectKS, to: address) { - transfer::transfer(ks, to); -} - -// Fails! ObjectK is not `store`, `public_transfer` requires `store` -public fun public_transfer_k(k: ObjectK, to: address) { - transfer::public_transfer(k, to); -} - -// Works! ObjectKS has `store` and the function is public -public fun public_transfer_ks(ks: ObjectKS, to: address) { - transfer::public_transfer(ks, to); -} -``` - -To expand on the example above: - -- ❌ `transfer_k` fails because ObjectK is not internal to module `transfer_b` -- ❌ `transfer_ks` fails because ObjectKS is not internal to module `transfer_b` -- ❌ `public_transfer_k` fails because ObjectK does not have the `store` ability -- ✅ `public_transfer_ks` works because ObjectKS has the `store` ability and the transfer is public - -## Implications of `store` - -The decision on whether to add the `store` ability to a type should be made carefully. On one hand, -it is de-facto a requirement for the type to be _usable_ by other applications. On the other hand, -it allows _wrapping_ and changing the intended storage model. For example, a character may be -intended to be owned by accounts, but with the `store` ability it can be frozen (cannot be shared - -this transition is restricted). diff --git a/book/storage/transfer-to-object.md b/book/storage/transfer-to-object.md index 0bb177a18..47b77af45 100644 --- a/book/storage/transfer-to-object.md +++ b/book/storage/transfer-to-object.md @@ -4,8 +4,8 @@ description: "Transfer to Object (TTO) in Sui: send objects to other objects and # Receiving as Object -[Address owned](./storage-functions.md#transfer) Object state supports two types of owners: an -account and another Object. If an object was transferred to another object, Sui provides a way to +The [address owned](./storage-functions.md#transfer) object state supports two types of owners: an +account and another object. If an object was transferred to another object, Sui provides a way to _receive_ this object through its owner's [`UID`][uid]. > This feature is also known as _"Transfer to Object"_ or TTO. @@ -16,7 +16,7 @@ Receiving functionality is implemented in the [`sui::transfer`][transfer] module special type `Receiving` which is instantiated through a special transaction argument, and the `receive` function which takes a [`UID`][uid] of the parent. -> Currently, `T` in the `transfer::receive` is a subject to [Internal Constraint][internal]. Public +> The `T` in `transfer::receive` is subject to the [Internal Constraint][internal]. The public > version of `receive` is called `public_receive`, and like other [storage functions][storage-funs] > it requires `T` to have [`store`][store]. @@ -35,14 +35,15 @@ public struct Receiving has drop { public fun receive(parent: &mut UID, to_receive: Receiving): T; ``` -Due to the `UID` type requirement, receiving cannot be performed on an arbitrary object that does -not provide access or special receiving implementation. This feature should be used be used with -caution and in a controlled setting. +Because `receive` requires a mutable reference to the parent's `UID`, receiving is only possible +through the module that defines the parent - or through the access it chooses to expose. An object +whose module provides no receiving implementation cannot release the objects sent to it, so this +feature should be used with caution and in a controlled setting. ## Example -As an illustration for _transfer_ and _receive_ consider an example: `PostOffice` allows registering -Post Boxes and sending to accounts' post boxes. +As an illustration of _transfer_ and _receive_, consider a `PostOffice` that registers post boxes +and lets anyone send objects to them: ```move file=packages/samples/sources/storage/transfer-to-object.move anchor=main @@ -50,23 +51,39 @@ Post Boxes and sending to accounts' post boxes. ## Use Cases -Transferring to objects is a powerful feature which allows objects to act as owners of other -objects. One of the reasons to use it is the extra authorization performed upon receiving, eg the -`PostOffice` in the example above could charge a receiving fee. +Transferring to objects is a powerful feature that lets objects act as owners of other objects, +and it enables designs that plain address ownership cannot express: -- Allows parallel execution of transfers to multiple objects without referencing them in the - transaction; -- Parent objects can also be transferred, acting as a container; -- PostBox-like applications, where user gets assets only after activating their account; -- Account abstraction-like applications where an object is mocking an account. +- **Controlled receiving.** Because receiving goes through the parent's module, extra logic can be + attached to it - the `PostOffice` above could, for example, charge a fee for every received + item. +- **Objects as containers.** A parent object collects assets sent to it and can itself be + transferred, carrying its entire "inventory" along - without ever listing the contents in a + transaction. +- **Deferred delivery.** Assets can be sent to an object before its owner is ready to claim them - + a post-box that accumulates items until the user activates their account. +- **Account-like objects.** An object with an ID that receives and releases assets behaves much + like an account, which makes TTO a building block for account-abstraction designs. -## Links +Sending _to an object_ is also naturally parallel: transfers to an object ID are plain transfers - +they do not reference the parent in the transaction, and therefore do not contend on it. + +## Next Steps + +This section concludes the Using Objects chapter: you can now define objects, place them into any +ownership state, manage their identity, and even make objects own other objects. The +[Advanced Programmability](./../programmability) chapter builds on all of it - starting with the +execution environment, and returning to object composition with +[Dynamic Fields](./../programmability/dynamic-fields), the second mechanism behind parent-child +object relationships. + +## Further Reading - [Transfer to Object](https://docs.sui.io/guides/developer/objects/transfers/transfer-to-object) in Sui Documentation - [`sui::transfer`][transfer] module documentation -[transfer]: https://docs.sui.io/references/framework/sui_sui/transfer +[transfer]: https://docs.sui.io/references/framework/sui/transfer [key]: ./key-ability.md [store]: ./store-ability.md [uid]: ./uid-and-id.md diff --git a/book/storage/uid-and-id.md b/book/storage/uid-and-id.md index a43d6ef4d..900ca00bb 100644 --- a/book/storage/uid-and-id.md +++ b/book/storage/uid-and-id.md @@ -5,7 +5,7 @@ description: "UID and ID in Sui Move: unique object identifiers, how they are cr # UID and ID The use of the `UID` type is required by the Sui Verifier on all types that have the -[`key`](./key-ability.md) ability. Here we go deeper into `UID` and its usage. +[`key`](./key-ability) ability. Here we go deeper into `UID` and its usage. ## Definition @@ -16,67 +16,77 @@ after the object was deleted. ```move module sui::object; -/// UID is a unique identifier of an object +/// UID is a unique identifier of an object. public struct UID has store { id: ID } -/// ID is a wrapper around an address -public struct ID has store, drop { +/// ID is a wrapper around an address; freely copyable. +public struct ID has copy, drop, store { bytes: address } ``` +Note the difference in abilities: an `ID` is plain, copyable data - a pointer that can name any +object (or even a non-existent one) without any special privileges. A `UID` can be neither copied +nor dropped: it is the identity of an object, and both its creation and its destruction are +explicit, controlled operations. + ## Fresh UID Generation -- `UID` is _derived_ from the `tx_hash` and an `index` which is incremented for each new UID. -- The `derive_id` function is implemented in the `sui::tx_context` module, and that is why - [TxContext](./../programmability/transaction-context.md) is required for `UID` generation. -- Sui Verifier will not allow using a UID that wasn't created in the same function. That prevents - UIDs from being pre-generated or reused after the object was unpacked. +A new `UID` is created with the `object::new(ctx)` function: -New UID is created with the `object::new` function. It takes a mutable reference to `TxContext`, and -returns a new `UID`. +- `UID` is _derived_ from the transaction digest and a counter of IDs created so far in the + transaction, which is incremented with each new UID. +- The counter lives in the transaction context, which is why + [TxContext](./../programmability/transaction-context) is required - as a mutable reference - for + UID generation. +- The `id` field of a newly created object must be a _fresh_ UID - one produced by `object::new` + in the same transaction. The Sui Verifier rejects packing an object with a UID taken from + another, unpacked object - so an identity can never be reused, even by the module that owns it. + +`UID` acts as the representation of an object, and enables features attached to the object's +identity. One of the key ones - [Dynamic Fields](./../programmability/dynamic-fields) - is +possible because the `UID` is explicit. Another - [Transfer to Object](./transfer-to-object), +covered at the end of this chapter - allows an object to receive other objects sent to its ID. + +## UID Lifecycle + +The `UID` is created with `object::new`, and deleted with the `object::delete` function. The +`delete` function consumes the UID _by value_, so it can only be called after the object was +[unpacked](./../move-basics/struct#unpacking-a-struct) - which, in turn, only the defining module +can do: + +```move file=packages/samples/sources/storage/uid-and-id.move anchor=lifecycle -```move -public fun uid(ctx: &mut TxContext) { - let id = object::new(ctx); // Create a fresh UID from TxContext. - id.delete(); // Delete the UID. -} ``` -`UID` acts as a representation of an object, and allows defining behaviors and features of an -object. One of the key features - [Dynamic Fields](./../programmability/dynamic-fields) - is -possible because of the `UID` type being explicit. Additionally, it allows receiving objects sent to -other objects. This feature is called [Transfer to Object (TTO)](./transfer-to-object.md), and we -will explain later in this chapter. +### Keeping the UID -## UID Derivation +The `UID` does not have to be deleted immediately after the object is unpacked. It may carry +[Dynamic Fields](./../programmability/dynamic-fields), or hold objects sent to it via +[Transfer to Object](./transfer-to-object) - deleting the UID would make those unreachable. For +such cases, the UID can be kept: stored as a plain `UID` field (not as `id`!) in another struct, +until the associated data is dealt with and the UID can be safely deleted. -Sui allows deriving UID's from other UIDs using _derivation keys_. This functionality is implemented -in the [`sui::derived_object`][derived-object] module and allows generating predictable and -deterministic `UIDs` for easier off-chain discovery. UID for each pair of parent + key can be -generated only once! +> The ability to keep a UID after its object is gone enables a niche technique known as _proof of +> deletion_: the returned UID is evidence that the object was destroyed, which an application can +> exchange for a reward, or use to bypass restrictions that applied to the live object. -```move -use sui::derived_object; +## UID Derivation -/// Some central application object. -public struct Base has key { id: UID } +Sui allows deriving UIDs from other UIDs using _derivation keys_. This functionality is +implemented in the [`sui::derived_object`][derived-object] module, and produces predictable, +deterministic IDs for easier offchain discovery. The UID for each parent + key pair can be +claimed only once: -/// Derived Object. -public struct Derived has key { id: UID } +```move file=packages/samples/sources/storage/uid-and-id.move anchor=derived -/// Create and share a new Derived object using `address` as a `key`. -public fun derive(base: &mut Base, key: address) { - let id = derived_object::claim(&mut base.id, key); - transfer::share_object(Derived { id }) -} ``` -Derived addresses reduce the load on off-chain indexers, since it is enough to store the ID of the -parent object and get derived IDs using a derivation function. ID derivation function is part of the -most SDKs, and also present in Move: +Derived addresses reduce the load on offchain indexers: it is enough to know the ID of the parent +object, and the IDs of derived objects can be computed with a derivation function - present in +most SDKs, and in Move itself: ```move module sui::derived_object; @@ -84,92 +94,49 @@ module sui::derived_object; /// Checks if a UID was derived with `key` at `parent`. public fun exists(parent: &UID, key: K): bool; -/// Derive inner `address` of a UID, regardless of whether it was created. +/// Derive the inner `address` of a UID, regardless of whether it was claimed. public fun derive_address(parent: ID, key: K): address; ``` -The same derivation functionality is used to generate UIDs for -[dynamic fields](./../programmability/dynamic-fields.md). - -## UID Lifecycle - -The `UID` type is created with the `object::new` function, and deleted with the `object::delete` -function. The `object::delete` consumes the UID _by value_, hence, it is only possible to delete -object's UID after the object [was unpacked](./../move-basics/struct.md#unpacking-a-struct). - -```move -public struct Character has key { id: UID } - -public fun character(ctx: &mut TxContext) { - // Instantiate `Character` object. - let char = Character { id: object::new(ctx) }; - - // Unpack object to get its UID. - let Character { id } = char; - - // Delete the UID. - id.delete(); -} -``` - -## Keeping the UID - -The `UID` does not need to be deleted immediately after the object struct is unpacked. Sometimes it -may carry [Dynamic Fields](./../programmability/dynamic-fields) or objects transferred to it via -[Transfer To Object](./transfer-to-object.md). In such cases, the UID may be kept and stored in a -separate object. - -## Proof of Deletion - -The ability to return the UID of an object may be utilized in pattern called _proof of deletion_. It -is a rarely used technique, but it may be useful in some cases, for example, the creator or an -application may incentivize the deletion of an object by exchanging the deleted IDs for some reward. - -In framework development this method could be used to ignore / bypass certain restrictions on -"taking" the object. If there's a container that enforces certain logic on transfers, like Kiosk -does, there could be a special scenario of skipping the checks by providing a proof of deletion. - -This is one of the open topics for exploration and research, and it may be used in various ways. +The same derivation mechanism is used internally to generate IDs for +[dynamic fields](./../programmability/dynamic-fields). ## ID -When talking about `UID` we should also mention the `ID` type. It is a wrapper around the `address` -type, and is used to represent an address-pointer. Usually, `ID` is used to point at an object, -however, there is no restriction, and no guarantee that the `ID` points to an existing object. +When talking about `UID` we should also mention the `ID` type. It is a freely copyable wrapper +around `address`, used to _point_ at an object. Usually an `ID` refers to some object, but there +is no restriction - and no guarantee - that the ID points to an existing object. -> ID can be received as a transaction argument in a -> [Transaction Block](./../concepts/what-is-a-transaction). Alternatively, ID can be created from an -> `address` value using `to_id()` function. - -```move -public fun conversion_methods(ctx: &mut TxContext) { - let uid: UID = object::new(ctx); - let id: ID = uid.to_inner(); +> An ID can be received as a transaction argument in a +> [Transaction Block](./../concepts/what-is-a-transaction). Alternatively, an ID can be created +> from an `address` value using the `to_id()` function. - let addr_from_uid: address = uid.to_address(); - let addr_from_id: address = id.to_address(); +```move file=packages/samples/sources/storage/uid-and-id.move anchor=conversions - uid.delete(); -} ``` -This example demonstrates different conversion methods: `UID.to_inner` creates a copy of underlying -`ID`, and `UID.to_address` returns inner address. Another often useful method `ID.to_address` copies -inner value from the `ID` type. - ## Fresh Object Address -[`TxContext`](./../programmability/transaction-context.md) provides the `fresh_object_address` -function which can be utilized to create unique addresses and `ID` - it may be useful in some -application that assign unique identifiers to user actions - for example, an order_id in a -marketplace. +[`TxContext`](./../programmability/transaction-context) provides the `fresh_object_address` +function, which produces a unique address using the same derivation as `object::new` - without +creating a `UID`. It is useful for applications that need unique identifiers for offchain +entities - for example, an `order_id` in a marketplace. + +## Summary + +- `UID` is the non-copyable, non-droppable identity of an object; `ID` is a freely copyable + pointer. +- Fresh UIDs come from `object::new(ctx)` and can never be reused for a new object. +- A UID is deleted with `object::delete` after unpacking - or kept, if data is still attached to + it. +- Derived UIDs (`sui::derived_object`) make object IDs predictable and discoverable offchain. -## Links +## Further Reading -- [`sui::object`][object] module documentation -- [`sui::derived_object`][derived-object] module documentation +- [`sui::object`][object] module documentation. +- [`sui::derived_object`][derived-object] module documentation. - [Derived Objects](https://docs.sui.io/guides/developer/objects/derived-objects) in Sui - Documentation + Documentation. -[object]: https://docs.sui.io/references/framework/sui_sui/object -[derived-object]: https://docs.sui.io/references/framework/sui_sui/derived_object +[object]: https://docs.sui.io/references/framework/sui/object +[derived-object]: https://docs.sui.io/references/framework/sui/derived_object diff --git a/book/testing/builder-pattern.md b/book/testing/builder-pattern.md index 855ea75db..4862e3325 100644 --- a/book/testing/builder-pattern.md +++ b/book/testing/builder-pattern.md @@ -101,7 +101,8 @@ use sui_system::validator_builder; #[test] fun test_validator_operations() { - let validator = validator_builder::preset() + let ctx = &mut tx_context::dummy(); + let validator = validator_builder::preset(1) .name("My Validator") .gas_price(1000) .commission_rate(500) // 5% @@ -112,25 +113,26 @@ fun test_validator_operations() { } ``` -The `preset()` function returns a builder pre-filled with valid test defaults, so tests only -override the fields they care about. +The `preset(index)` function returns a builder pre-filled with valid test defaults - keys, addresses +and economic parameters - for one of several predefined validators, so tests only override the +fields they care about. ### TxContextBuilder in Sui Framework The [`TxContextBuilder`][tx-context-builder] allows customizing transaction context for specific -test scenarios: +test scenarios. The builder is passed to `begin_with_context()` to start a scenario, or to +`next_with_context()` to advance an existing one: ```move use sui::test_scenario as ts; #[test] fun test_epoch_dependent_logic() { - let mut test = ts::begin(@0x1); - let ctx = test - .ctx_builder() - .set_epoch(100) - .set_epoch_timestamp(1000000) - .build(); + let mut test = ts::begin_with_context( + ts::ctx_builder_from_sender(@0x1) + .set_epoch(100) + .set_epoch_timestamp(1_000_000), + ); // test logic that depends on epoch... diff --git a/book/testing/coverage.md b/book/testing/coverage.md index 046826a36..c41dfcd30 100644 --- a/book/testing/coverage.md +++ b/book/testing/coverage.md @@ -16,8 +16,9 @@ To generate coverage data, run your tests with the `--coverage` flag: sui move test --coverage ``` -This will run all tests and collect coverage information. The coverage data is stored in the `build` -directory and can be analyzed using the `sui move coverage` subcommands. +This will run all tests and collect coverage information. The coverage data is stored in a +`.coverage_map.mvcov` file in the package root (next to `Move.toml`) and can be analyzed using the +`sui move coverage` subcommands. ## Coverage Summary @@ -33,17 +34,19 @@ This outputs a table showing the coverage percentage for each module: +-------------------------+ | Move Coverage Summary | +-------------------------+ -Module 0x0::my_module +Module 0000000000000000000000000000000000000000000000000000000000000000::my_module >>> % Module coverage: 85.71 -Module 0x0::another_module +Module 0000000000000000000000000000000000000000000000000000000000000000::another_module >>> % Module coverage: 100.00 -Module 0x0::untested_module +Module 0000000000000000000000000000000000000000000000000000000000000000::untested_module >>> % Module coverage: 0.00 +-------------------------+ | % Move Coverage: 62.50 | +-------------------------+ ``` +> Modules are listed under the full 32-byte form of their package address - `0x0` in this example. + To see coverage broken down by individual functions, add the `--summarize-functions` flag: ```bash diff --git a/book/testing/extend-foreign-module.md b/book/testing/extend-foreign-module.md index d16dbebc9..bf1b8346a 100644 --- a/book/testing/extend-foreign-module.md +++ b/book/testing/extend-foreign-module.md @@ -188,12 +188,11 @@ Extensions have important constraints to be aware of: - **Edition compatibility**: Extension code is subject to the same edition features as the target module. If the target module uses an older edition, your extension code must be compatible with that edition. -- **Edition requirement**: Extensions require the `2024.alpha` edition or later. Ensure your - `Move.toml` specifies a compatible edition. +- **Edition requirement**: Extensions are currently available only in the `2024.alpha` edition. + Ensure your `Move.toml` specifies it. ## Further Reading - [Module Extensions | Reference](./../../reference/extensions) - detailed specification of the extension syntax and semantics - [Integrating Pyth in Sui](https://docs.pyth.network/price-feeds/core/use-real-time-data/pull-integration/sui) -- [App Examples: Oracles](https://docs.sui.io/guides/developer/app-examples/oracle) diff --git a/book/testing/gas-profiling.md b/book/testing/gas-profiling.md index ed6643c05..32ad38ee6 100644 --- a/book/testing/gas-profiling.md +++ b/book/testing/gas-profiling.md @@ -9,7 +9,7 @@ testing framework provides built-in tools to measure gas usage during test execu that, a special utility `sui analyze-trace` is available for more thorough analysis of gas usage. > The statistics shown by `-s` only reflect **computation units** - they do not include storage -> costs. Additionally, compiler computation units don't map directly to actual on-chain gas charges; +> costs. Additionally, compiler computation units don't map directly to actual onchain gas charges; > they show relative computational complexity, useful for comparing implementations against each > other. To get actual gas costs, publish your package to testnet and measure real transactions. @@ -30,11 +30,11 @@ Test Statistics: ┌────────────────────────────────────────────────────────┬────────────┬───────────────────────────┐ │ Test Name │ Time │ Gas Used │ ├────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ -│ book::my_module::test_simple_operation │ 0.003 │ 1 │ +│ book::my_module::test_simple_operation │ 0.006 │ 998001 │ ├────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ -│ book::my_module::test_complex_operation │ 0.011 │ 59 │ +│ book::my_module::test_complex_operation │ 0.007 │ 998068 │ ├────────────────────────────────────────────────────────┼────────────┼───────────────────────────┤ -│ book::my_module::test_with_objects │ 0.008 │ 25 │ +│ book::my_module::test_with_objects │ 0.006 │ 998001 │ └────────────────────────────────────────────────────────┴────────────┴───────────────────────────┘ Test result: OK. Total tests: 3; passed: 3; failed: 0 @@ -44,6 +44,10 @@ Test result: OK. Total tests: 3; passed: 3; failed: 0 - **Time**: Execution time in seconds - **Gas Used**: Gas units consumed by the test +> Every test's total includes a large fixed base cost - even an empty test reports roughly 998000 +> gas units. When comparing tests, look at the difference between their totals rather than the +> absolute values. + ## CSV Output For programmatic analysis or importing into spreadsheets, use the `csv` option: @@ -55,10 +59,10 @@ sui move test -s csv This produces comma-separated output: ``` -test_name,time_ns,gas_used -book::my_module::test_simple_operation,3381750,1 -book::my_module::test_complex_operation,8454125,59 -book::my_module::test_with_objects,3905625,25 +name,nanos,gas +book::my_module::test_simple_operation,5992125,998001 +book::my_module::test_complex_operation,6870583,998068 +book::my_module::test_with_objects,6022917,998001 ``` The time is in nanoseconds, which allows for more precise measurements when comparing similar @@ -70,14 +74,18 @@ Use the `-i` or `--gas-limit` flag to set a maximum gas budget for tests. Tests will timeout: ```bash -sui move test -i 50 +sui move test -i 1000 ``` +> The limit is measured in internal execution gas units, which do not map one-to-one to the values +> in the `Gas Used` column - a trivial test that reports ~998000 gas passes comfortably with a +> limit of 1000. + Output when a test exceeds the gas limit: ``` -[ PASS ] book::my_module::test_simple_operation [ TIMEOUT ] book::my_module::test_complex_operation +[ PASS ] book::my_module::test_simple_operation [ PASS ] book::my_module::test_with_objects Test failures: @@ -87,6 +95,8 @@ Failures in book::my_module: ┌── test_complex_operation ────── │ Test timed out └────────────────── + +Test result: FAILED. Total tests: 3; passed: 2; failed: 1 ``` This is useful for: @@ -117,33 +127,35 @@ public fun sum_formula(n: u64): u64 { #[test] fun test_sum_loop() { - let result = sum_loop(100); - assert_eq!(result, 4950); + let result = sum_loop(1000); + assert_eq!(result, 499500); } #[test] fun test_sum_formula() { - let result = sum_formula(100); - assert_eq!(result, 4950); + let result = sum_formula(1000); + assert_eq!(result, 499500); } ``` Running with statistics reveals the difference: ```bash -sui move test -s comparison +sui move test comparison -s ``` ```table ┌────────────────────────────────────┬────────────┬───────────────────────────┐ │ Test Name │ Time │ Gas Used │ ├────────────────────────────────────┼────────────┼───────────────────────────┤ -│ book::comparison::test_sum_loop │ 0.005 │ 201 │ +│ book::comparison::test_sum_loop │ 0.003 │ 998078 │ ├────────────────────────────────────┼────────────┼───────────────────────────┤ -│ book::comparison::test_sum_formula │ 0.002 │ 3 │ +│ book::comparison::test_sum_formula │ 0.001 │ 998001 │ └────────────────────────────────────┴────────────┴───────────────────────────┘ ``` +The loop costs 77 gas units on top of the base cost, while the formula adds nothing measurable. + ## Trace Analysis For deeper profiling, you can generate execution traces from tests and visualize them with @@ -158,7 +170,7 @@ Run tests with the `--trace` flag to produce trace files: sui move test --trace ``` -Trace files are written to the `traces/` directory inside the package build folder. +Trace files are written to the `traces/` directory in the package root (next to `Move.toml`). ### Step 2: Generate a Gas Profile @@ -169,10 +181,10 @@ sui analyze-trace -p traces/ gas-profile ``` This outputs a `gas_profile_.json` file in the current directory. You can specify a -different output directory with the `-o` flag: +different output directory with the `-o` flag, which goes before the `gas-profile` subcommand: ```bash -sui analyze-trace -p traces/ gas-profile -o ./profiles +sui analyze-trace -p traces/ -o ./profiles gas-profile ``` ### Step 3: Visualize with Speedscope diff --git a/book/testing/good-tests.md b/book/testing/good-tests.md index 86e0599eb..a3daa581a 100644 --- a/book/testing/good-tests.md +++ b/book/testing/good-tests.md @@ -24,10 +24,21 @@ understand what scenario is being tested and what the expected outcome is. > [Builder Pattern](./builder-pattern.md) which is covered later in this chapter. ```move +module book::readable_tests; + +public struct Balance has copy, drop { value: u64 } + +public fun new(value: u64): Balance { Balance { value } } +public fun add(balance: &mut Balance, amount: u64) { balance.value = balance.value + amount; } +public fun value(balance: &Balance): u64 { balance.value } + +#[test_only] +use std::unit_test::assert_eq; + #[test] fun test_add_increases_balance_by_specified_amount() { // Arrange: set up initial state - let mut balance = balance::new(100); + let mut balance = new(100); // Act: perform the operation being tested balance.add(50); @@ -82,7 +93,7 @@ implementations without breaking tests. Edge cases are where bugs often hide. For numeric operations, consider: - Zero values -- Maximum values (`U64_MAX`, `U128_MAX`) +- Maximum values (`std::u64::max_value!()`, `std::u128::max_value!()`) - Boundary conditions (off-by-one errors) - Empty collections diff --git a/book/testing/index.md b/book/testing/index.md index c70c4a3cd..801c4f91f 100644 --- a/book/testing/index.md +++ b/book/testing/index.md @@ -1,5 +1,6 @@ --- -description: "Testing Move smart contracts on Sui: unit tests, test scenarios, coverage reports, gas profiling, and best practices." +title: Testing Move Programs +description: "Testing Move smart contracts on Sui: unit tests, test scenarios, linting, coverage reports, gas profiling, and best practices." --- # Testing @@ -12,7 +13,7 @@ cannot verify that your transfer logic sends funds to the right recipient, that at the right time, or that your access control matches your intended policy. These are properties of your design, not the language - and they can only be verified through testing. -The stakes of getting it wrong are uniquely high in on-chain programming: +The stakes of getting it wrong are uniquely high in onchain programming: - **Financial risk**: Bugs in asset-handling code can lead to permanent loss of funds. A single overlooked edge case in transfer logic or access control can be exploited, resulting in @@ -21,10 +22,10 @@ The stakes of getting it wrong are uniquely high in on-chain programming: with the ability to upgrade, the previous version of the code will always be available. Thorough testing before deployment is your primary defense against vulnerabilities. - **Adversarial environment**: Published Move packages are effectively open-source - anyone can read - and decompile on-chain bytecode. This means malicious actors can study your code in detail, + and decompile onchain bytecode. This means malicious actors can study your code in detail, searching for exploitable flaws. Your code must handle not just expected inputs, but intentional attempts to break it. -- **Composability risks**: Move modules interact with other on-chain code. Testing must verify that +- **Composability risks**: Move modules interact with other onchain code. Testing must verify that your code behaves correctly not only in isolation but also when composed with other packages. Given these stakes, comprehensive testing is not optional - it is essential for any Move application diff --git a/book/testing/linting.md b/book/testing/linting.md new file mode 100644 index 000000000..533d34a6b --- /dev/null +++ b/book/testing/linting.md @@ -0,0 +1,159 @@ +--- +description: "Run Move linters with sui move lint: catch Sui-specific antipatterns at compile time, suppress false positives, and enforce lints in CI." +--- + +# Running Lints + +The Move compiler ships with a set of _lints_ - static checks that flag suspicious patterns in the +code at compile time. Tests verify that the code does what it should; lints catch code that +compiles and may even pass tests, but does something a more experienced Move developer would not +write: transfers that break composability, comparisons that never do what they look like they do, +or an `entry` function that can never be called. Running lints regularly - and keeping the package +free of warnings - is a cheap way to maintain code quality. + +## Running Lints + +The `sui move lint` command compiles the package and runs the full set of linters: + +```bash +sui move lint +``` + +To also check the code in the `tests` directory, add the `--test` flag: + +```bash +sui move lint --test +``` + +The same checks are available on other commands via the `--lint` flag - for example, +`sui move test --lint` runs the tests and the full lint set in one go. + +Consider a module with a function that transfers a newly created object to the transaction sender: + +```move +module book::mint; + +public struct Item has key, store { id: UID } + +public fun mint(ctx: &mut TxContext) { + let item = Item { id: object::new(ctx) }; + transfer::transfer(item, ctx.sender()); +} +``` + +Running the linter prints a warning with an explanation and a pointer to the exact expression: + +``` +warning[Lint W99001]: non-composable transfer to sender + ┌─ ./sources/mint.move:7:5 + │ +5 │ public fun mint(ctx: &mut TxContext) { + │ ---- Returning an object from a function, allows a caller to use the object and enables composability via programmable transactions. +6 │ let item = Item { id: object::new(ctx) }; +7 │ transfer::transfer(item, ctx.sender()); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + │ │ │ + │ │ Transaction sender address coming from here + │ Transfer of an object to transaction sender address + │ + = This warning can be suppressed with '#[allow(lint(self_transfer))]' applied to the 'module' or module member ('const', 'fun', or 'struct') +``` + +The fix suggested by this particular lint is to return the `Item` from the function instead of +transferring it, and let the caller decide what to do with the object. + +## Default and Extra Lints + +Lints come in two tiers. The _default_ tier contains the most important Sui-specific checks, and +runs on every compilation - a plain `sui move build` or `sui move test` reports these warnings +too. The _extra_ tier adds two more Sui checks and a set of code style lints; it runs when linting +is explicitly requested - by `sui move lint` or the `--lint` flag. + +## Suppressing Lints + +Lints are heuristics, and sometimes the flagged code is intentional. A lint can be suppressed with +the `#[allow(lint())]` attribute, applied to a module or a module member, using the lint +name printed in the warning: + +```move +public struct Account has key { id: UID } + +/// An account object, deliberately created for and owned by the sender. +#[allow(lint(self_transfer))] +public fun new_account(ctx: &mut TxContext) { + transfer::transfer( + Account { id: object::new(ctx) }, + ctx.sender(), + ); +} +``` + +A single attribute can suppress multiple lints: `#[allow(lint(share_owned, self_transfer))]`. +Treat suppressions like any other exception - keep them narrow (prefer a function over the whole +module) and explain the reason in a comment or doc comment. + +## Lints in CI + +To enforce a warning-free codebase, add the `--warnings-are-errors` flag - the command then fails +with a non-zero exit code on any warning, including lints: + +```bash +sui move lint --test --warnings-are-errors +``` + +For tooling that consumes the output programmatically, `--json-errors` switches diagnostics to +JSON format. + +## Lint Reference + +The linter groups its checks into two sets: the _default_ lints that run on every compilation, and +the _extra_ lints that only run under the `--lint` flag. + +### Default Lints + +These run on every compilation: + +| Lint | Code | What it flags | +| --- | --- | --- | +| `share_owned` | W99000 | Sharing an object that may have been previously owned; share objects in the transaction that creates them | +| `self_transfer` | W99001 | Transferring a new object to the sender instead of returning it; hurts composability | +| `custom_state_change` | W99002 | A custom transfer/share/freeze policy on a type with `store`; the `public_*` [storage functions](./../storage/storage-functions) can bypass it | +| `coin_field` | W99003 | A struct field of type `Coin`; [`Balance`](./../programmability/balance-and-coin) is cheaper and usually the right choice | +| `freeze_wrapped` | W99004 | Freezing an object that wraps other objects | +| `collection_equality` | W99005 | Comparing [dynamic collections](./../programmability/dynamic-collections) with `==`; only the `id` and `size` are compared, never the contents | +| `public_random` | W99006 | A `public` function taking [`Random`](./../programmability/randomness); exposes randomness to composition attacks | +| `missing_key` | W99007 | A struct with an `id: UID` field but no `key` ability | +| `public_entry` | W99010 | Unnecessary [`entry`](./../move-advanced/entry-functions) modifier on a `public` function | +| `uncallable_function` | W99011 | A function that can never be called in a transaction, such as an `entry` function taking `&mut Clock` | + +### Extra Lints + +Enabled by `sui move lint` or the `--lint` flag: + +| Lint | Code | What it flags | +| --- | --- | --- | +| `freezing_capability` | W99008 | Freezing a type that looks like a [capability](./../programmability/capability) | +| `prefer_mut_tx_context` | W99009 | A `public` function taking `&TxContext`; prefer `&mut TxContext` to keep the signature future-proof | + +The extra tier also includes code style lints (codes `W04xxx`): `constant_naming`, `while_true`, +`unnecessary_math`, `unneeded_return`, `abort_without_constant`, `loop_without_exit`, +`unnecessary_conditional`, `self_assignment`, `redundant_ref_deref`, `unnecessary_unit`, +`always_equal_operands`, and `combinable_comparisons`. Each flags a small readability or +correctness issue and suggests the simpler equivalent. + +## Summary + +| Command | Description | +| --- | --- | +| `sui move lint` | Compile the package and run the full lint set | +| `sui move lint --test` | Also lint the code in the `tests` directory | +| `sui move lint --warnings-are-errors` | Fail on any warning - for CI | +| `sui move build` / `sui move test` | Run the default lint tier | +| `sui move test --lint` | Run tests with the full lint set | +| `--no-lint` | Disable linters entirely | + +## Further Reading + +- [Code Quality Checklist](./../guides/code-quality-checklist) - a broader review checklist that + lints automate a part of. +- [Move CLI reference](https://docs.sui.io/references/cli/move) in the Sui Documentation. diff --git a/book/testing/random-test.md b/book/testing/random-test.md index 381a7412e..b7a0bec6b 100644 --- a/book/testing/random-test.md +++ b/book/testing/random-test.md @@ -9,7 +9,7 @@ attribute. This enables property-based testing, where a test runs multiple times generated values to discover edge cases you might not think to test manually. > The `#[random_test]` attribute is a compiler feature for test inputs, separate from the -> `sui::random` module used for on-chain randomness. +> `sui::random` module used for onchain randomness. ## Basic Usage @@ -85,7 +85,7 @@ use std::unit_test::assert_eq; // Targeted test for specific case #[test] fun test_add_zero() { - assert_eq!(safe_add(std::u64::max(), 0), std::u64::max()); + assert_eq!(safe_add(std::u64::max_value!(), 0), std::u64::max_value!()); } // Random test for general properties @@ -129,9 +129,8 @@ When a random test fails, the output includes the seed and instructions to repro ``` ┌── test_that_failed ────── (seed = 2033439370411573084) │ ... -│ This test uses randomly generated inputs. Rerun with -│ `sui move test test_that_failed --seed 2033439370411573084` -│ to recreate this test failure. +│ This test uses randomly generated inputs. Rerun with `test test_that_failed --seed 2033439370411573084` to recreate this test failure. +│ └────────────────── ``` diff --git a/book/testing/test-scenario.md b/book/testing/test-scenario.md index d57e49491..85274a85b 100644 --- a/book/testing/test-scenario.md +++ b/book/testing/test-scenario.md @@ -413,9 +413,9 @@ fun test_with_clock() { let mut scenario = test_scenario::begin(alice); // Create system objects (Clock, Random, DenyList) + // This call advances the transaction, so the objects are immediately available scenario.create_system_objects(); - scenario.next_tx(alice); { // Now Clock is available as a shared object let clock = scenario.take_shared(); @@ -510,9 +510,10 @@ fun test_token_transfer_flow() { scenario.return_to_sender(token); }; - // Verify final state via effects + // Verify final state via effects - `return_to_sender` is recorded as a + // transfer back to bob in the effects of the final transaction let effects = scenario.end(); - assert_eq!(effects.transferred_to_account().size(), 0); // No transfers in final tx + assert_eq!(effects.transferred_to_account().size(), 1); } ``` diff --git a/book/testing/test-utilities.md b/book/testing/test-utilities.md index f6bf119cd..dc4c89d51 100644 --- a/book/testing/test-utilities.md +++ b/book/testing/test-utilities.md @@ -28,10 +28,10 @@ In published code, `assert!` should normally have an abort code as the second ar identify failures. However, in tests, the abort code is not necessary and doesn't provide any value. ```move -// In published code - abort code required +// In published code - abort code recommended assert!(balance >= amount, EInsufficientBalance); -// In test code - abort code optional +// In test code - abort code unnecessary assert!(balance >= amount); ``` @@ -55,12 +55,13 @@ The `assert_eq!` macro from `std::unit_test` solves this by printing both values fails: ```move +#[test_only] use std::unit_test::assert_eq; #[test] fun test_balance_update() { let balance = calculate_balance(); - assert_eq!(balance, 1000); // fails with: "Assertion failed: 750 != 1000" + assert_eq!(balance, 1000); // on failure, prints: "Assertion failed:", 750, "!=", 1000 } ``` @@ -72,6 +73,7 @@ assertion fails. To compare by reference, use `assert_ref_eq!` instead of `assert_eq!`: ```move +#[test_only] use std::unit_test::assert_ref_eq; #[test] @@ -91,7 +93,7 @@ logic implemented for each type. ```move module std::unit_test; -/// Consumes any value `T` and makes it disappear. +/// Black hole function to destroy any value in `test` mode. public native fun destroy(v: T); ``` @@ -116,6 +118,7 @@ In published code, `Ticket` type may not have a deletion function or require a c before deletion. In this case, `destroy` is the best way to deal with the value: ```move +#[test_only] use std::unit_test; #[test] diff --git a/book/testing/testing-basics.md b/book/testing/testing-basics.md index cf081a9fe..42e54c3c8 100644 --- a/book/testing/testing-basics.md +++ b/book/testing/testing-basics.md @@ -8,7 +8,7 @@ The Move compiler has a built-in testing framework - tests are written in Move a your source code. You annotate functions with `#[test]`, and the compiler handles discovery and execution. The VM execution environment is the same as in production, so your code runs with identical semantics. However, network and storage features are simulated in tests and don't behave -exactly as they do during actual on-chain execution - something to keep in mind when testing +exactly as they do during actual onchain execution - something to keep in mind when testing interactions with objects, transactions, and other platform-specific functionality. ## What is a Test? @@ -111,6 +111,18 @@ fun test_wrong_error_code() { } ``` +### Abort Codes from Other Modules + +The `abort_code` argument can also reference a constant defined in another module - including the +[Standard Library](./../move-basics/standard-library) and the +[Sui Framework](./../programmability/sui-framework) - by spelling out its full path. Visibility +does not matter here: the attribute can name a private constant of a dependency. This is the way to +test a function that is expected to fail _inside_ a dependency: + +```move file=packages/samples/sources/testing/testing-basics.move anchor=foreign_abort_code + +``` + ### Expected Location Specify where the abort should occur using `location`: @@ -122,9 +134,9 @@ fun test_abort_location() { } // Use `location = Self` for aborts in the current module -#[test, expected_failure(abort_code = 1, location = Self)] +#[test, expected_failure(abort_code = ENotFound, location = Self)] fun test_abort_in_self() { - abort 1 + abort ENotFound } ``` @@ -136,10 +148,11 @@ functions to have `public` or `public(package)` visibility so they can be called modules - since test-only code is stripped from production builds, this does not affect the public API of your package. -> Note: a good rule of thumb is to add `_for_testing` suffix to test-only functions and constants. -> This helps distinguish them from production code and makes it easier to find them in the codebase. -> Given that test-only functions often do things that production code cannot, this is a good way to -> ensure that you are not accidentally using a test-only function in production code. +> Note: a good rule of thumb is to add a `_for_testing` suffix to test-only functions and a `TEST_` +> prefix to test-only constants. This helps distinguish them from production code and makes it +> easier to find them in the codebase. Given that test-only functions often do things that +> production code cannot, this is a good way to ensure that you are not accidentally using a +> test-only function in production code. ### Test-Only Imports diff --git a/book/testing/transaction-context.md b/book/testing/transaction-context.md index 469824c9e..6944b15bf 100644 --- a/book/testing/transaction-context.md +++ b/book/testing/transaction-context.md @@ -81,11 +81,11 @@ fun test_with_hint() { ## Tracking Created Objects -When testing object creation, you may want to verify how many objects were created or get the ID of -the last created object: +When testing object creation, you may want to verify how many objects were created or get the +address of the last created object: ```move -use std::unit_test::assert_eq; +use std::unit_test::{assert_eq, destroy}; #[test] fun test_object_creation_count() { @@ -99,10 +99,13 @@ fun test_object_creation_count() { let obj2 = my_module::new(ctx); assert_eq!(ctx.ids_created(), 2); - // Get the ID of the most recently created object + // Get the address of the most recently created object (derived from its ID) let last_id = ctx.last_created_object_id(); + assert_eq!(last_id, object::id(&obj2).to_address()); - // ... + // Objects don't have `drop`, so they need to be cleaned up + destroy(obj1); + destroy(obj2); } ``` @@ -165,7 +168,7 @@ fun test_with_full_context() { | `new_from_hint()` | Like `new` but generates tx_hash from integer | | `create()` | Full control including gas parameters | | `ids_created()` | Check number of objects created | -| `last_created_object_id()` | Get ID of most recent object | +| `last_created_object_id()` | Get address of the most recent object | | `increment_epoch_number()` | Simulate epoch progression | | `increment_epoch_timestamp()` | Simulate time passing | diff --git a/book/testing/using-system-objects.md b/book/testing/using-system-objects.md index cbde7c84c..bc536991f 100644 --- a/book/testing/using-system-objects.md +++ b/book/testing/using-system-objects.md @@ -52,7 +52,7 @@ fun test_shared_clock() { ## Random -The `Random` object provides on-chain randomness. In tests, the full `Random` shared object can only +The `Random` object provides onchain randomness. In tests, the full `Random` shared object can only be created inside a [test scenario](./test-scenario.md) via `random::create_for_testing`. However, the preferred approach is to structure your code so that the core logic takes a `RandomGenerator` parameter - this lets you create a generator directly in unit tests with @@ -82,13 +82,18 @@ public(package) fun inner_function(gen: &mut RandomGenerator): Option { #[test] fun test_simple_random() { - // Deterministic, always the same value. + // Non-deterministic seed, useful for fuzzing. The result differs between + // runs, so don't assert a specific outcome. let mut gen = random::new_generator_for_testing(); - assert!(inner_function(&mut gen).is_none()); + let _result = inner_function(&mut gen); // Deterministic (reproducible with same seed) - let seed = b"Arbitrary seed bytes"; + let seed: vector = "Arbitrary seed bytes"; let mut gen = random::new_generator_from_seed_for_testing(seed); + assert!(inner_function(&mut gen).is_none()); + + // A different seed gives a different - but still reproducible - result + let mut gen = random::new_generator_from_seed_for_testing("move book"); assert!(inner_function(&mut gen).is_some()); } ``` @@ -110,10 +115,10 @@ fun test_random_shared() { let mut random = scenario.take_shared(); - // Initialize with seed bytes (required before use) + // Initialize with 32 bytes of randomness (required before use) random.update_randomness_state_for_testing( 0, - x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F", + x"2020202020202020202020202020202020202020202020202020202020202020", scenario.ctx(), ); @@ -197,8 +202,8 @@ fun test_with_all_system_objects() { let mut scenario = test_scenario::begin(@0xA); // Creates Clock, Random, and DenyList as shared objects + // (advances the transaction, so they are immediately available) scenario.create_system_objects(); - scenario.next_tx(@0xA); // Take objects by type let clock = scenario.take_shared(); @@ -228,7 +233,6 @@ use sui::test_scenario::{Self, most_recent_id_shared}; fun test_take_by_id() { let mut scenario = test_scenario::begin(@0xA); scenario.create_system_objects(); - scenario.next_tx(@0xA); // Get the ID of the most recent shared Clock let clock_id = most_recent_id_shared().destroy_some(); diff --git a/book/your-first-move/hello-sui.md b/book/your-first-move/hello-sui.md index b908473ca..d0648d3ef 100644 --- a/book/your-first-move/hello-sui.md +++ b/book/your-first-move/hello-sui.md @@ -174,12 +174,12 @@ INCLUDING DEPENDENCY SuiSystem INCLUDING DEPENDENCY Sui INCLUDING DEPENDENCY MoveStdlib BUILDING todo_list -Successfully verified dependencies on-chain against source. +Successfully verified dependencies onchain against source. Transaction Digest: GpcDV6JjjGQMRwHpEz582qsd5MpCYgSwrDAq1JXcpFjW ``` As you can see, when we run the `publish` command, the CLI first builds the package, then verifies -the dependencies on-chain, and finally publishes the package. The output of the command is the +the dependencies onchain, and finally publishes the package. The output of the command is the transaction digest, which is a unique identifier of the transaction and can be used to query the transaction status. @@ -354,7 +354,7 @@ $ sui client publish --gas-budget 100000000 --json ### Using the Results -After the package is published on chain, we can interact with it. To do this, we need to find the +After the package is published onchain, we can interact with it. To do this, we need to find the address (object ID) of the package. It's under the `Published Objects` section of the `Object Changes` output. The address is unique for each package, so you will need to copy it from the output. @@ -608,7 +608,7 @@ sui client object $LIST_ID --json Commands don't have to be in the same package or operate on the same object. Within a single transaction block, you can interact with multiple packages and objects. This is a powerful feature -that allows you to build complex interactions on-chain! +that allows you to build complex interactions onchain! ## Conclusion diff --git a/book/your-first-move/hello-world.md b/book/your-first-move/hello-world.md index c270b7c98..b7382898b 100644 --- a/book/your-first-move/hello-world.md +++ b/book/your-first-move/hello-world.md @@ -111,7 +111,7 @@ _module_name_tests.move_), or inside the module they're testing. Modules, imports, constants and functions can be annotated with `#[test_only]`. This attribute is used to exclude modules, functions or imports from the build process. This is useful when you want -to add helpers for your tests without including them in the code that will be published on chain. +to add helpers for your tests without including them in the code that will be published onchain. The _hello_world_tests.move_ file contains a commented out test module template: diff --git a/packages/hello_world/sources/hello_world.move b/packages/hello_world/sources/hello_world.move index 5a6190b54..eaf234b0f 100644 --- a/packages/hello_world/sources/hello_world.move +++ b/packages/hello_world/sources/hello_world.move @@ -8,6 +8,6 @@ use std::string::String; /// Returns the "Hello World!" as a `String`. public fun hello_world(): String { - b"Hello, World!".to_string() + "Hello, World!" } // ANCHOR_END: source \ No newline at end of file diff --git a/packages/hello_world/tests/hello_world_tests.move b/packages/hello_world/tests/hello_world_tests.move index 292435ecb..c03ffa910 100644 --- a/packages/hello_world/tests/hello_world_tests.move +++ b/packages/hello_world/tests/hello_world_tests.move @@ -8,6 +8,6 @@ use hello_world::hello_world; #[test] fun test_hello_world() { - assert_eq!(hello_world::hello_world(), b"Hello, World!".to_string()); + assert_eq!(hello_world::hello_world(), "Hello, World!"); } // ANCHOR_END: test \ No newline at end of file diff --git a/packages/samples/Move.lock b/packages/samples/Move.lock index b8230a878..5d33d38d6 100644 --- a/packages/samples/Move.lock +++ b/packages/samples/Move.lock @@ -5,13 +5,13 @@ version = 4 [pinned.testnet.MoveStdlib] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "ff1fe0ec455153707136cffd0dc77dfb77ba9ad5" } use_environment = "testnet" manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" deps = {} [pinned.testnet.Sui] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "22f9fc9781732d651e18384c9a8eb1dabddf73a6" } +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "ff1fe0ec455153707136cffd0dc77dfb77ba9ad5" } use_environment = "testnet" manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" deps = { MoveStdlib = "MoveStdlib" } diff --git a/packages/samples/sources/move-basics/abilities-introduction.move b/packages/samples/sources/move-basics/abilities-introduction.move new file mode 100644 index 000000000..af176a980 --- /dev/null +++ b/packages/samples/sources/move-basics/abilities-introduction.move @@ -0,0 +1,31 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_field)] +module book::abilities_introduction; + +// ANCHOR: definition +/// This struct has the `copy` and `drop` abilities. +public struct VeryAble has copy, drop { + /// The fields must support the abilities of the struct: + /// `u64` has `copy` and `drop` (and more). + value: u64, +} +// ANCHOR_END: definition + +#[test_only] +use std::unit_test::assert_eq; + +#[test] +fun test_very_able() { + // ANCHOR: use + let a = VeryAble { value: 10 }; + + // `copy`: `a` is copied into `b` - both are usable afterwards. + let b = a; + assert_eq!(a.value, b.value); + + // `drop`: neither value has to be stored or unpacked; both are + // silently discarded at the end of the function. + // ANCHOR_END: use +} diff --git a/packages/samples/sources/move-basics/address.move b/packages/samples/sources/move-basics/address.move index 4a65781d5..98b1059f9 100644 --- a/packages/samples/sources/move-basics/address.move +++ b/packages/samples/sources/move-basics/address.move @@ -21,7 +21,7 @@ let other = @sui; // ANCHOR: to_u256 use sui::address; -let addr_as_u256: u256 = address::to_u256(@0x1); +let addr_as_u256: u256 = @0x1.to_u256(); let addr = address::from_u256(addr_as_u256); // ANCHOR_END: to_u256 } @@ -32,7 +32,7 @@ let addr = address::from_u256(addr_as_u256); use sui::address; use std::string::String; -let addr_as_string: String = address::to_string(@0x1); +let addr_as_string: String = @0x1.to_string(); // ANCHOR_END: to_string } @@ -41,7 +41,7 @@ let addr_as_string: String = address::to_string(@0x1); // ANCHOR: to_bytes use sui::address; -let addr_as_u8: vector = address::to_bytes(@0x1); +let addr_as_u8: vector = @0x1.to_bytes(); let addr = address::from_bytes(addr_as_u8); // ANCHOR_END: to_bytes } diff --git a/packages/samples/sources/move-basics/assert-and-abort.move b/packages/samples/sources/move-basics/assert-and-abort.move index 3c1439f81..4b16261bb 100644 --- a/packages/samples/sources/move-basics/assert-and-abort.move +++ b/packages/samples/sources/move-basics/assert-and-abort.move @@ -16,6 +16,14 @@ if (!user_has_access) { // ANCHOR_END: abort } +#[test, expected_failure] +fun test_clean_abort() { + // ANCHOR: clean_abort + // `abort` can also be used without an explicit abort code. + abort + // ANCHOR_END: clean_abort +} + #[test] fun show_assert() { let user_has_access = true; @@ -27,6 +35,9 @@ assert!(user_has_access, 0); if (!user_has_access) { abort 0 }; + +// the abort code can be omitted +assert!(user_has_access); // ANCHOR_END: assert } @@ -50,10 +61,10 @@ public struct User { is_authorized: bool, value: u64 } // ANCHOR: error_attribute #[error] -const ENotAuthorized: vector = b"The user is not authorized to perform this action"; +const ENotAuthorized: vector = "The user is not authorized to perform this action"; #[error] -const EValueTooLow: vector = b"The value is too low, it should be at least 10"; +const EValueTooLow: vector = "The value is too low, it should be at least 10"; /// Performs an action on behalf of the user. public fun update_value(user: &mut User, value: u64) { diff --git a/packages/samples/sources/move-basics/control-flow.move b/packages/samples/sources/move-basics/control-flow.move index 95a42a91b..9a0188b3c 100644 --- a/packages/samples/sources/move-basics/control-flow.move +++ b/packages/samples/sources/move-basics/control-flow.move @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -#[allow(unused_function)] +#[allow(unused_function, untyped_literal)] // ANCHOR: module module book::control_flow; // ANCHOR_END: module @@ -16,7 +16,8 @@ fun test_if() { // `x > 0` is a boolean expression. if (x > 0) { - std::debug::print(&b"X is bigger than 0".to_string()) + let message: std::string::String = "X is bigger than 0"; + std::debug::print(&message) }; } // ANCHOR_END: if_condition @@ -33,6 +34,22 @@ fun test_if_else() { assert_eq!(y, 1); } // ANCHOR_END: if_else +// ANCHOR: else_if +// Returns a letter grade for a score from 0 to 100. +fun grade(score: u8): vector { + if (score >= 90) "A" + else if (score >= 80) "B" + else if (score >= 70) "C" + else "F" +} + +#[test] +fun test_else_if() { + assert_eq!(grade(95), "A"); + assert_eq!(grade(82), "B"); + assert_eq!(grade(40), "F"); +} +// ANCHOR_END: else_if // ANCHOR: while_loop // This function iterates over the `x` variable until it reaches 10, the // return value is the number of iterations it took to reach 10. @@ -131,6 +148,63 @@ fun test_continue_loop() { assert_eq!(x, 10) // 10 } // ANCHOR_END: continue_loop +// ANCHOR: labeled_loop +// Searches a grid (a vector of rows) for `target`, returning `true` as +// soon as it is found. The `'search` label lets the inner loop break out +// of *both* loops at once. +fun grid_contains(grid: &vector>, target: u8): bool { + let mut row = 0; + + 'search: loop { + // Ran out of rows without finding the target. + if (row >= grid.length()) break false; + + let inner = &grid[row]; + let mut col = 0; + + while (col < inner.length()) { + if (inner[col] == target) { + // Found it - break the outer `'search` loop directly, + // skipping any remaining columns and rows. + break 'search true + }; + col = col + 1; + }; + + row = row + 1; + } +} + +#[test] +fun test_grid_contains() { + let grid = vector[ + vector[1, 2, 3], + vector[4, 5, 6], + vector[7, 8, 9], + ]; + + assert_eq!(grid_contains(&grid, 5), true); + assert_eq!(grid_contains(&grid, 10), false); +} +// ANCHOR_END: labeled_loop +// ANCHOR: labeled_block +// Classifies a number, exiting the `'result` block early with `return` +// as soon as the answer is known. +fun classify(x: u64): vector { + 'result: { + if (x == 0) return 'result "zero"; + if (x % 2 == 0) return 'result "even"; + "odd" + } +} + +#[test] +fun test_labeled_block() { + assert_eq!(classify(0), "zero"); + assert_eq!(classify(4), "even"); + assert_eq!(classify(7), "odd"); +} +// ANCHOR_END: labeled_block // ANCHOR: return_statement /// This function returns `true` if `x` is greater than 0 and not 5, /// otherwise it returns `false`. diff --git a/packages/samples/sources/move-basics/copy-ability.move b/packages/samples/sources/move-basics/copy-ability.move index 719b8a99a..7024d4d01 100644 --- a/packages/samples/sources/move-basics/copy-ability.move +++ b/packages/samples/sources/move-basics/copy-ability.move @@ -11,12 +11,16 @@ public struct Copyable has copy {} #[test] fun test_copy() { // ANCHOR: copyable_test -let a = Copyable {}; // allowed because the Copyable struct has the `copy` ability -let b = a; // `a` is copied to `b` -let c = *&b; // explicit copy via dereference operator +let a = Copyable {}; -// Copyable doesn't have the `drop` ability, so every instance (a, b, and c) must -// be used or explicitly destructured. The `drop` ability is explained below. +// `a` is copied into `b` implicitly - both are usable afterwards. +let b = a; + +// The `copy` keyword makes the copy explicit. +let c = copy a; + +// `Copyable` does not have the `drop` ability, so every instance - +// `a`, `b`, and `c` - has to be used. Here, we unpack all of them. let Copyable {} = a; let Copyable {} = b; let Copyable {} = c; diff --git a/packages/samples/sources/move-basics/enum-and-match-2.move b/packages/samples/sources/move-basics/enum-and-match-2.move index f0e8bdbf7..0220ba026 100644 --- a/packages/samples/sources/move-basics/enum-and-match-2.move +++ b/packages/samples/sources/move-basics/enum-and-match-2.move @@ -6,6 +6,7 @@ module book::segment_tests; use book::segment; +use std::string::String; #[test] fun test_full_enum_cycle() { @@ -14,14 +15,14 @@ fun test_full_enum_cycle() { // Create a vector of different Segment variants. let segments = vector[ segment::new_empty(), - segment::new_string(b"hello".to_string()), - segment::new_special(b" ", 0), // plaintext - segment::new_string(b"move".to_string()), - segment::new_special(b"21", 1), // hex + segment::new_string("hello"), + segment::new_special(" ", 0), // utf8 + segment::new_string("move"), + segment::new_special("!", 1), // ascii ]; // Aggregate all segments into the final string using `vector::fold!` macro. - let result = segments.fold!(b"".to_string(), |mut acc, segment| { + let result = segments.fold!("", |mut acc: String, segment| { // Do not append empty, only `Special` and `String`. if (!segment.is_empty()) { acc.append(segment.to_string()); @@ -30,6 +31,6 @@ fun test_full_enum_cycle() { }); // Check that the result is what's expected. - assert_eq!(result, b"hello move!".to_string()); + assert_eq!(result, "hello move!"); } // ANCHOR_END: enum_test diff --git a/packages/samples/sources/move-basics/enum-and-match.move b/packages/samples/sources/move-basics/enum-and-match.move index d7a17e0d7..27f7b7f0e 100644 --- a/packages/samples/sources/move-basics/enum-and-match.move +++ b/packages/samples/sources/move-basics/enum-and-match.move @@ -45,9 +45,9 @@ public struct Segments(vector) has copy, drop; fun test_segments() { let _ = Segments(vector[ Segment::Empty, - Segment::String(b"hello".to_string()), - Segment::String(b" move".to_string()), - Segment::Special { content: b"21", encoding: 1 }, + Segment::String("hello"), + Segment::String(" move"), + Segment::Special { content: "21", encoding: 1 }, ]); } // ANCHOR_END: struct @@ -107,18 +107,19 @@ public fun try_into_inner_string(s: Segment): Option { public fun to_string(s: &Segment): String { match (*s) { // Return an empty string. - Segment::Empty => b"".to_string(), + Segment::Empty => "", // Return the inner string. Segment::String(str) => str, // Return the decoded contents based on the encoding. Segment::Special { content, encoding } => { - // Perform a match on the encoding, we only support 0 - ut8, 1 - hex. + // Perform a match on the encoding; we support 0 - UTF-8 and 1 - ASCII. match (encoding) { - // Plain encoding, return content. + // UTF-8 encoding, interpret content as a UTF-8 string. 0 => content.to_string(), - // HEX encoding, decode and return. - 1 => sui::hex::decode(content).to_string(), + // ASCII encoding - stricter, aborts on non-ASCII bytes. + 1 => content.to_ascii_string().to_string(), // We have to provide a wildcard pattern, because values of `u8` are 0-255. + // Abort execution if the encoding is unknown. _ => abort, } }, diff --git a/packages/samples/sources/move-basics/expression.move b/packages/samples/sources/move-basics/expression.move index 12b04cc02..9653485d0 100644 --- a/packages/samples/sources/move-basics/expression.move +++ b/packages/samples/sources/move-basics/expression.move @@ -19,6 +19,7 @@ let h = 0x0A; // 0x0A is a literal let v = b"hello"; // b"hello" is a byte vector literal let x = x"0A"; // x"0A" is a byte vector literal let c = vector[1, 2, 3]; // vector[] is a vector literal +let s: std::string::String = "hello"; // "hello" is a string literal // ANCHOR_END: literals // ANCHOR: operators diff --git a/packages/samples/sources/move-basics/function.move b/packages/samples/sources/move-basics/function.move index c81fd06bd..a5f154a3a 100644 --- a/packages/samples/sources/move-basics/function.move +++ b/packages/samples/sources/move-basics/function.move @@ -30,7 +30,7 @@ fun return_nothing() { // ANCHOR: tuple_return fun get_name_and_age(): (vector, u8) { - (b"John", 25) + ("John", 25) } // ANCHOR_END: tuple_return @@ -39,7 +39,7 @@ fun get_name_and_age(): (vector, u8) { // Tuple must be destructured to access its elements. // Name and age are declared as immutable variables. let (name, age) = get_name_and_age(); -assert_eq!(name, b"John"); +assert_eq!(name, "John"); assert_eq!(age, 25); // ANCHOR_END: tuple_return_imm diff --git a/packages/samples/sources/move-basics/generics.move b/packages/samples/sources/move-basics/generics.move index dac0533c8..1145b1caa 100644 --- a/packages/samples/sources/move-basics/generics.move +++ b/packages/samples/sources/move-basics/generics.move @@ -106,6 +106,35 @@ public fun update_age(user: &mut User, age: u8) { } // ANCHOR_END: update_user +// ANCHOR: test_user +#[test] +fun test_user() { + // In this instance, the `metadata` field is a `u64`... + let mut user1 = User { + name: "Alice", + age: 30, + metadata: 1000u64, + }; + + // ...and in this instance, it is a `bool`. + let mut user2 = User { + name: "Bob", + age: 40, + metadata: true, + }; + + // The same functions work for both instances. + user1.update_name("Alice II"); + user2.update_name("Bob II"); + + assert_eq!(user1.name, "Alice II"); + assert_eq!(user2.name, "Bob II"); + + let User { .. } = user1; + let User { .. } = user2; +} +// ANCHOR_END: test_user + // ANCHOR: phantom /// A generic type with a phantom type parameter. public struct Coin { @@ -122,6 +151,10 @@ fun test_phantom_type() { let coin1: Coin = Coin { value: 10 }; let coin2: Coin = Coin { value: 20 }; + // This line will not compile: `Coin` and `Coin` + // are different types and cannot be mixed up. + // let mixed: Coin = coin2; + // Unpacking is identical because the phantom type parameter is not used. let Coin { value: _ } = coin1; let Coin { value: _ } = coin2; diff --git a/packages/samples/sources/move-basics/internal-permit-2.move b/packages/samples/sources/move-basics/internal-permit-2.move new file mode 100644 index 000000000..704b9cdca --- /dev/null +++ b/packages/samples/sources/move-basics/internal-permit-2.move @@ -0,0 +1,33 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: use_permit +/// A module that registers its own type in the `type_registry`. +module book::registry_user; + +use book::type_registry::Registry; + +/// The type we are going to register. +public struct MyApp() + +/// Registers `MyApp` in the given registry. The permit can only be +/// created here - in the module that defines `MyApp`. +public fun register_my_app(registry: &mut Registry) { + let permit = internal::permit(); + registry.register(permit, "My App"); +} +// ANCHOR_END: use_permit + +#[test_only] +use std::unit_test::assert_eq; +#[test_only] +use book::type_registry; + +#[test] +fun test_register_my_app() { + // ANCHOR: test + let mut registry = type_registry::new(); + register_my_app(&mut registry); + assert_eq!(registry.size(), 1); + // ANCHOR_END: test +} diff --git a/packages/samples/sources/move-basics/internal-permit.move b/packages/samples/sources/move-basics/internal-permit.move new file mode 100644 index 000000000..1d69711ca --- /dev/null +++ b/packages/samples/sources/move-basics/internal-permit.move @@ -0,0 +1,32 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: registry +/// A registry where a type can be registered under a human-readable +/// name, but only by the module that defines the type. +module book::type_registry; + +use std::string::String; + +/// Stores the names of registered types. +public struct Registry has drop { + names: vector, +} + +/// Creates a new, empty `Registry`. +public fun new(): Registry { + Registry { names: vector[] } +} + +/// Registers the type `T` under the given `name`. The `Permit` +/// argument proves that the call was authorized by the module +/// that defines `T`. +public fun register(registry: &mut Registry, _permit: internal::Permit, name: String) { + registry.names.push_back(name); +} + +/// Returns the number of registered types. +public fun size(registry: &Registry): u64 { + registry.names.length() +} +// ANCHOR_END: registry diff --git a/packages/samples/sources/move-basics/macros.move b/packages/samples/sources/move-basics/macros.move new file mode 100644 index 000000000..4f611e265 --- /dev/null +++ b/packages/samples/sources/move-basics/macros.move @@ -0,0 +1,67 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module book::macros; + +#[test_only] +use std::unit_test::assert_eq; + +// ANCHOR: max +/// Returns the larger of the two values. +public macro fun max<$T>($a: $T, $b: $T): $T { + let a = $a; + let b = $b; + if (a > b) a else b +} +// ANCHOR_END: max + +#[test] +fun test_max() { + // ANCHOR: max_use + assert_eq!(max!(1, 2), 2); + assert_eq!(max!(10u8, 5), 10); + assert_eq!(max!(100u128, 200), 200); + // ANCHOR_END: max_use +} + +// ANCHOR: repeat +/// Calls the `$f` lambda `$n` times, passing in the iteration number. +public macro fun repeat($n: u64, $f: |u64|) { + let n = $n; + let mut i = 0; + while (i < n) { + $f(i); + i = i + 1; + } +} +// ANCHOR_END: repeat + +#[test] +fun test_repeat() { + // ANCHOR: repeat_use + let mut sum = 0; + repeat!(4, |i| sum = sum + i); + assert_eq!(sum, 6); // 0 + 1 + 2 + 3 + // ANCHOR_END: repeat_use +} + +#[test] +fun test_std_macros() { + // ANCHOR: std_macros + // `Option` macros: `destroy_or!` unwraps the value with a default... + let opt = option::some(10); + assert_eq!(opt.destroy_or!(0), 10); + + // ...and `map!` transforms the inner value, if it is present. + let doubled = option::some(5).map!(|x| x * 2); + assert_eq!(doubled, option::some(10)); + + // Integer macros iterate over numbers without a `while` loop. + let mut sum = 0u64; + 10u64.do!(|i| sum = sum + i); + assert_eq!(sum, 45); // 0 + 1 + ... + 9 + + // And the `assert_eq!` macro, used all over this book, is + // defined in the `std::unit_test` module. + // ANCHOR_END: std_macros +} diff --git a/packages/samples/sources/move-basics/module-members.move b/packages/samples/sources/move-basics/module-members.move index 9cc91086d..801839a5a 100644 --- a/packages/samples/sources/move-basics/module-members.move +++ b/packages/samples/sources/move-basics/module-members.move @@ -5,18 +5,15 @@ // ANCHOR: members module book::my_module_with_members; -// import +// import - brings the `my_module` module into scope use book::my_module; -// a constant +// a constant - an immutable, module-private value const CONST: u8 = 0; -// a struct +// a struct - a custom data type public struct Struct {} -// method alias -public use fun function as Struct.struct_fun; - -// function -fun function(_: &Struct) { /* function body */ } +// a function - a unit of executable code +fun function() { /* function body */ } // ANCHOR_END: members diff --git a/packages/samples/sources/move-basics/option.move b/packages/samples/sources/move-basics/option.move index 27b231c7e..7706b37cc 100644 --- a/packages/samples/sources/move-basics/option.move +++ b/packages/samples/sources/move-basics/option.move @@ -27,27 +27,61 @@ public fun register( #[test_only] use std::unit_test::{assert_eq, assert_ref_eq}; -#[test] fun use_option() { +#[test] fun register_users() { +// ANCHOR: registry_use +// A user with a middle name... +let ada = register( + "Ada", + option::some("King"), + "Lovelace", +); + +// ...and a user without one. No reserved values, no guesswork. +let grace = register( + "Grace", + option::none(), + "Hopper", +); +// ANCHOR_END: registry_use +} +#[test] fun use_option() { // ANCHOR: usage -// `option::some` creates an `Option` value with a value. -let mut opt = option::some(b"Alice"); +// `option::some` creates an option holding a value. +let mut opt: Option = option::some("Alice"); -// `option::none` creates an `Option` without a value. We need to specify the -// type since it can't be inferred from context. -let empty : Option = option::none(); +// `option::none` creates an empty option. The element type has to +// be specified when it cannot be inferred from use. +let empty: Option = option::none(); -// `option.is_some()` returns true if option contains a value. +// Checking the state of an option. assert_eq!(opt.is_some(), true); assert_eq!(empty.is_none(), true); -// internal value can be `borrow`ed and `borrow_mut`ed. -assert_ref_eq!(opt.borrow(), &b"Alice"); +// `borrow` reads the value without taking it out of the option. +assert_ref_eq!(opt.borrow(), &"Alice"); -// `option.extract` takes the value out of the option, leaving the option empty. +// `extract` takes the value out, leaving the option empty. let inner = opt.extract(); - -// `option.is_none()` returns true if option is None. +assert_eq!(inner, "Alice"); assert_eq!(opt.is_none(), true); // ANCHOR_END: usage } + +#[test] fun option_macros() { +// ANCHOR: macros +// `destroy_or!` consumes the option, returning a default when empty. +let value = option::some(10u8).destroy_or!(0); +assert_eq!(value, 10); + +let missing = option::none().destroy_or!(0); +assert_eq!(missing, 0); + +// `is_some_and!` tests the value against a condition. +let is_big = option::some(10u8).is_some_and!(|n| *n > 5); +assert_eq!(is_big, true); + +// `do!` runs the lambda only when there is a value. +option::some(10u8).do!(|n| assert_eq!(n, 10)); +// ANCHOR_END: macros +} diff --git a/packages/samples/sources/move-basics/ownership-and-scope.move b/packages/samples/sources/move-basics/ownership-and-scope.move new file mode 100644 index 000000000..8c62c8a5e --- /dev/null +++ b/packages/samples/sources/move-basics/ownership-and-scope.move @@ -0,0 +1,86 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_variable)] +module book::ownership; + +// ANCHOR: coin +/// A struct representing a digital asset. Note that `Coin` has no +/// abilities: its value cannot be copied and cannot be discarded. +public struct Coin { value: u64 } + +/// Creates a new `Coin`. The new value is returned, and its ownership +/// is transferred to the caller of the function. +public fun mint(value: u64): Coin { + Coin { value } +} + +/// Takes ownership of a `Coin` and destroys it by unpacking. +public fun spend(coin: Coin) { + let Coin { value: _ } = coin; // the coin is destroyed here +} +// ANCHOR_END: coin + +// ANCHOR: scope +public fun scope() { + // `a` is not yet declared and cannot be used here + let a = 1u8; // `a` comes into scope and is owned by `scope` + // `a` can be used here +} // scope ends; `a` goes out of scope +// ANCHOR_END: scope + +#[test_only] +use std::unit_test::assert_eq; + +#[test] +fun test_move_semantics() { + // ANCHOR: move_to_function + let coin = mint(100); // the test function owns the coin + spend(coin); // ownership of the value moves into `spend` + // `coin` can no longer be used here + // ANCHOR_END: move_to_function + + // ANCHOR: move_to_variable + let coin = mint(100); + let new_owner = coin; // the value moves from `coin` to `new_owner` + // `coin` can no longer be used here + spend(new_owner); + // ANCHOR_END: move_to_variable +} + +#[test] +fun test_copy_semantics() { + // ANCHOR: copy_types + let x = 10u64; + let y = x; // `x` is copied into `y`, not moved + + // both `x` and `y` can be used after the assignment + assert_eq!(x, y); + // ANCHOR_END: copy_types + + // ANCHOR: explicit_move + let x = 10u64; + let y = move x; // explicitly move `x` instead of copying it + // `x` can no longer be used here + // ANCHOR_END: explicit_move +} + +#[test] +fun test_scopes_with_blocks() { + // ANCHOR: blocks + let x = 1u8; + { + let y = 2u8; // `y` is owned by the block + let z = x + y; // variables from the outer scope are accessible + }; // block ends; `y` and `z` go out of scope + // only `x` can be used here + // ANCHOR_END: blocks + + // ANCHOR: block_return + let x = { + let y = 2u8; + y + 1 // the result of the block moves to `x` + }; // `y` goes out of scope + assert_eq!(x, 3); + // ANCHOR_END: block_return +} diff --git a/packages/samples/sources/move-basics/primitive-types.move b/packages/samples/sources/move-basics/primitive-types.move index 2c8d5b6b1..c863e7e0a 100644 --- a/packages/samples/sources/move-basics/primitive-types.move +++ b/packages/samples/sources/move-basics/primitive-types.move @@ -1,49 +1,96 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -module book::primitive_types { - -#[test, allow(unused_variable, unused_let_mut)] -fun test_primitive_types() { +module book::primitive_types; +#[test, allow(unused_variable, unused_let_mut, unused_assignment)] +fun variables_and_assignment() { // ANCHOR: variables_and_assignment +// The type annotation is optional when it can be inferred. let x: bool = true; -let mut y: u8 = 42; +let y = 10u8; + +// A `mut` variable can be reassigned with the `=` operator. +let mut z: u8 = 42; +z = 43; // ANCHOR_END: variables_and_assignment // ANCHOR: shadowing let x: u8 = 42; -let x: u16 = 42; + +// The new `x` replaces the previous one, and may +// even have a different type. +let x: u16 = (x as u16) + 1; // ANCHOR_END: shadowing +} +#[test, allow(unused_variable)] +fun booleans() { // ANCHOR: boolean -let x = true; -let y = false; +// The type of a boolean is always inferred. +let is_ready = true; +let is_done = false; + +// Logical operators: `&&` (and), `||` (or), and `!` (not). +let in_progress = is_ready && !is_done; // ANCHOR_END: boolean +} +#[test, allow(unused_variable)] +fun integers() { // ANCHOR: integers -let x: u8 = 42; -let y: u16 = 42; -// ... -let z: u256 = 42; +let small: u8 = 42; +let medium: u16 = 1_000; // underscores improve readability +let large: u256 = 100_000_000_000; +let hex: u64 = 0x2A; // hexadecimal literal, 42 // ANCHOR_END: integers // ANCHOR: integer_explicit_type -// Both are equivalent +// Both are equivalent. let x: u8 = 42; let x = 42u8; // ANCHOR_END: integer_explicit_type +// ANCHOR: comparison +let a = 10u8; +let b = 20u8; + +// Comparison produces a `bool`; the operands must be of the same type. +let is_less = a < b; // true +let is_equal = a == b; // false +// ANCHOR_END: comparison +} + +#[test, allow(unused_variable)] +fun casting() { // ANCHOR: cast_as let x: u8 = 42; let y: u16 = x as u16; -let z = 2 * (x as u16); // ambiguous, requires parentheses +let z = 2 * (x as u16); // ambiguity requires parentheses // ANCHOR_END: cast_as // ANCHOR: overflow +// The same values that would overflow `u8` arithmetic +// fit comfortably once upcast to `u16`. let x: u8 = 255; let y: u8 = 255; let z: u16 = (x as u16) + ((y as u16) * 2); // ANCHOR_END: overflow } + +#[test, expected_failure(arithmetic_error, location = Self)] +fun downcast_abort() { +// ANCHOR: downcast +let x: u16 = 300; +let y = x as u8; // ABORTS! 300 does not fit into `u8` +// ANCHOR_END: downcast +} + +#[test, expected_failure(arithmetic_error, location = Self)] +fun overflow_abort() { +// ANCHOR: overflow_abort +let x = 255u8; +let y = 1u8; +let z = x + y; // ABORTS! The result does not fit into `u8` +// ANCHOR_END: overflow_abort } diff --git a/packages/samples/sources/move-basics/references.move b/packages/samples/sources/move-basics/references.move index 59931d90f..ee19be509 100644 --- a/packages/samples/sources/move-basics/references.move +++ b/packages/samples/sources/move-basics/references.move @@ -44,6 +44,28 @@ public fun recycle(card: Card) { } // ANCHOR_END: move +// ANCHOR: getter +/// Getter: a reference to the `uses` field, derived from +/// the `card` reference taken as an argument. +public fun uses(card: &Card): &u8 { + &card.uses +} +// ANCHOR_END: getter + +#[test] +fun test_getter() { + let mut card = purchase(); + assert!(*card.uses() == 3); + + enter_metro(&mut card); + assert!(*card.uses() == 2); + + // spend the remaining rides and recycle the card + enter_metro(&mut card); + enter_metro(&mut card); + recycle(card); +} + // ANCHOR: test #[test] fun test_card() { @@ -61,6 +83,26 @@ fun test_card() { } // ANCHOR_END: test +// ANCHOR: deref +#[test] +fun test_dereference() { + let mut card = purchase(); + + // A reference to the `uses` field - a `u8` value. + let uses_ref = &card.uses; + + // The dereference operator `*` copies the value behind the reference. + let uses: u8 = *uses_ref; + assert!(uses == 3); + + // Writing through a mutable reference is also a dereference. + *(&mut card.uses) = 0; + assert!(card.uses == 0); + + recycle(card); +} +// ANCHOR_END: deref + // ANCHOR: move_2024 #[test] fun test_card_2024() { diff --git a/packages/samples/sources/move-basics/string.move b/packages/samples/sources/move-basics/string.move index 84cd76ef7..b907e1507 100644 --- a/packages/samples/sources/move-basics/string.move +++ b/packages/samples/sources/move-basics/string.move @@ -21,25 +21,34 @@ public fun bytes(self: &MyString): &vector { } // ANCHOR_END: custom -// use std::ascii::String; - -#[allow(unused_variable)] -#[test] -fun using_strings() { -// ANCHOR: ascii -// the module is `std::ascii` and the type is `String` -use std::ascii::{Self, String}; - -// strings can be created using the `string` function -// type declaration is not necessary, we put it here for clarity -let hey: String = ascii::string(b"Hey"); - -// there is a handy alias `.to_ascii_string()` on the `vector` type -let hey = b"Hey".to_ascii_string(); - -let beep: vector = "\x0a \\ haha"; - -// ANCHOR_END: ascii +#[test_only] +use std::unit_test::assert_eq; + +#[test] fun string_literals() { +// ANCHOR: literals +// The type of a string literal is inferred from the context: +// it can be a UTF-8 `String`... +let hello: std::string::String = "Hello"; + +// ...an ASCII `String`... +let ascii: std::ascii::String = "ASCII"; + +// ...or a plain vector of bytes. +let bytes: vector = "Hello"; + +// A byte string literal always yields a `vector`. +let bytes = b"Hello"; + +// So does a hex string literal: each pair of hex digits is one byte. +let bytes: vector = x"48656C6C6F"; // "Hello" +// ANCHOR_END: literals + +// ANCHOR: escapes +// Special characters are written with the `\` escape: `\n` - newline, +// `\r` - carriage return, `\t` - tab, `\\` - backslash, `\"` - double +// quote, and `\xHH` - a byte written as two hex digits. +let escaped: std::string::String = "Quote: \"...\"\nNew line,\ttab, \\ and \x41 is 'A'"; +// ANCHOR_END: escapes } #[test] fun using_strings_utf8() { @@ -56,16 +65,117 @@ let hello = b"Hello".to_string(); // ANCHOR_END: utf8 } +#[test] fun common_operations() { +use std::string::String; + +// ANCHOR: common_ops +let mut str: String = "Hello,"; +let another: String = " World!"; + +// `append(String)` adds the content to the end of the string. +str.append(another); +assert_eq!(str, "Hello, World!"); + +// `substring(start, end)` copies a slice of the string. +assert_eq!(str.substring(0, 5), "Hello"); + +// `index_of(&String)` returns the index of the first occurrence... +assert_eq!(str.index_of(&"World"), 7); + +// ...or the length of the string if there is no occurrence. +assert_eq!(str.index_of(&"Rust"), str.length()); + +// Strings can be compared with `==` and `!=`; the comparison is +// done byte by byte. +assert!(str == "Hello, World!"); + +// `length()` returns the number of bytes in the string. +assert_eq!(str.length(), 13); + +// Methods can also be chained! Get the length of a substring. +assert_eq!(str.substring(0, 5).length(), 5); + +// `is_empty()` returns true if the string is empty. +assert_eq!(str.is_empty(), false); + +// `as_bytes()` returns the underlying byte vector for custom operations. +let bytes: &vector = str.as_bytes(); +// ANCHOR_END: common_ops +} + +#[test] fun number_to_string() { +// ANCHOR: number_to_string +// Every unsigned integer type has a `to_string` method, which +// converts the number into its decimal representation. +assert_eq!(42u64.to_string(), "42"); +assert_eq!(255u8.to_string(), "255"); +assert_eq!(1000000u128.to_string(), "1000000"); +// ANCHOR_END: number_to_string +} + #[test] fun safe_strings() { +use std::string; + // ANCHOR: safe_utf8 -// this is a valid UTF-8 string +// `try_utf8` returns `Some(String)` if the bytes are valid UTF-8... +let hello = string::try_utf8(b"Hello"); +assert_eq!(hello.is_some(), true); + +// ...and `None` if they are not. +let invalid = string::try_utf8(b"\xFF"); +assert_eq!(invalid.is_none(), true); + +// The `.try_to_string()` alias on `vector` does the same. let hello = b"Hello".try_to_string(); +assert_eq!(hello.is_some(), true); +// ANCHOR_END: safe_utf8 +} -assert!(hello.is_some()); // abort if the value is not valid UTF-8 +#[test] fun utf8_limitations() { +use std::string::String; -// this is not a valid UTF-8 string -let invalid = b"\xFF".try_to_string(); +// ANCHOR: limitations +// `length()` returns the number of bytes, not characters! +let ascii_only: String = "hello"; // 5 characters, 5 bytes +let accented: String = "héllo"; // 5 characters, 6 bytes +let emoji: String = "🥳"; // 1 character, 4 bytes -assert!(invalid.is_none()); // abort if the value is valid UTF-8 -// ANCHOR_END: safe_utf8 +assert_eq!(ascii_only.length(), 5); +assert_eq!(accented.length(), 6); +assert_eq!(emoji.length(), 4); +// ANCHOR_END: limitations +} + +// ANCHOR: substring_abort +#[test, expected_failure] +fun test_substring_aborts_mid_character() { + let s: std::string::String = "héllo"; + // 'é' occupies bytes 1 and 2 - slicing through it aborts + let _ = s.substring(0, 2); +} +// ANCHOR_END: substring_abort + +#[test] fun using_ascii_strings() { +// ANCHOR: ascii +// The `.to_ascii_string()` alias on `vector` constructs an +// `ascii::String`; it aborts if any byte is not valid ASCII. +let hey = b"Hey".to_ascii_string(); + +// ASCII strings provide the same core operations as UTF-8 strings: +// `length`, `append`, `insert`, `substring`, `index_of`, and so on. +assert_eq!(hey.length(), 3); + +// As well as some unique ones, like changing the case... +assert_eq!(hey.to_uppercase(), "HEY"); +assert_eq!(hey.to_lowercase(), "hey"); + +// ...and checking if all characters are printable. +assert_eq!(hey.all_characters_printable(), true); + +// An `ascii::String` can always be converted into a UTF-8 `String`, +let hey_utf8 = hey.to_string(); + +// and a UTF-8 `String` - into ASCII, if its contents allow it. +let hey_ascii = hey_utf8.to_ascii(); +// ANCHOR_END: ascii } diff --git a/packages/samples/sources/move-basics/struct-methods-3.move b/packages/samples/sources/move-basics/struct-methods-3.move index 8ef9ce7b6..54e45528d 100644 --- a/packages/samples/sources/move-basics/struct-methods-3.move +++ b/packages/samples/sources/move-basics/struct-methods-3.move @@ -1,31 +1,24 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -// ANCHOR: hero_to_bytes -// TODO: better example (external module...) -module book::hero_to_bytes; +// ANCHOR: string_alias +module book::string_alias; -// Alias for the `bcs::to_bytes` method. Imported aliases should be defined -// in the top of the module. -// public use fun bcs::to_bytes as Hero.to_bytes; +use std::string::String; -/// A struct representing a hero. -public struct Hero has drop { - health: u8, - mana: u8, -} - -/// Create a new Hero. -public fun new(): Hero { Hero { health: 100, mana: 100 } } +/// Alias `std::string::length` as `String.num_bytes`. +/// A local alias can be declared for any type, even an external one. +use fun std::string::length as String.num_bytes; -// #[test_only] -// use std::unit_test::assert_eq; +#[test_only] +use std::unit_test::assert_eq; #[test] -// Test the methods of the `Hero` struct. -fun test_hero_serialize() { - // let mut hero = new(); - // let serialized = hero.to_bytes(); - // assert_eq!(serialized.length(), 3); +fun test_string_alias() { + let s: String = "Hello"; + + // Same function, two names: the built-in method and our alias. + assert_eq!(s.length(), 5); + assert_eq!(s.num_bytes(), 5); } -// ANCHOR_END: hero_to_bytes +// ANCHOR_END: string_alias diff --git a/packages/samples/sources/move-basics/struct.move b/packages/samples/sources/move-basics/struct.move index f7afd5a49..bcd5a7b6a 100644 --- a/packages/samples/sources/move-basics/struct.move +++ b/packages/samples/sources/move-basics/struct.move @@ -4,9 +4,9 @@ #[allow(unused_variable, unused_field)] module book::struct_syntax; +// ANCHOR: def use std::string::String; -// ANCHOR: def /// A struct representing an artist. public struct Artist { /// The name of the artist. @@ -23,48 +23,112 @@ public struct Record { year: u16, /// Whether the record is a debut album. is_debut: bool, - /// The edition of the record. + /// The edition of the record, if defined. edition: Option, } // ANCHOR_END: def +// ANCHOR: positional_def +/// The duration of a record: minutes and seconds. +public struct Duration(u64, u64) +// ANCHOR_END: positional_def + +// ANCHOR: getter +/// Returns the name of the artist. A "getter" for the `name` field. +public fun name(artist: &Artist): String { + artist.name +} +// ANCHOR_END: getter + +// ANCHOR: setter +/// Updates the name of the artist. A "setter" for the `name` field. +public fun set_name(artist: &mut Artist, name: String) { + artist.name = name; +} +// ANCHOR_END: setter + #[test_only] use std::unit_test::assert_eq; -#[test] fun test_pack_unpack() { +#[test] +fun test_pack_access_unpack() { + // ANCHOR: pack + let mut artist = Artist { + name: "The Beatles", + }; + // ANCHOR_END: pack + + // ANCHOR: pack_shorthand + let name: String = "Queen"; -// ANCHOR: pack -let mut artist = Artist { - name: b"The Beatles".to_string() -}; -// ANCHOR_END: pack + // The local variable `name` has the same name as the field, so + // instead of `Artist { name: name }` we can write: + let queen = Artist { name }; + // ANCHOR_END: pack_shorthand -// ANCHOR: access -// Access the `name` field of the `Artist` struct. -let artist_name = artist.name; + // ANCHOR: access + // Read the `name` field of the `Artist` struct. + assert_eq!(artist.name, "The Beatles"); -// Access a field of the `Artist` struct. -assert_eq!(artist.name, b"The Beatles".to_string()); + // Mutate the `name` field. Requires `artist` to be declared as `mut`. + artist.name = "Led Zeppelin"; -// Mutate the `name` field of the `Artist` struct. -artist.name = b"Led Zeppelin".to_string(); + // Check that the `name` field has been mutated. + assert_eq!(artist.name, "Led Zeppelin"); + // ANCHOR_END: access -// Check that the `name` field has been mutated. -assert_eq!(artist.name, b"Led Zeppelin".to_string()); -// ANCHOR_END: access + // ANCHOR: getter_setter_use + // Call the setter and then the getter defined above. + artist.set_name("Pink Floyd"); + assert_eq!(artist.name(), "Pink Floyd"); + // ANCHOR_END: getter_setter_use + + // ANCHOR: unpack + // Unpack the `Artist` struct, binding the value of the `name` + // field to a new variable `name`. + let Artist { name } = artist; + // ANCHOR_END: unpack + + // ANCHOR: unpack_ignore + // Unpack the `Artist` struct and ignore the `name` field. + let Artist { name: _ } = queen; + // ANCHOR_END: unpack_ignore +} + +#[test] +fun test_unpack_rest() { + // ANCHOR: unpack_rest + let record = Record { + title: "Abbey Road", + artist: Artist { name: "The Beatles" }, + year: 1969, + is_debut: false, + edition: option::none(), + }; + + // Unpack the `Record`, keeping `title` and `artist`, and + // ignoring all of the other fields with `..`. + let Record { title, artist, .. } = record; + + assert_eq!(title, "Abbey Road"); + + // The `artist` binding holds a non-discardable `Artist` value, + // so it, in turn, must be unpacked as well. + let Artist { name: _ } = artist; + // ANCHOR_END: unpack_rest +} -// ANCHOR: unpack -// Unpack the `Artist` struct and create a new variable `name` -// with the value of the `name` field. -let Artist { name } = artist; -// ANCHOR_END: unpack +#[test] +fun test_positional() { + // ANCHOR: positional_use + // Pack a positional struct - parentheses instead of curly braces. + let duration = Duration(3, 5); -let artist = Artist { - name: b"The Beatles".to_string() -}; + // Access the fields by their position, starting at 0. + assert_eq!(duration.0, 3); + assert_eq!(duration.1, 5); -// ANCHOR: unpack_ignore -// Unpack the `Artist` struct and ignore the `name` field. -let Artist { name: _ } = artist; -// ANCHOR_END: unpack_ignore + // Unpack the struct, binding each field by its position. + let Duration(minutes, seconds) = duration; + // ANCHOR_END: positional_use } diff --git a/packages/samples/sources/move-basics/type-reflection.move b/packages/samples/sources/move-basics/type-reflection.move index 437c3ffe6..cbcf523b8 100644 --- a/packages/samples/sources/move-basics/type-reflection.move +++ b/packages/samples/sources/move-basics/type-reflection.move @@ -34,6 +34,6 @@ use std::unit_test::assert_eq; fun test_type_reflection() { let (type_name, module_name, _address_str) = do_i_know_you(); - assert_eq!(module_name, b"type_reflection".to_ascii_string()); + assert_eq!(module_name, "type_reflection"); } // ANCHOR_END: main diff --git a/packages/samples/sources/move-basics/vector.move b/packages/samples/sources/move-basics/vector.move index 8695e2ecb..7054ceae4 100644 --- a/packages/samples/sources/move-basics/vector.move +++ b/packages/samples/sources/move-basics/vector.move @@ -5,6 +5,7 @@ module book::vector_syntax { #[test_only] use std::unit_test::assert_eq; + #[test] fun test_vector() { // ANCHOR: literal // An empty vector of bool elements. @@ -21,21 +22,74 @@ let vv: vector> = vector[ // ANCHOR_END: literal } +#[test] fun vector_access() { +// ANCHOR: access +let v: vector = vector[10, 20, 30]; + +// `length` returns the number of elements. +assert_eq!(v.length(), 3); +assert_eq!(v.is_empty(), false); + +// The index syntax borrows an element; for copyable +// types the borrowed value can be read directly. +assert_eq!(v[0], 10); + +// Accessing an index outside of bounds aborts: +// v[3]; // ABORTS! +// ANCHOR_END: access +} + #[test] fun vector_methods() { // ANCHOR: methods let mut v = vector[10u8, 20, 30]; -assert_eq!(v.length(), 3); -assert_eq!(v.is_empty(), false); +// `push_back` adds an element to the end of the vector. +v.push_back(40); // [10, 20, 30, 40] + +// `pop_back` removes the last element and returns it. +let last = v.pop_back(); // [10, 20, 30] +assert_eq!(last, 40); + +// `insert` places an element at the given index, shifting +// the elements after it to the right. +v.insert(15, 1); // [10, 15, 20, 30] -v.push_back(40); -let last_value = v.pop_back(); +// `remove` takes an element out at the given index, shifting +// the elements after it to the left. +let removed = v.remove(2); // [10, 15, 30] +assert_eq!(removed, 20); -assert_eq!(last_value, 40); +// The index syntax can also modify an element in place; the `&mut` +// and `*` in this expression are explained in the References section. +*(&mut v[0]) = 5; // [5, 15, 30] +assert_eq!(v[0], 5); // ANCHOR_END: methods } -} +#[test] fun vector_macros() { +// ANCHOR: macros +let v = vector[1u64, 2, 3, 4]; + +// `count!` returns the number of elements matching the condition. +let even_count = v.count!(|n| *n % 2 == 0); +assert_eq!(even_count, 2); + +// `map!` transforms each element, returning a new vector. +let doubled = v.map!(|n| n * 2); +assert_eq!(doubled, vector[2, 4, 6, 8]); + +// `fold!` collapses the vector into a single value, +// in this case - the sum of all elements. +let sum = v.fold!(0, |acc, n| acc + n); +assert_eq!(sum, 10); + +// `do!` calls the function on each element of the vector. +let mut total = 0u64; +v.do!(|n| total = total + n); +assert_eq!(total, 10); +// ANCHOR_END: macros +} +} module book::non_droppable_vec { diff --git a/packages/samples/sources/programmability/address-balances.move b/packages/samples/sources/programmability/address-balances.move new file mode 100644 index 000000000..b2bacae9e --- /dev/null +++ b/packages/samples/sources/programmability/address-balances.move @@ -0,0 +1,47 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module book::address_balances; + +use sui::balance::{Self, Balance}; +use sui::coin::{Self, Coin}; +use sui::funds_accumulator::Withdrawal; + +// ANCHOR: pay +/// Deposit a coin into the recipient's address balance. +public fun pay(coin: Coin, recipient: address) { + coin.send_funds(recipient); +} +// ANCHOR_END: pay + +// ANCHOR: collect +/// Redeem a withdrawal provided by the transaction into a spendable coin. +public fun collect(w: Withdrawal>, ctx: &mut TxContext): Coin { + coin::redeem_funds(w, ctx) +} +// ANCHOR_END: collect + +// ANCHOR: split_join +public fun inspect_and_split(w: &mut Withdrawal>): (address, u256) { + // Read who the funds belong to and how much may still be withdrawn. + let owner = w.owner(); + let remaining = w.limit(); + + // Carve off a sub-withdrawal with its own, smaller limit. The + // parent's limit is reduced by the same amount. + let sub: Withdrawal> = w.split(100); + + // Withdrawals from the same owner can be joined back together, + // adding the limits up. + w.join(sub); + + (owner, remaining) +} +// ANCHOR_END: split_join + +// ANCHOR: object_withdraw +/// Withdraw `value` units of `T` held at this object's address. +public fun withdraw(id: &mut UID, value: u64): Withdrawal> { + balance::withdraw_funds_from_object(id, value) +} +// ANCHOR_END: object_withdraw diff --git a/packages/samples/sources/programmability/balance-and-coin-2.move b/packages/samples/sources/programmability/balance-and-coin-2.move new file mode 100644 index 000000000..fc5381052 --- /dev/null +++ b/packages/samples/sources/programmability/balance-and-coin-2.move @@ -0,0 +1,39 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: gold +/// A module that creates the GOLD currency on package publish. +module book::gold; + +use sui::coin_registry; + +/// The One-Time Witness for the GOLD currency. +public struct GOLD has drop {} + +/// Called once, on package publish. Creates the `Currency` and +/// a `TreasuryCap` to manage the supply. +fun init(otw: GOLD, ctx: &mut TxContext) { + let (initializer, treasury_cap) = coin_registry::new_currency_with_otw( + otw, + 8, // decimals + "GOLD", // symbol + "Gold", // name + "In-game gold currency", // description + "https://example.com/gold.svg", // icon URL + ctx, + ); + + // Finalize the initializer, claiming the `MetadataCap`. + let metadata_cap = initializer.finalize(ctx); + + // Transfer both capabilities to the publisher. + transfer::public_transfer(treasury_cap, ctx.sender()); + transfer::public_transfer(metadata_cap, ctx.sender()); +} +// ANCHOR_END: gold + +#[test] +fun test_init() { + let ctx = &mut tx_context::dummy(); + init(sui::test_utils::create_one_time_witness(), ctx); +} diff --git a/packages/samples/sources/programmability/balance-and-coin-3.move b/packages/samples/sources/programmability/balance-and-coin-3.move new file mode 100644 index 000000000..2d0351995 --- /dev/null +++ b/packages/samples/sources/programmability/balance-and-coin-3.move @@ -0,0 +1,87 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: doubloon +/// A module that creates the Doubloon currency dynamically - at any +/// point after the package is published. +module book::doubloon; + +use sui::coin::Coin; +use sui::coin_registry::{Self, CoinRegistry, MetadataCap}; + +/// The type of the currency. For dynamic creation, the type must have +/// `key` and only `key`. +public struct Doubloon has key { id: UID } + +/// Creates the "Doubloon" currency. Unlike `init`, this function can be +/// called at any time, but only from the module that defines `Doubloon`. +public fun create_currency( + registry: &mut CoinRegistry, + ctx: &mut TxContext, +): (Coin, MetadataCap) { + let (mut initializer, mut treasury_cap) = coin_registry::new_currency( + registry, + 6, // decimals + "DBL", // symbol + "Doubloon", // name + "Pirate-themed currency", // description + "https://example.com/doubloon.svg", // icon URL + ctx, + ); + + // Mint the entire supply upfront, then give up the `TreasuryCap`, + // fixing the supply forever - no more minting or burning. + let coins = treasury_cap.mint(1_000_000_000, ctx); + initializer.make_supply_fixed(treasury_cap); + + // Finalize the initializer; `Currency` becomes a shared object. + let metadata_cap = initializer.finalize(ctx); + + (coins, metadata_cap) +} +// ANCHOR_END: doubloon + +#[test_only] +use std::unit_test::assert_eq; +#[test_only] +use sui::coin; +#[test_only] +use sui::test_utils; + +#[test] +fun test_create_currency() { + // `dummy()` context has the `@0x0` sender, required by the test-only + // registry constructor. + let ctx = &mut tx_context::dummy(); + let mut registry = coin_registry::create_coin_data_registry_for_testing(ctx); + + let (coins, metadata_cap) = create_currency(&mut registry, ctx); + assert_eq!(coins.value(), 1_000_000_000); + + test_utils::destroy(coins); + test_utils::destroy(metadata_cap); + coin_registry::share_for_testing(registry); +} + +#[test] +fun test_mint_burn() { + let ctx = &mut tx_context::dummy(); + + // ANCHOR: mint_burn + // Test-only constructor - normally the `TreasuryCap` comes from + // currency creation. + let mut treasury_cap = coin::create_treasury_cap_for_testing(ctx); + + // Mint 100 units, increasing the total supply. + let coin = treasury_cap.mint(100, ctx); + assert_eq!(coin.value(), 100); + assert_eq!(treasury_cap.total_supply(), 100); + + // Burn the coin, decreasing the total supply. + let burned = treasury_cap.burn(coin); + assert_eq!(burned, 100); + assert_eq!(treasury_cap.total_supply(), 0); + // ANCHOR_END: mint_burn + + test_utils::destroy(treasury_cap); +} diff --git a/packages/samples/sources/programmability/balance-and-coin-4.move b/packages/samples/sources/programmability/balance-and-coin-4.move new file mode 100644 index 000000000..91cc74929 --- /dev/null +++ b/packages/samples/sources/programmability/balance-and-coin-4.move @@ -0,0 +1,21 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: currency_reader +module book::currency_reader; + +use sui::coin::Coin; +use sui::coin_registry::Currency; + +/// The coin value has a fractional part. +const ENotWholeUnit: u64 = 0; + +/// The number of whole units in the coin, calculated with the +/// on-chain `decimals` value. Aborts if the value has a fractional +/// part - a "half a coin" deposit is not allowed. +public fun whole_units(currency: &Currency, coin: &Coin): u64 { + let one_unit = 10u64.pow(currency.decimals()); + assert!(coin.value() % one_unit == 0, ENotWholeUnit); + coin.value() / one_unit +} +// ANCHOR_END: currency_reader diff --git a/packages/samples/sources/programmability/balance-and-coin.move b/packages/samples/sources/programmability/balance-and-coin.move new file mode 100644 index 000000000..b9d99e9d4 --- /dev/null +++ b/packages/samples/sources/programmability/balance-and-coin.move @@ -0,0 +1,67 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module book::balance_and_coin; + +#[test_only] +use sui::balance; +#[test_only] +use sui::coin; +#[test_only] +use sui::sui::SUI; +#[test_only] +use std::unit_test::assert_eq; + +#[test] +fun test_balance_operations() { + // ANCHOR: balance + // There is no public constructor for `Balance` - in this test we use + // a test-only helper. Real balances come from minting or from a `Coin`. + let mut balance = balance::create_for_testing(1000); + assert_eq!(balance.value(), 1000); + + // Split part of the balance into a new `Balance`. + let part = balance.split(300); + assert_eq!(balance.value(), 700); + assert_eq!(part.value(), 300); + + // Join it back; `join` returns the new total. + let total = balance.join(part); + assert_eq!(total, 1000); + + // A zero `Balance` can be created and destroyed freely, + // as it does not represent any value. + let zero = balance::zero(); + assert_eq!(zero.value(), 0); + zero.destroy_zero(); + // ANCHOR_END: balance + + balance.destroy_for_testing(); +} + +#[test] +fun test_coin_operations() { + let ctx = &mut tx_context::dummy(); + + // ANCHOR: coin + // Like `Balance`, `Coin` has no public constructor - here we use a + // test-only helper to mint one out of thin air. + let mut coin = coin::mint_for_testing(1000, ctx); + assert_eq!(coin.value(), 1000); + + // `Coin` is an object, so splitting requires `ctx` to create a new UID. + let part = coin.split(300, ctx); + assert_eq!(coin.value(), 700); + assert_eq!(part.value(), 300); + + // A `Coin` can be turned into a `Balance` and back. + let balance = part.into_balance(); + let part = balance.into_coin(ctx); + + // Join the split part back into the original coin. + coin.join(part); + assert_eq!(coin.value(), 1000); + // ANCHOR_END: coin + + coin.burn_for_testing(); +} diff --git a/packages/samples/sources/programmability/bcs.move b/packages/samples/sources/programmability/bcs.move index ba4a57b15..6aeb18398 100644 --- a/packages/samples/sources/programmability/bcs.move +++ b/packages/samples/sources/programmability/bcs.move @@ -2,137 +2,232 @@ // SPDX-License-Identifier: Apache-2.0 #[allow(unused_variable, unused_field)] -module book::bcs { - use std::string::String; +module book::bcs; -public struct CustomData has drop { - num: u8, - string: String, - value: bool -} +use std::string::String; +// ANCHOR: user_def +/// A struct we will encode and decode in the examples below. public struct User has drop { age: u8, is_active: bool, - name: String + name: String, +} +// ANCHOR_END: user_def + +// ANCHOR: enum_def +/// A status of a delivery order. +public enum Status has drop { + Pending, + Shipped { tracking: u64 }, } +// ANCHOR_END: enum_def #[test_only] use std::unit_test::assert_eq; -#[test] fun test_encode() { -// ANCHOR: encode -use sui::bcs; - -// 0x01 - a single byte with value 1 (or 0 for false) -let bool_bytes = bcs::to_bytes(&true); -assert_eq!(bool_bytes, x"01"); - -// 0x2a - just a single byte -let u8_bytes = bcs::to_bytes(&42u8); -assert_eq!(u8_bytes, x"2A"); - -// 0x2a00000000000000 - 8 bytes -let u64_bytes = bcs::to_bytes(&42u64); -assert_eq!(u64_bytes, x"2A00000000000000"); - -// address is a fixed sequence of 32 bytes -// 0x0000000000000000000000000000000000000000000000000000000000000002 -let addr = bcs::to_bytes(&@sui); -assert_eq!(addr, x"0000000000000000000000000000000000000000000000000000000000000002"); -// ANCHOR_END: encode - -// ANCHOR: encode_struct -let data = CustomData { - num: 42, - string: b"hello, world!".to_string(), - value: true -}; - -let struct_bytes = bcs::to_bytes(&data); - -let mut custom_bytes = vector[]; -custom_bytes.append(bcs::to_bytes(&42u8)); -custom_bytes.append(bcs::to_bytes(&b"hello, world!".to_string())); -custom_bytes.append(bcs::to_bytes(&true)); - -// struct is just a sequence of fields, so the bytes should be the same! -assert_eq!(struct_bytes, custom_bytes); -// ANCHOR_END: encode_struct +#[test] +fun test_encode() { + // ANCHOR: encode + use sui::bcs; + + // 0x01 - a single byte with value 1 (or 0 for false) + let bool_bytes = bcs::to_bytes(&true); + assert_eq!(bool_bytes, x"01"); + + // 0x2a - just a single byte + let u8_bytes = bcs::to_bytes(&42u8); + assert_eq!(u8_bytes, x"2A"); + + // 0x2a00000000000000 - 8 bytes, little-endian + let u64_bytes = bcs::to_bytes(&42u64); + assert_eq!(u64_bytes, x"2A00000000000000"); + + // address is a fixed sequence of 32 bytes + // 0x0000000000000000000000000000000000000000000000000000000000000002 + let addr = bcs::to_bytes(&@sui); + assert_eq!(addr, x"0000000000000000000000000000000000000000000000000000000000000002"); + // ANCHOR_END: encode + + // ANCHOR: encode_struct + let user = User { + age: 42, + is_active: true, + name: "Bob", + }; + + // A struct is encoded as its fields, one after another, in the + // order they are declared: no names, no types, no separators. + // + // age | is_active | name + // 2A | 01 | 03 42 6F 62 (length + "Bob") + let user_bytes = bcs::to_bytes(&user); + assert_eq!(user_bytes, x"2A0103426F62"); + + // Concatenating individually encoded fields gives the same bytes! + let name: String = "Bob"; + let mut field_bytes = vector[]; + field_bytes.append(bcs::to_bytes(&42u8)); + field_bytes.append(bcs::to_bytes(&true)); + field_bytes.append(bcs::to_bytes(&name)); + + assert_eq!(user_bytes, field_bytes); + // ANCHOR_END: encode_struct } -#[test] fun test_bcs() { -// ANCHOR: decode -use sui::bcs; +#[test] +fun test_decode() { + // ANCHOR: decode + use sui::bcs; + + // The decoder wraps the bytes; it must be declared as mutable, + // because every `peel_*` call consumes a part of the input. + let mut bcs = bcs::new(x"012A2823000000000000"); -// BCS instance should always be declared as mutable -let mut bcs = bcs::new(x"010000000000000000"); + let bool_value = bcs.peel_bool(); + assert_eq!(bool_value, true); -// Same bytes can be read differently, for example: Option -let value: Option = bcs.peel_option_u64(); + let u8_value = bcs.peel_u8(); + assert_eq!(u8_value, 42); -assert_eq!(value.is_some(), true); -assert_eq!(*value.borrow(), 0); + // Whatever was not decoded can be taken back out of the wrapper. + let remainder = bcs.into_remainder_bytes(); + assert_eq!(remainder.length(), 8); + // ANCHOR_END: decode -let remainder = bcs.into_remainder_bytes(); + // ANCHOR: chain_decode + let mut bcs = bcs::new(x"012A2823000000000000"); -assert_eq!(remainder.length(), 0); -// ANCHOR_END: decode + // mind the order!!! + // handy way to peel multiple values + let (bool_value, u8_value, u64_value) = ( + bcs.peel_bool(), + bcs.peel_u8(), + bcs.peel_u64(), + ); -// ANCHOR: chain_decode -let mut bcs = bcs::new(x"0101010F0000000000F00000000000"); + assert_eq!(u64_value, 9000); + // ANCHOR_END: chain_decode +} + +#[test] +fun test_not_self_describing() { + use sui::bcs; -// mind the order!!! -// handy way to peel multiple values -let (bool_value, u8_value, u64_value) = ( - bcs.peel_bool(), - bcs.peel_u8(), - bcs.peel_u64() -); -// ANCHOR_END: chain_decode + // ANCHOR: not_self_describing + // The exact same 6 bytes that encoded the `User` above... + let mut bcs = bcs::new(x"2A0103426F62"); -// ANCHOR: decode_vector -let mut bcs = bcs::new(x"0101010F0000000000F00000000000"); + // ...can be read as completely different types. The bytes carry + // no type information - the reader decides what they mean. + let num = bcs.peel_u16(); // 0x012A = 298 + let vec = bcs.peel_vec_u8(); // [0x42, 0x6F, 0x62] -// bcs.peel_vec_length() peels the length of the vector :) -let mut len = bcs.peel_vec_length(); -let mut vec = vector[]; + assert_eq!(num, 298); + assert_eq!(vec, vector[66, 111, 98]); + // ANCHOR_END: not_self_describing +} -// then iterate depending on the data type -while (len > 0) { - vec.push_back(bcs.peel_u64()); // or any other type - len = len - 1; -}; +#[test] +fun test_decode_vector() { + use sui::bcs; -assert_eq!(vec.length(), 1); + // ANCHOR: decode_vector + // vector[1u64, 2u64]: length prefix `02`, then the two elements + let mut bcs = bcs::new(x"0201000000000000000200000000000000"); -// The above `while` can be simplified and made more readable using a `macro`. -// bcs.peel_vec!(|bcs| bcs.peel_u64()) is equivalent to the above `while` loop. -// ANCHOR_END: decode_vector + // first, peel the length of the vector... + let mut len = bcs.peel_vec_length(); + let mut vec = vector[]; -// ANCHOR: decode_option -let mut bcs = bcs::new(x"00"); -let is_some = bcs.peel_bool(); + // ...then peel each element in a loop + while (len > 0) { + vec.push_back(bcs.peel_u64()); // or any other type + len = len - 1; + }; -assert_eq!(is_some, false); + assert_eq!(vec, vector[1, 2]); + // ANCHOR_END: decode_vector -let mut bcs = bcs::new(x"0101"); -let is_some = bcs.peel_bool(); -let value = bcs.peel_u8(); + // ANCHOR: decode_vector_macro + let mut bcs = bcs::new(x"0201000000000000000200000000000000"); -assert_eq!(is_some, true); -assert_eq!(value, 1); -// ANCHOR_END: decode_option + // The `peel_vec!` macro does the same in a single call. + let vec = bcs.peel_vec!(|bcs| bcs.peel_u64()); + assert_eq!(vec, vector[1, 2]); -// ANCHOR: decode_struct -let mut bcs = bcs::new(x"0101010F0000000000F00000000000"); + // For vectors of primitive types, there are ready-made functions. + let mut bcs = bcs::new(x"0201000000000000000200000000000000"); + let vec = bcs.peel_vec_u64(); + assert_eq!(vec, vector[1, 2]); + // ANCHOR_END: decode_vector_macro +} -// Note: order matters! -let user = User { - age: bcs.peel_u8(), - is_active: bcs.peel_bool(), - name: bcs.peel_vec_u8().to_string() -}; -// ANCHOR_END: decode_struct +#[test] +fun test_decode_option() { + use sui::bcs; + + // ANCHOR: decode_option + // `option::none()` is a single `00` byte... + let mut bcs = bcs::new(x"00"); + let none = bcs.peel_option!(|bcs| bcs.peel_u8()); + assert!(none.is_none()); + + // ...and `option::some(42u8)` is `01` followed by the value. + let mut bcs = bcs::new(x"012A"); + let some = bcs.peel_option!(|bcs| bcs.peel_u8()); + assert_eq!(some, option::some(42)); + + // For primitive types, there are ready-made `peel_option_*` functions. + let mut bcs = bcs::new(x"012A"); + let some = bcs.peel_option_u8(); + assert_eq!(some, option::some(42)); + // ANCHOR_END: decode_option } + +#[test] +fun test_round_trip() { + use sui::bcs; + + // ANCHOR: round_trip + let user = User { + age: 42, + is_active: true, + name: "Bob", + }; + + // Encode the value... + let mut bcs = bcs::new(bcs::to_bytes(&user)); + + // ...and decode it back, peeling the fields in exactly the order + // they are declared in the struct definition. + let decoded = User { + age: bcs.peel_u8(), + is_active: bcs.peel_bool(), + name: bcs.peel_vec_u8().to_string(), + }; + + assert_eq!(user, decoded); + // ANCHOR_END: round_trip +} + +#[test] +fun test_decode_enum() { + use sui::bcs; + + // ANCHOR: decode_enum + let status = Status::Shipped { tracking: 12345 }; + + // An enum value is encoded as the variant index, followed by the + // fields of that variant. + let mut bcs = bcs::new(bcs::to_bytes(&status)); + + let decoded = match (bcs.peel_enum_tag()) { + 0 => Status::Pending, + 1 => Status::Shipped { tracking: bcs.peel_u64() }, + _ => abort, + }; + + assert_eq!(status, decoded); + // ANCHOR_END: decode_enum } diff --git a/packages/samples/sources/programmability/capability-4.move b/packages/samples/sources/programmability/capability-4.move index 2ba47cd1e..472990479 100644 --- a/packages/samples/sources/programmability/capability-4.move +++ b/packages/samples/sources/programmability/capability-4.move @@ -7,7 +7,7 @@ public struct User has key, store { id: UID } // ANCHOR: with_capability /// Grants the owner the right to create new users in the system. -public struct AdminCap {} +public struct AdminCap has key { id: UID } /// Creates a new user in the system. Requires the `AdminCap` capability to be /// passed as the first argument. diff --git a/packages/samples/sources/programmability/capability.move b/packages/samples/sources/programmability/capability.move index f81ca273e..d3b336f01 100644 --- a/packages/samples/sources/programmability/capability.move +++ b/packages/samples/sources/programmability/capability.move @@ -5,7 +5,6 @@ module book::capability; use std::string::String; -use sui::event; /// The capability granting the application admin the right to create new /// accounts in the system. @@ -17,9 +16,6 @@ public struct Account has key, store { name: String } -/// A simple `Ping` event with no data. -public struct Ping has copy, drop { by: ID } - /// Creates a new account in the system. Requires the `AdminCap` capability /// to be passed as the first argument. public fun new(_: &AdminCap, name: String, ctx: &mut TxContext): Account { @@ -29,15 +25,8 @@ public fun new(_: &AdminCap, name: String, ctx: &mut TxContext): Account { } } -/// Account, and any other objects, can also be used as a Capability in the -/// application. For example, to emit an event. -public fun send_ping(acc: &Account) { - event::emit(Ping { - by: acc.id.to_inner() - }) -} - -/// Updates the account name. Can only be called by the `Account` owner. +/// The `Account` itself acts as a capability too: only its owner can pass +/// a mutable reference to it, and hence only the owner can update the name. public fun update(account: &mut Account, name: String) { account.name = name; } diff --git a/packages/samples/sources/programmability/collections-2.move b/packages/samples/sources/programmability/collections-2.move index b1240a0f4..afb0f5958 100644 --- a/packages/samples/sources/programmability/collections-2.move +++ b/packages/samples/sources/programmability/collections-2.move @@ -17,10 +17,10 @@ use std::unit_test::assert_eq; #[test] fun vec_set_playground() { - let set = vec_set::empty(); // create an empty set - let mut set = vec_set::singleton(1u8); // create a set with a single item + let mut set = vec_set::empty(); // create an empty set - set.insert(2); // add an item to the set + set.insert(1u8); // add items to the set + set.insert(2); set.insert(3); assert_eq!(set.contains(&1), true); // check if an item is in the set @@ -28,5 +28,10 @@ fun vec_set_playground() { assert_eq!(set.is_empty(), false); // check if the set is empty set.remove(&2); // remove an item from the set + assert_eq!(set.contains(&2), false); + + // the contents can be taken out as a plain vector, e.g. for iteration + let items = set.into_keys(); + assert_eq!(items, vector[1, 3]); } // ANCHOR_END: vec_set diff --git a/packages/samples/sources/programmability/collections-3.move b/packages/samples/sources/programmability/collections-3.move index 778d576ae..d298550aa 100644 --- a/packages/samples/sources/programmability/collections-3.move +++ b/packages/samples/sources/programmability/collections-3.move @@ -15,17 +15,31 @@ public struct Metadata has drop { } #[test_only] -use std::unit_test::assert_eq; +use std::unit_test::{assert_eq, assert_ref_eq}; #[test] fun vec_map_playground() { - let mut map = vec_map::empty(); // create an empty map + let mut map: VecMap = vec_map::empty(); // create an empty map - map.insert(2, b"two".to_string()); // add a key-value pair to the map - map.insert(3, b"three".to_string()); + map.insert(2, "two"); // add a key-value pair to the map + map.insert(3, "three"); assert_eq!(map.contains(&2), true); // check if a key is in the map + assert_eq!(map.length(), 2); // get the number of entries - map.remove(&2); // remove a key-value pair from the map + // index syntax borrows a value by key, aborts if the key is missing + assert_ref_eq!(&map[&2], &"two"); + + // `try_get` copies the value, returns `none` if the key is missing + assert_eq!(map.try_get(&2), option::some("two")); + assert_eq!(map.try_get(&4), option::none()); + + // an existing value can be replaced through a mutable reference + *(&mut map[&3]) = "III"; + + // `remove` returns the key-value pair + let (key, value) = map.remove(&2); + assert_eq!(key, 2); + assert_eq!(value, "two"); } // ANCHOR_END: vec_map diff --git a/packages/samples/sources/programmability/display.move b/packages/samples/sources/programmability/display.move index 07f3f9a02..945edadc1 100644 --- a/packages/samples/sources/programmability/display.move +++ b/packages/samples/sources/programmability/display.move @@ -1,17 +1,12 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 - #[allow(unused_field)] // ANCHOR: hero module book::arena; use std::string::String; -use sui::package; -use sui::display; - -/// The One Time Witness to claim the `Publisher` object. -public struct ARENA has drop {} +use sui::display_registry::{Self, DisplayRegistry, DisplayCap}; /// Some object which will be displayed. public struct Hero has key { @@ -20,40 +15,43 @@ public struct Hero has key { level: u64, } -/// In the module initializer we create the `Publisher` object, and then -/// the Display for the `Hero` type. -fun init(otw: ARENA, ctx: &mut TxContext) { - let publisher = package::claim(otw, ctx); - let mut display = display::new(&publisher, ctx); - - display.add( - b"name".to_string(), - b"{class} (lvl. {level})".to_string() - ); - - display.add( - b"description".to_string(), - b"One of the greatest heroes of all time. Join us!".to_string() +/// Creates the `Display`. Call it exactly once, right after publishing: +/// the registry holds a single `Display` per type, so a second call aborts. +/// It is an `entry` function rather than a `public` one, so that a later +/// package upgrade can remove it - upgrade rules freeze `public` functions, +/// but not `entry` ones. +entry fun create_display(registry: &mut DisplayRegistry, ctx: &mut TxContext) { + let (mut display, cap) = display_registry::new( + registry, + internal::permit(), + ctx, ); - display.add( - b"link".to_string(), - b"https://example.com/hero/{id}".to_string() - ); + display.set(&cap, "name", "{class} (lvl. {level})"); + display.set(&cap, "description", "One of the greatest heroes of all time. Join us!"); + display.set(&cap, "link", "https://example.com/hero/{id}"); + display.set(&cap, "image_url", "https://example.com/hero/{class}.jpg"); - display.add( - b"image_url".to_string(), - b"https://example.com/hero/{class}.jpg".to_string() - ); + // Share the `Display` so clients can find it, and send the capability to + // the publisher, who keeps it to update the fields later. + display.share(); + transfer::public_transfer(cap, ctx.sender()); +} +// ANCHOR_END: hero - // Update the display with the new data. - // Must be called to apply changes. - display.update_version(); +// ANCHOR: migrate +use sui::display::Display as LegacyDisplay; - transfer::public_transfer(publisher, ctx.sender()); - transfer::public_transfer(display, ctx.sender()); +/// Claim the `DisplayCap` for the system-migrated `Display`, giving up +/// the legacy V1 `Display` object, which is destroyed in the process. +public fun claim_display_cap( + display: &mut display_registry::Display, + legacy: LegacyDisplay, + ctx: &mut TxContext, +): DisplayCap { + display.claim(legacy, ctx) } -// ANCHOR_END: hero +// ANCHOR_END: migrate // ANCHOR: background /// An attempt to standardize the object structure for display. diff --git a/packages/samples/sources/programmability/dynamic-collections.move b/packages/samples/sources/programmability/dynamic-collections.move index 6a7dd7578..7b1d92e2d 100644 --- a/packages/samples/sources/programmability/dynamic-collections.move +++ b/packages/samples/sources/programmability/dynamic-collections.move @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 #[allow(unused_use, unused_field, unused_variable)] -module book::dynamic_collections { +module book::dynamic_collections; + use std::string::String; // ANCHOR: bag_struct @@ -28,7 +29,9 @@ let mut bag = bag::new(ctx); // bag has the `length` function to get the number of elements assert_eq!(bag.length(), 0); -bag.add(b"my_key", b"my_value".to_string()); +// the type of the value is defined at insertion; here it is a `String` +let value: String = "my_value"; +bag.add(b"my_key", value); // length has changed to 1 assert_eq!(bag.length(), 1); @@ -69,8 +72,8 @@ let mut table = table::new(ctx); // table has the `length` function to get the number of elements assert_eq!(table.length(), 0); -table.add(@0xa11ce, b"my_value".to_string()); -table.add(@0xb0b, b"another_value".to_string()); +table.add(@0xa11ce, "my_value"); +table.add(@0xb0b, "another_value"); // length has changed to 2 assert_eq!(table.length(), 2); @@ -88,6 +91,45 @@ table.destroy_empty(); // ANCHOR_END: table_usage } +// ANCHOR: object_table_struct +/// Imported from the `sui::object_table` module. +use sui::object_table::{Self, ObjectTable}; + +/// A profile is an object - it has the `key` and `store` abilities. +public struct Profile has key, store { + id: UID, + name: String, +} + +/// An example of an `ObjectTable` as a struct field. +public struct ProfileRegistry has key { + id: UID, + profiles: ObjectTable +} +// ANCHOR_END: object_table_struct + +#[test] fun test_object_table() { +let ctx = &mut tx_context::dummy(); + +// ANCHOR: object_table_usage +let mut profiles = object_table::new(ctx); + +// the interface is the same as the regular `Table` +profiles.add(@0xa11ce, Profile { + id: object::new(ctx), + name: "Alice", +}); + +// the stored object keeps its `ID` and can be looked up without its type +let profile_id = profiles.value_id(@0xa11ce); // Option + +// objects cannot be dropped - remove the entry before destroying the table +let profile = profiles.remove(@0xa11ce); +profiles.destroy_empty(); +// ANCHOR_END: object_table_usage +std::unit_test::destroy(profile); +} + // ANCHOR: linked_table_struct /// Imported from the `sui::linked_table` module. use sui::linked_table::{Self, LinkedTable}; @@ -113,9 +155,9 @@ let mut linked_table = linked_table::new(ctx); // linked_table has the `length` function to get the number of elements assert_eq!(linked_table.length(), 0); -linked_table.push_front(@0xa0a, b"first_value".to_string()); -linked_table.push_back(@0xb1b, b"second_value".to_string()); -linked_table.push_back(@0xc2c, b"third_value".to_string()); +linked_table.push_front(@0xa0a, "first_value"); +linked_table.push_back(@0xb1b, "second_value"); +linked_table.push_back(@0xc2c, "third_value"); // length has changed to 3 assert_eq!(linked_table.length(), 3); @@ -133,4 +175,3 @@ let (_third_addr, _third_value) = linked_table.pop_back(); linked_table.destroy_empty(); // ANCHOR_END: linked_table_usage } -} diff --git a/packages/samples/sources/programmability/dynamic-fields.move b/packages/samples/sources/programmability/dynamic-fields.move index 2eff2cb75..7c9d9aa68 100644 --- a/packages/samples/sources/programmability/dynamic-fields.move +++ b/packages/samples/sources/programmability/dynamic-fields.move @@ -68,7 +68,8 @@ let ctx = &mut tx_context::dummy(); let mut character = Character { id: object::new(ctx) }; // Attach a `String` via a `vector` name -df::add(&mut character.id, b"string_key", b"Hello, World!".to_string()); +let string_value: String = "Hello, World!"; +df::add(&mut character.id, b"string_key", string_value); // Attach a `u64` via a `u32` name df::add(&mut character.id, 1000u32, 1_000_000_000u64); @@ -133,21 +134,21 @@ let ctx = &mut tx_context::dummy(); // ANCHOR: custom_type_usage let mut character = Character { id: object::new(ctx) }; -// Attaching via an `AccessoryKey { name: b"hat" }` +// Attaching via an `AccessoryKey { name: "hat" }` df::add( &mut character.id, - AccessoryKey { name: b"hat".to_string() }, + AccessoryKey { name: "hat" }, Hat { id: object::new(ctx), color: 0xFF0000 } ); -// Attaching via an `AccessoryKey { name: b"mustache" }` +// Attaching via an `AccessoryKey { name: "mustache" }` df::add( &mut character.id, - AccessoryKey { name: b"mustache".to_string() }, + AccessoryKey { name: "mustache" }, Mustache { id: object::new(ctx) } ); // Attaching via a `MetadataKey` -df::add(&mut character.id, MetadataKey {}, 42); +df::add(&mut character.id, MetadataKey {}, 42u64); // ANCHOR_END: custom_type_usage std::unit_test::destroy(character); } diff --git a/packages/samples/sources/programmability/dynamic-object-fields.move b/packages/samples/sources/programmability/dynamic-object-fields.move index c64d05413..415262e49 100644 --- a/packages/samples/sources/programmability/dynamic-object-fields.move +++ b/packages/samples/sources/programmability/dynamic-object-fields.move @@ -34,7 +34,7 @@ fun equip_accessory() { // However for non-key structs we can only use `dynamic_field` df::add(&mut character.id, b"metadata_key", Metadata { - name: b"John".to_string() + name: "John" }); // Borrow the hat from the character diff --git a/packages/samples/sources/programmability/epoch-and-time.move b/packages/samples/sources/programmability/epoch-and-time.move index 1c743be6b..04eaa1c9b 100644 --- a/packages/samples/sources/programmability/epoch-and-time.move +++ b/packages/samples/sources/programmability/epoch-and-time.move @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #[allow(unused_variable)] -module book::epoch_and_time { +module book::epoch_and_time; // ANCHOR: epoch public fun current_epoch(ctx: &TxContext) { @@ -60,5 +60,3 @@ fun use_clock_in_test() { clock.destroy_for_testing(); } // ANCHOR_END: test - -} diff --git a/packages/samples/sources/programmability/events.move b/packages/samples/sources/programmability/events.move index e50b7a074..3f52bb313 100644 --- a/packages/samples/sources/programmability/events.move +++ b/packages/samples/sources/programmability/events.move @@ -19,7 +19,7 @@ public struct ItemPurchased has copy, drop { } /// A marketplace function which performs the purchase of an item. -public fun purchase(coin: Coin, ctx: &mut TxContext) { +public fun purchase(seller: address, coin: Coin, ctx: &mut TxContext): Item { let item = Item { id: object::new(ctx) }; // Create an instance of `ItemPurchased` and pass it to `event::emit`. @@ -28,7 +28,33 @@ public fun purchase(coin: Coin, ctx: &mut TxContext) { price: coin.value() }); - // Omitting the rest of the implementation to keep the example simple. - abort + // Send the payment to the seller, return the item to the caller. + transfer::public_transfer(coin, seller); + item } // ANCHOR_END: emit + +#[test_only] +use std::unit_test::assert_eq; + +// ANCHOR: test +#[test] +fun test_emit_item_purchased() { + let ctx = &mut tx_context::dummy(); + let item = Item { id: object::new(ctx) }; + let item_id = object::id(&item); + + event::emit(ItemPurchased { item: item_id, price: 100 }); + + // Total number of events emitted in this test so far. + assert_eq!(event::num_events(), 1); + + // Read back all `ItemPurchased` events and check their contents. + let purchases = event::events_by_type(); + assert_eq!(purchases.length(), 1); + assert_eq!(purchases[0].item, item_id); + assert_eq!(purchases[0].price, 100); + + std::unit_test::destroy(item); +} +// ANCHOR_END: test diff --git a/packages/samples/sources/programmability/module-initializer.move b/packages/samples/sources/programmability/module-initializer.move index fb433868c..11ccefe45 100644 --- a/packages/samples/sources/programmability/module-initializer.move +++ b/packages/samples/sources/programmability/module-initializer.move @@ -28,3 +28,17 @@ fun init(ctx: &mut TxContext) { }); } // ANCHOR_END: main + +// ANCHOR: test +#[test_only] +use std::unit_test::assert_eq; + +#[test] +fun test_init() { + let ctx = &mut tx_context::dummy(); + init(ctx); + + // Two objects were created: the `ShopOwnerCap` and the `Shop`. + assert_eq!(ctx.ids_created(), 2); +} +// ANCHOR_END: test diff --git a/packages/samples/sources/programmability/package-upgrades-2.move b/packages/samples/sources/programmability/package-upgrades-2.move new file mode 100644 index 000000000..e7d7db7ed --- /dev/null +++ b/packages/samples/sources/programmability/package-upgrades-2.move @@ -0,0 +1,59 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: lazy +module book::versioned_config; + +use sui::dynamic_field as df; + +/// The current version of the package. +const VERSION: u8 = 2; + +/// The base object stays thin - its layout can never change in an +/// upgrade. The actual configuration is attached as a dynamic field. +public struct Config has key { + id: UID, + version: u8, +} + +/// Configuration attached as a dynamic field in version 1. +public struct ConfigV1 has store { fee: u64 } + +/// Version 2 of the configuration adds a new field. +public struct ConfigV2 has store { fee: u64, discount: u64 } + +/// Read the fee, migrating the configuration on first access. +public fun fee(config: &mut Config): u64 { + config.migrate_if_needed(); + df::borrow(&config.id, 0).fee +} + +/// Replace `ConfigV1` with `ConfigV2` the first time the object is +/// used after the upgrade. +fun migrate_if_needed(config: &mut Config) { + if (config.version == 1) { + let ConfigV1 { fee } = df::remove(&mut config.id, 0u8); + df::add(&mut config.id, 0u8, ConfigV2 { fee, discount: 0 }); + config.version = VERSION; + } +} +// ANCHOR_END: lazy + +#[test] +fun test_lazy_migration() { + use std::unit_test::assert_eq; + + let ctx = &mut tx_context::dummy(); + + // The object was created by the previous version (1) of the package + // and carries the old configuration. + let mut config = Config { id: object::new(ctx), version: 1 }; + df::add(&mut config.id, 0u8, ConfigV1 { fee: 1000 }); + + // The first access migrates the configuration to `ConfigV2`. + assert_eq!(config.fee(), 1000); + assert_eq!(config.version, VERSION); + assert_eq!(df::borrow(&config.id, 0).discount, 0); + + std::unit_test::destroy(config); +} diff --git a/packages/samples/sources/programmability/package-upgrades.move b/packages/samples/sources/programmability/package-upgrades.move index e632259fb..5d8d47b5b 100644 --- a/packages/samples/sources/programmability/package-upgrades.move +++ b/packages/samples/sources/programmability/package-upgrades.move @@ -1,3 +1,76 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +// ANCHOR: versioned +module book::versioned_state; + +/// The version of the package this module belongs to. Incremented on +/// every upgrade that has to invalidate previous versions. +const VERSION: u8 = 2; + +/// Trying to use an object with an older or newer package version. +const EVersionMismatch: u64 = 0; + +/// Shared state of the application; the `version` field gates access, +/// tying the object to a single version of the package. +public struct Counter has key { + id: UID, + version: u8, + value: u64, +} + +/// Every function that uses the shared object starts with a version +/// check: only the package version stored in the object may proceed. +public fun increment(counter: &mut Counter) { + assert!(counter.version == VERSION, EVersionMismatch); + counter.value = counter.value + 1; +} +// ANCHOR_END: versioned + +// ANCHOR: migrate +/// Grants the holder the permission to migrate the shared state. +public struct AdminCap has key, store { id: UID } + +/// The object is already migrated to the current version. +const ENotUpgrade: u64 = 1; + +/// Bump the version of the shared object, so that only the current +/// package version can use it. Called by the admin after an upgrade. +public fun migrate(counter: &mut Counter, _: &AdminCap) { + assert!(counter.version < VERSION, ENotUpgrade); + counter.version = VERSION; +} +// ANCHOR_END: migrate + +#[test] +fun test_migrate() { + use std::unit_test::assert_eq; + + let ctx = &mut tx_context::dummy(); + let admin = AdminCap { id: object::new(ctx) }; + + // The object was created and shared by the previous version (1). + let mut counter = Counter { id: object::new(ctx), version: 1, value: 0 }; + + // After the upgrade, the admin migrates the object; from now on + // only the current version of the package can use it. + counter.migrate(&admin); + counter.increment(); + + assert_eq!(counter.version, VERSION); + assert_eq!(counter.value, 1); + + std::unit_test::destroy(counter); + std::unit_test::destroy(admin); +} + +#[test, expected_failure(abort_code = EVersionMismatch)] +fun test_version_mismatch_fail() { + let ctx = &mut tx_context::dummy(); + let mut counter = Counter { id: object::new(ctx), version: 1, value: 0 }; + + // The object was not migrated - the call aborts. + counter.increment(); + + abort +} diff --git a/packages/samples/sources/programmability/randomness.move b/packages/samples/sources/programmability/randomness.move index e632259fb..76f4514a6 100644 --- a/packages/samples/sources/programmability/randomness.move +++ b/packages/samples/sources/programmability/randomness.move @@ -1,3 +1,131 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +#[allow(unused_variable)] +module book::randomness; + +use sui::random::{Random, RandomGenerator}; + +// ANCHOR: main +const GOLD: u8 = 0; +const SILVER: u8 = 1; +const BRONZE: u8 = 2; + +/// A medal of a random quality, awarded to the caller. +public struct Medal has key { + id: UID, + quality: u8, +} + +/// The entry function - a thin "facade" which takes the `Random` object, +/// creates a generator, and forwards it to the implementation. +entry fun mint_medal(random: &Random, ctx: &mut TxContext) { + let mut generator = random.new_generator(ctx); + let medal = mint_medal_impl(&mut generator, ctx); + transfer::transfer(medal, ctx.sender()); +} + +/// The actual implementation: 10% for Gold, 30% for Silver, 60% for Bronze. +/// Thanks to `public(package)` visibility and the `RandomGenerator` +/// parameter, this function can be called directly in tests. +public(package) fun mint_medal_impl(generator: &mut RandomGenerator, ctx: &mut TxContext): Medal { + let value = generator.generate_u8_in_range(1, 100); + let quality = if (value <= 10) GOLD else if (value <= 40) SILVER else BRONZE; + + Medal { id: object::new(ctx), quality } +} +// ANCHOR_END: main + +#[test_only] +use std::unit_test::{assert_eq, destroy}; +#[test_only] +use sui::random; +#[test_only] +use sui::test_scenario; + +// ANCHOR: generator +#[test] +fun test_generator_methods() { + let mut generator = random::new_generator_for_testing(); + + // Booleans, integers of any size, and integers in a range. + let coin_flip: bool = generator.generate_bool(); + let any_u64: u64 = generator.generate_u64(); + let dice: u8 = generator.generate_u8_in_range(1, 6); + + // Random bytes and shuffling of vectors. + let bytes = generator.generate_bytes(32); + let mut cards = vector[1u8, 2, 3, 4, 5]; + generator.shuffle(&mut cards); +} +// ANCHOR_END: generator + +// ANCHOR: test_unit +#[test] +fun test_mint_medal() { + let ctx = &mut tx_context::dummy(); + + // Generators created from the same seed return the same sequence of + // values, making the test fully reproducible... + let mut gen1 = random::new_generator_from_seed_for_testing("victory"); + let mut gen2 = random::new_generator_from_seed_for_testing("victory"); + assert_eq!(gen1.generate_u64(), gen2.generate_u64()); + + // ...and different seeds produce different values. Search for seeds + // which lead the test into the branch you want to check: `"victory"` + // rolls a 6 (Gold), and `"trophy"` rolls a 90 (Bronze). + let mut gold_gen = random::new_generator_from_seed_for_testing("victory"); + let gold_medal = mint_medal_impl(&mut gold_gen, ctx); + assert_eq!(gold_medal.quality, GOLD); + destroy(gold_medal); + + let mut bronze_gen = random::new_generator_from_seed_for_testing("trophy"); + let bronze_medal = mint_medal_impl(&mut bronze_gen, ctx); + assert_eq!(bronze_medal.quality, BRONZE); + destroy(bronze_medal); + + // A non-seeded generator is useful for property-style tests: whatever + // the outcome, the quality must be one of the three defined values. + let mut generator = random::new_generator_for_testing(); + 100u8.do!(|_| { + let medal = mint_medal_impl(&mut generator, ctx); + assert!(medal.quality <= BRONZE); + destroy(medal); + }); +} +// ANCHOR_END: test_unit + +// ANCHOR: test_scenario +#[test] +fun test_mint_medal_via_entry() { + let user = @0xA11CE; + + // The `Random` object can only be created and updated by the system, + // so the scenario has to start as the system address `0x0`. + let mut scenario = test_scenario::begin(@0x0); + random::create_for_testing(scenario.ctx()); + scenario.next_tx(@0x0); + + let mut random: Random = scenario.take_shared(); + random.update_randomness_state_for_testing( + 0, + x"1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F", + scenario.ctx(), + ); + test_scenario::return_shared(random); + + // Now the user calls the entry function, as a transaction would. + scenario.next_tx(user); + let random: Random = scenario.take_shared(); + mint_medal(&random, scenario.ctx()); + test_scenario::return_shared(random); + + // The `Medal` is now owned by the caller. + scenario.next_tx(user); + let medal: Medal = scenario.take_from_sender(); + assert!(medal.quality <= BRONZE); + scenario.return_to_sender(medal); + + scenario.end(); +} +// ANCHOR_END: test_scenario diff --git a/packages/samples/sources/programmability/transaction-context.move b/packages/samples/sources/programmability/transaction-context.move index 373f554ab..2897bf244 100644 --- a/packages/samples/sources/programmability/transaction-context.move +++ b/packages/samples/sources/programmability/transaction-context.move @@ -6,9 +6,13 @@ module book::transaction_context; // ANCHOR: reading public fun some_action(ctx: &TxContext) { - let me = ctx.sender(); + let sender = ctx.sender(); + let tx_digest = ctx.digest(); let epoch = ctx.epoch(); - let digest = ctx.digest(); + let epoch_start = ctx.epoch_timestamp_ms(); + let sponsor = ctx.sponsor(); + let gas_price = ctx.gas_price(); + let ref_gas_price = ctx.reference_gas_price(); // ... } // ANCHOR_END: reading diff --git a/packages/samples/sources/programmability/witness-pattern-2.move b/packages/samples/sources/programmability/witness-pattern-2.move new file mode 100644 index 000000000..7890c44c1 --- /dev/null +++ b/packages/samples/sources/programmability/witness-pattern-2.move @@ -0,0 +1,15 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: main +module book::witness; + +/// A struct that can only be created with a witness of `T`. +public struct Instance has drop {} + +/// Create a new `Instance` with the provided witness. The witness is +/// discarded after use. +public fun new(_witness: T): Instance { + Instance {} +} +// ANCHOR_END: main diff --git a/packages/samples/sources/programmability/witness-pattern-3.move b/packages/samples/sources/programmability/witness-pattern-3.move new file mode 100644 index 000000000..a6844f01a --- /dev/null +++ b/packages/samples/sources/programmability/witness-pattern-3.move @@ -0,0 +1,21 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: source +module book::witness_source; + +use book::witness::{Self, Instance}; + +/// A struct used as a witness - canonically, an empty struct with `drop`. +public struct W has drop {} + +/// Create a new instance of `Instance`. +public fun new_instance(): Instance { + witness::new(W {}) +} +// ANCHOR_END: source + +#[test] +fun test_new_instance() { + let _instance = new_instance(); +} diff --git a/packages/samples/sources/programmability/witness-pattern.move b/packages/samples/sources/programmability/witness-pattern.move index 45bbdb977..6c2fb3ea3 100644 --- a/packages/samples/sources/programmability/witness-pattern.move +++ b/packages/samples/sources/programmability/witness-pattern.move @@ -3,10 +3,8 @@ module book::witness_definition; -// ANCHOR: definition /// Canonical definition of a witness - a type with the `drop` ability. public struct MyWitness has drop {} -// ANCHOR_END: definition // ANCHOR: regulated_coin /// A custom RegulatedCoin type with implementable functions. @@ -41,3 +39,20 @@ public fun join(coin: &mut RegulatedCoin, other: RegulatedCoin) { id.delete(); } // ANCHOR_END: regulated_coin + +#[test_only] +use std::unit_test::assert_eq; + +#[test] +fun test_regulated_coin() { + let ctx = &mut tx_context::dummy(); + + // The privileged functions require the `MyWitness` witness. + let mut coin = mint(MyWitness {}, 100, ctx); + let other = mint(MyWitness {}, 50, ctx); + + // The public `join` function does not. + coin.join(other); + + assert_eq!(burn(MyWitness {}, coin), 150); +} diff --git a/packages/samples/sources/storage/internal-constraint.move b/packages/samples/sources/storage/internal-constraint.move new file mode 100644 index 000000000..1b5c93258 --- /dev/null +++ b/packages/samples/sources/storage/internal-constraint.move @@ -0,0 +1,22 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +// ANCHOR: main +/// Defines the type `A`. +module book::exercise_internal; + +use sui::event; + +/// Type defined in this module, so it's internal here. +public struct A has copy, drop {} + +/// Works, because `A` is defined in this module. +public fun call_internal() { + event::emit(A {}) +} +// ANCHOR_END: main + +#[test] +fun test_call_internal() { + call_internal(); +} diff --git a/packages/samples/sources/storage/key-ability.move b/packages/samples/sources/storage/key-ability.move new file mode 100644 index 000000000..63b1d2151 --- /dev/null +++ b/packages/samples/sources/storage/key-ability.move @@ -0,0 +1,33 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module book::key_ability; + +use std::string::String; + +// ANCHOR: user +/// An object: a struct with the `key` ability and an `id: UID` field. +public struct User has key { + id: UID, // required by the Sui Verifier, always the first field + name: String, // all other fields must have `store` +} + +/// Creates a new `User` object. The fresh `UID` is derived from the +/// transaction context `ctx`. +public fun new(name: String, ctx: &mut TxContext): User { + User { + id: object::new(ctx), + name, + } +} +// ANCHOR_END: user + +#[test] +fun test_new() { + let mut ctx = tx_context::dummy(); + let user = new("Alice", &mut ctx); + + // The object cannot be discarded: unpack it and delete the UID. + let User { id, name: _ } = user; + id.delete(); +} diff --git a/packages/samples/sources/storage/storage-functions.move b/packages/samples/sources/storage/storage-functions.move new file mode 100644 index 000000000..f3a92969c --- /dev/null +++ b/packages/samples/sources/storage/storage-functions.move @@ -0,0 +1,133 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module book::storage_functions; + +use std::string::String; + +// ANCHOR: admin_cap +/// A struct with `key` is an object. The first field is `id: UID`! +public struct AdminCap has key { id: UID } + +/// `init` is a special function called once, when the module is +/// published. It is the best place to create singleton objects, +/// such as an admin capability. +fun init(ctx: &mut TxContext) { + // Create the `AdminCap` object in this scope. + let admin_cap = AdminCap { id: object::new(ctx) }; + + // Transfer the object to the transaction sender. + transfer::transfer(admin_cap, ctx.sender()); +} + +/// Transfers the `AdminCap` object to the `recipient`. Thus, the +/// recipient becomes the owner of the object, and only they can +/// access it. +public fun transfer_admin_cap(cap: AdminCap, recipient: address) { + transfer::transfer(cap, recipient); +} +// ANCHOR_END: admin_cap + +// ANCHOR: mint_and_transfer +/// Some `Gift` object that the admin can `mint_and_transfer`. +public struct Gift has key, store { id: UID } + +/// Creates a new `Gift` object and transfers it to the `recipient`. +public fun mint_and_transfer( + _: &AdminCap, + recipient: address, + ctx: &mut TxContext, +) { + let gift = Gift { id: object::new(ctx) }; + transfer::public_transfer(gift, recipient); +} +// ANCHOR_END: mint_and_transfer + +// ANCHOR: freeze_gift +/// Freezes the `Gift` object so it becomes immutable. +/// `Gift` has `key` + `store`, so `public_freeze_object` can be used! +public fun freeze_gift(gift: Gift) { + transfer::public_freeze_object(gift); +} +// ANCHOR_END: freeze_gift + +// ANCHOR: config +/// Some `Config` object that the admin can `create_and_freeze`. +public struct Config has key { + id: UID, + message: String, +} + +/// Creates a new `Config` object and freezes it. +public fun create_and_freeze( + _: &AdminCap, + message: String, + ctx: &mut TxContext, +) { + let config = Config { + id: object::new(ctx), + message, + }; + + // Freeze the object so it becomes immutable. + transfer::freeze_object(config); +} + +/// Returns the message from the `Config` object. +/// Can access the object by immutable reference! +public fun message(c: &Config): String { c.message } +// ANCHOR_END: config + +// ANCHOR: frozen_uncallable +/// The function can be defined, but it won't be callable on a frozen +/// object - only immutable references to it are available. +public fun message_mut(c: &mut Config): &mut String { &mut c.message } +// ANCHOR_END: frozen_uncallable + +// ANCHOR: share +/// Creates a new `Config` object and shares it. +public fun create_and_share(message: String, ctx: &mut TxContext) { + let config = Config { + id: object::new(ctx), + message, + }; + + // Share the object so it becomes shared. + transfer::share_object(config); +} +// ANCHOR_END: share + +// ANCHOR: delete_shared +/// Deletes the `Config` object, takes it by value. +/// Can be called on a shared object! +public fun delete_config(c: Config) { + let Config { id, message: _ } = c; + id.delete() +} +// ANCHOR_END: delete_shared + +#[test] +fun test_storage_flow() { + let mut ctx = tx_context::dummy(); + + // Mimic what `init` does, then exercise the admin functions. + let cap = AdminCap { id: object::new(&mut ctx) }; + + mint_and_transfer(&cap, @0xB0B, &mut ctx); + create_and_freeze(&cap, "frozen", &mut ctx); + create_and_share("shared", &mut ctx); + + transfer_admin_cap(cap, @0xA11CE); +} + +#[test] +fun test_delete_config() { + let mut ctx = tx_context::dummy(); + let config = Config { + id: object::new(&mut ctx), + message: "doomed", + }; + + assert!(message(&config) == "doomed"); + delete_config(config); +} diff --git a/packages/samples/sources/storage/store-ability.move b/packages/samples/sources/storage/store-ability.move index b0789974b..200db94f4 100644 --- a/packages/samples/sources/storage/store-ability.move +++ b/packages/samples/sources/storage/store-ability.move @@ -1,6 +1,35 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -module book::store_ability { - +module book::store_ability; + +use std::string::String; + +// ANCHOR: definition +/// Extra metadata with `store`; all of its fields must have `store` as well! +public struct Metadata has store { + bio: String, +} + +/// An object for a single user record. +public struct User has key { + id: UID, + name: String, // `String` has `store` + age: u8, // all integers have `store` + metadata: Metadata, // another type with the `store` ability +} +// ANCHOR_END: definition + +#[test] +fun test_pack_unpack() { + let mut ctx = tx_context::dummy(); + let user = User { + id: object::new(&mut ctx), + name: "Alice", + age: 100, + metadata: Metadata { bio: "Movegeist" }, + }; + + let User { id, metadata: Metadata { bio: _ }, .. } = user; + id.delete(); } diff --git a/packages/samples/sources/storage/uid-and-id.move b/packages/samples/sources/storage/uid-and-id.move new file mode 100644 index 000000000..c28aa57b0 --- /dev/null +++ b/packages/samples/sources/storage/uid-and-id.move @@ -0,0 +1,75 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +#[allow(unused_variable)] +module book::uid_and_id; + +// ANCHOR: lifecycle +public struct Character has key { id: UID } + +/// Creates a `Character` object and immediately destroys it: +/// the UID can only be deleted after the object is unpacked. +public fun create_and_destroy(ctx: &mut TxContext) { + // Instantiate the `Character` object with a fresh UID. + let char = Character { id: object::new(ctx) }; + + // Unpack the object to take out its UID. + let Character { id } = char; + + // Delete the UID. + id.delete(); +} +// ANCHOR_END: lifecycle + +// ANCHOR: derived +use sui::derived_object; + +/// Some central application object. +public struct Base has key { id: UID } + +/// A derived object. +public struct Derived has key { id: UID } + +/// Creates and shares a new `Derived` object, using an `address` +/// as the derivation key. +public fun derive(base: &mut Base, key: address) { + let id = derived_object::claim(&mut base.id, key); + transfer::share_object(Derived { id }) +} +// ANCHOR_END: derived + +// ANCHOR: conversions +public fun conversion_methods(ctx: &mut TxContext) { + let uid: UID = object::new(ctx); + + // `to_inner` returns a copy of the underlying `ID`. + let id: ID = uid.to_inner(); + + // Both `UID` and `ID` can be converted to a plain address. + let addr_from_uid: address = uid.to_address(); + let addr_from_id: address = id.to_address(); + + uid.delete(); +} +// ANCHOR_END: conversions + +#[test] +fun test_lifecycle_and_conversions() { + let mut ctx = tx_context::dummy(); + create_and_destroy(&mut ctx); + conversion_methods(&mut ctx); +} + +#[test] +fun test_derive() { + let mut ctx = tx_context::dummy(); + let mut base = Base { id: object::new(&mut ctx) }; + + derive(&mut base, @0x1); + + // The derived address is deterministic and can be recomputed. + let derived_addr = derived_object::derive_address(base.id.to_inner(), @0x1); + assert!(derived_object::exists(&base.id, @0x1)); + + transfer::share_object(base); +} diff --git a/packages/samples/sources/testing/builder_pattern_builder.move b/packages/samples/sources/testing/builder_pattern_builder.move index 5d1bfa7eb..783035d83 100644 --- a/packages/samples/sources/testing/builder_pattern_builder.move +++ b/packages/samples/sources/testing/builder_pattern_builder.move @@ -59,9 +59,9 @@ public fun is_active(mut self: UserBuilder, is_active: bool): UserBuilder { public fun build(self: UserBuilder): User { let UserBuilder { name, age, email, balance, is_active } = self; user::new( - name.destroy_or!(b"Default User".to_string()), + name.destroy_or!("Default User"), age.destroy_or!(18), - email.destroy_or!(b"user@example.com".to_string()), + email.destroy_or!("user@example.com"), balance.destroy_or!(0), is_active.destroy_or!(true), ) @@ -73,9 +73,9 @@ public fun build(self: UserBuilder): User { fun test_balance_check_without_builder() { // We only care about `balance`, but must specify everything let user = user::new( - b"Alice".to_string(), + "Alice", 25, - b"alice@example.com".to_string(), + "alice@example.com", 1000, // <-- the only field we care about true, ); @@ -86,9 +86,9 @@ fun test_balance_check_without_builder() { fun test_inactive_user_without_builder() { // We only care about `is_active`, but must specify everything let user = user::new( - b"Bob".to_string(), + "Bob", 30, - b"bob@example.com".to_string(), + "bob@example.com", 500, false, // <-- the only field we care about ); diff --git a/packages/samples/sources/testing/testing-basics.move b/packages/samples/sources/testing/testing-basics.move new file mode 100644 index 000000000..aa1600f1c --- /dev/null +++ b/packages/samples/sources/testing/testing-basics.move @@ -0,0 +1,19 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +module book::testing_basics; + +// ANCHOR: foreign_abort_code +/// The test aborts inside `sui::dynamic_field`, and the expected abort +/// code is imported from that module by its full path. +#[test, expected_failure(abort_code = sui::dynamic_field::EFieldDoesNotExist)] +fun test_borrow_missing_field() { + let ctx = &mut tx_context::dummy(); + let id = object::new(ctx); + + // There is no field with this name, so `borrow` aborts. + let _: &u64 = sui::dynamic_field::borrow(&id, b"missing"); + + id.delete(); +} +// ANCHOR_END: foreign_abort_code diff --git a/packages/samples/sources/your-first-move/hello_world.move b/packages/samples/sources/your-first-move/hello_world.move index d6132559e..0a1b61ffc 100644 --- a/packages/samples/sources/your-first-move/hello_world.move +++ b/packages/samples/sources/your-first-move/hello_world.move @@ -6,7 +6,7 @@ module book::hello_world; use std::string::String; public fun hello_world(): String { - b"Hello, World!".to_string() + "Hello, World!" } #[test_only] @@ -14,6 +14,6 @@ use std::unit_test::assert_eq; #[test] fun test_is_hello_world() { - let expected = b"Hello, World!".to_string(); + let expected: String = "Hello, World!"; assert_eq!(hello_world(), expected); } diff --git a/packages/samples/sources/your-first-move/hello_world_debug.move b/packages/samples/sources/your-first-move/hello_world_debug.move index 4a0ced349..8a6eadecf 100644 --- a/packages/samples/sources/your-first-move/hello_world_debug.move +++ b/packages/samples/sources/your-first-move/hello_world_debug.move @@ -7,7 +7,7 @@ use std::string::String; use std::debug; public fun hello_world(): String { - let result = b"Hello, World!".to_string(); + let result: String = "Hello, World!"; debug::print(&result); result } @@ -17,7 +17,7 @@ use std::unit_test::assert_eq; #[test] fun test_is_hello_world() { - let expected = b"Hello, World!".to_string(); + let expected: String = "Hello, World!"; let actual = hello_world(); assert_eq!(actual, expected); diff --git a/packages/samples/sources/your-first-move/hello_world_docs.move b/packages/samples/sources/your-first-move/hello_world_docs.move index e3000fb0c..e6da52442 100644 --- a/packages/samples/sources/your-first-move/hello_world_docs.move +++ b/packages/samples/sources/your-first-move/hello_world_docs.move @@ -8,7 +8,7 @@ use std::string::String; /// As the name says: returns a string "Hello, World!". public fun hello_world(): String { - b"Hello, World!".to_string() + "Hello, World!" } #[test_only] @@ -17,7 +17,7 @@ use std::unit_test::assert_eq; #[test] /// This is a test for the `hello_world` function. fun test_is_hello_world() { - let expected = b"Hello, World!".to_string(); + let expected: String = "Hello, World!"; let actual = hello_world(); assert_eq!(actual, expected); diff --git a/reference/abort-and-assert/clever-errors.md b/reference/abort-and-assert/clever-errors.md index 5129cd34e..a2717a294 100644 --- a/reference/abort-and-assert/clever-errors.md +++ b/reference/abort-and-assert/clever-errors.md @@ -79,6 +79,35 @@ above is present in the `u64` abort code when coupled with the module where the > Clever abort code values do _not_ need to be a `vector` -- it can be any valid constant type > in Move. +## Explicit Error Codes + +By default, a clever error derives its identifying information entirely from the source -- the line +of the abort, and the name and value of the constant. The `#[error]` attribute also accepts an +explicit `code` argument, written `#[error(code = )]`, which attaches a developer-chosen code to +the error: + +```move +module 0x42::a_module; + +/// Tries to create an object twice with the same parent-key combination. +#[error(code = 0)] +const EObjectAlreadyExists: vector = b"Derived object is already claimed."; +``` + +The code is an unsigned 8-bit integer, and it is stored in its own field of the `u64` abort code, +separate from the line number and from the constant's name and value. Unlike the line number, which +shifts whenever the source file changes, the code is fixed by the developer, so it gives each error +a stable numeric identifier that tooling can display and match on. When a code is present, decoders +surface it alongside the rendered message, for example: + +``` +Error from '0x42::a_module::claim' (line 22), error code 0, 'EObjectAlreadyExists': "Derived object is already claimed." +``` + +The constant's name and value are still recorded, so the human-readable message renders just as it +does for a bare `#[error]`. Assigning explicit codes this way is the convention used throughout the +Sui Framework, where each module gives its error constants small, stable codes. + ## Assertions with no Abort Codes Assertions and `abort` statements without an abort code will automatically derive an abort code from @@ -194,6 +223,9 @@ Note that the Move abort will come with some additional information -- important module where the error occurred. This is important because the identifier index, and constant index are relative to the module's identifier and constant tables (if not set the sentinel values). +The high bits that this layout labels _reserved_ also hold the explicit error code set with +[`#[error(code = N)]`](#explicit-error-codes), in a dedicated 8-bit field, when one is provided. + > To decode a clever abort code, you will need to know the module where the error occurred if either > the identifier index or constant index are not set to the sentinel value of `0xffff`. diff --git a/reference/index.md b/reference/index.md index db1611549..d5f340e10 100644 --- a/reference/index.md +++ b/reference/index.md @@ -4,6 +4,8 @@ description: "The official Move language reference: comprehensive documentation # The Move Reference +_by the Move contributors, adapted for Sui with contributions from the Move community_ + Welcome to Move, a next generation language for secure asset programming. Its primary use case is in blockchain environments, where Move programs are used to construct state changes. Move allows developers to write programs that flexibly manage and transfer assets, while providing the security diff --git a/site/.gitignore b/site/.gitignore index b2d6de306..c81b8c8f7 100644 --- a/site/.gitignore +++ b/site/.gitignore @@ -18,3 +18,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# Generated files of production builds (kept separate so builds do not +# clobber a running dev server, see package.json build script) +.docusaurus-build diff --git a/site/docusaurus.config.ts b/site/docusaurus.config.ts index 37c72234a..3a6d56801 100644 --- a/site/docusaurus.config.ts +++ b/site/docusaurus.config.ts @@ -35,7 +35,15 @@ export default { plugins: [ llmsTxt, - '@docusaurus/plugin-sitemap', + [ + '@docusaurus/plugin-sitemap', + { + // Use git history to emit for each page. + lastmod: 'date', + // Keep utility routes out of the sitemap. + ignorePatterns: ['/404', '/search'], + }, + ], [ '@docusaurus/plugin-content-docs', { @@ -156,7 +164,7 @@ export default { stylesheets: [ { - href: 'https://fonts.googleapis.com/css2?family=Rubik:wght@300&display=swap', + href: 'https://fonts.googleapis.com/css2?family=Rubik:wght@300&display=swap', type: 'text/css', }, { @@ -166,6 +174,20 @@ export default { ], headTags: [ + // Bitmap favicon fallbacks: DuckDuckGo, Bing and older browsers do not + // render the SVG favicon and look for /favicon.ico or PNG links instead. + { + tagName: 'link', + attributes: { rel: 'icon', href: '/favicon.ico', sizes: '48x48' }, + }, + { + tagName: 'link', + attributes: { rel: 'icon', href: '/favicon-32x32.png', type: 'image/png', sizes: '32x32' }, + }, + { + tagName: 'link', + attributes: { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '180x180' }, + }, { tagName: 'script', attributes: { diff --git a/site/package.json b/site/package.json index 8de0ab8bd..28cfcddd2 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", - "build": "docusaurus build", + "build": "DOCUSAURUS_GENERATED_FILES_DIR_NAME=.docusaurus-build docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", diff --git a/site/src/plugins/llms-txt.ts b/site/src/plugins/llms-txt.ts index c75f413aa..a8875f5a2 100644 --- a/site/src/plugins/llms-txt.ts +++ b/site/src/plugins/llms-txt.ts @@ -93,7 +93,20 @@ function extractAnchor(fileContent: string, anchor: string): string | null { const contentStart = fileContent.indexOf('\n', startIdx) + 1; const endIdx = fileContent.indexOf(endMarker, contentStart); if (endIdx === -1) return null; - return fileContent.slice(contentStart, endIdx).trimEnd(); + + const snippet = fileContent + .slice(contentStart, endIdx) + .trimEnd() + .split('\n') + .filter((line) => !line.includes('// ANCHOR')); + + // Strip the common leading indentation so a snippet anchored inside a + // function body renders flush-left; relative nesting is preserved. Mirrors + // the dedent in the remark plugin (mdbook-anchor-code.ts). + const nonBlank = snippet.filter((line) => line.trim().length > 0); + if (nonBlank.length === 0) return snippet.join('\n'); + const indent = Math.min(...nonBlank.map((line) => line.match(/^[ \t]*/)![0].length)); + return snippet.map((line) => (line.trim().length > 0 ? line.slice(indent) : '')).join('\n'); } function stripHtmlComments(content: string): string { diff --git a/site/src/plugins/mdbook-anchor-code.ts b/site/src/plugins/mdbook-anchor-code.ts index 7d711a533..9c386fd19 100644 --- a/site/src/plugins/mdbook-anchor-code.ts +++ b/site/src/plugins/mdbook-anchor-code.ts @@ -47,9 +47,21 @@ const plugin: Plugin<[Options?], Root> = ({ rootDir = process.cwd() } = {}) => { throw new Error(`No end anchor for "${anchor}" in ${absPath}`); } - content = lines + const snippet = lines .slice(start + 1, end) - .filter((e) => !e.includes('// ANCHOR_END') && !e.includes('// ANCHOR')) + .filter((e) => !e.includes('// ANCHOR_END') && !e.includes('// ANCHOR')); + + // Strip the common leading indentation, so a snippet anchored inside a + // function body renders flush-left instead of jumping to the right. + // Relative (nested) indentation is preserved; blank lines are ignored + // when measuring and left empty in the output. + const indent = Math.min( + ...snippet + .filter((line) => line.trim().length > 0) + .map((line) => line.match(/^[ \t]*/)![0].length), + ); + content = snippet + .map((line) => (line.trim().length > 0 ? line.slice(indent) : '')) .join('\n'); } diff --git a/site/src/theme/prism-move.js b/site/src/theme/prism-move.js index 16a2ffbe1..f02b5dfe0 100644 --- a/site/src/theme/prism-move.js +++ b/site/src/theme/prism-move.js @@ -305,6 +305,14 @@ }, }, + // Plain string literal: "hello". Must come after the `b"..."` and + // `x"..."` forms so those prefixed literals are matched first. + 'quoted-string-literal': { + pattern: /"(\\[\s\S]|[^\\"])*"/, + greedy: true, + alias: 'string', + }, + 'macro-call': { pattern: /\b(\w+)!/, inside: { diff --git a/site/static/apple-touch-icon.png b/site/static/apple-touch-icon.png new file mode 100644 index 000000000..852a01ddb Binary files /dev/null and b/site/static/apple-touch-icon.png differ diff --git a/site/static/favicon-32x32.png b/site/static/favicon-32x32.png new file mode 100644 index 000000000..c4c7a7f89 Binary files /dev/null and b/site/static/favicon-32x32.png differ diff --git a/site/static/favicon.ico b/site/static/favicon.ico new file mode 100644 index 000000000..d50276dd7 Binary files /dev/null and b/site/static/favicon.ico differ diff --git a/site/static/robots.txt b/site/static/robots.txt new file mode 100644 index 000000000..c4c31cac7 --- /dev/null +++ b/site/static/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://move-book.com/sitemap.xml