Document not found (404)
-This URL is invalid, sorry. Please use the navigation bar or search to continue.
- -diff --git a/sui/404.html b/sui/404.html deleted file mode 100644 index ad5dd817..00000000 --- a/sui/404.html +++ /dev/null @@ -1,222 +0,0 @@ - - -
- - -This URL is invalid, sorry. Please use the navigation bar or search to continue.
- -Control flow statements are used to control the flow of execution in a program. They are used to make decisions, to repeat a block of code, and to exit a block of code early. Sui has the following control flow statements (explained in detail below):
+Control flow statements are used to control the flow of execution in a program. They are used to make decisions, to repeat a block of code, and to exit a block of code early. Move has the following control flow statements (explained in detail below):
if
and else
expressionsloop
and while
loopsbreak
and continue
statementsreturn
statementif
and if-else
- making decisions on whether to execute a block of codeloop
and while
loops - repeating a block of codebreak
and continue
statements - exiting a loop earlyreturn
statement - exiting a function earlyThe if
expression is used to make decisions in a program. It evaluates a boolean 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 the if
expression is:
if (<bool_expression>) <expression>;
+if (<bool_expression>) <expression> else <expression>;
+
+Just like any other expression, if
requires a semicolon, if there are other expressions following it. The else
keyword is optional, except for the case when the resulting value is assigned to a variable. We will cover this below.
module book::if_condition {
+ #[test]
+ fun test_if() {
+ let x = 5;
+
+ // `x > 0` is a boolean expression.
+ if (x > 0) {
+ std::debug::print(&b"X is bigger than 0".to_string())
+ };
+ }
+}
+
+Let's see how we can use if
and else
to assign a value to a variable:
module book::if_else {
+ #[test]
+ fun test_if_else() {
+ let x = 5;
+ let y = if (x > 0) {
+ 1
+ } else {
+ 0
+ };
+
+ assert!(y == 1, 0);
+ }
+}
+
+Here we assign the value of the if
expression to the variable y
. If x
is greater than 0, y
will be assigned the value 1, otherwise 0. The else
block is necessary, because both branches must return a value of the same type. If we omit the else
block, the compiler will throw an error.
Conditional expressions are one of the most important control flow statements in Move. They can use either user provided input or some already stored data to make decisions. In particular, they are used in the assert!
macro to check if a condition is true, and if not, to abort execution. We will get to it very soon!
Loops are used to execute a block of code multiple times. Move has two built-in types of loops: loop
and while
. In many cases they can be used interchangeably, but usually while
is used when the number of iterations is known in advance, and loop
is used when the number of iterations is not known in advance or there are multiple exit points.
Loops are helpful when dealing with collections, such as vectors, or when we want to repeat a block of code until a certain condition is met. However, it is important to be careful with loops, as they can lead to infinite loops, which can lead to gas exhaustion and the transaction being aborted.
+while
loopThe while
statement is used to execute a block of code as long as a boolean expression is true. Just like we've seen with if
, the boolean expression is evaluated before each iteration of the loop. Just like conditional statements, the while
loop is an expression and requires a semicolon if there are other expressions following it.
The syntax for the while
loop is:
while (<bool_expression>) <expression>;
+
+Here is an example of a while
loop with a very simple condition:
module book::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.
+ //
+ // If `x` is 0, then the function will return 10.
+ // If `x` is 5, then the function will return 5.
+ fun while_loop(x: u8): u8 {
+ let mut y = 0;
+
+ // This will loop until `x` is 10.
+ // And will never run if `x` is 10 or more.
+ while (x < 10) {
+ y = y + 1;
+ };
+
+ y
+ }
+
+ #[test]
+ fun test_while() {
+ assert!(while_loop(0) == 10, 0); // 10 times
+ assert!(while_loop(5) == 5, 0); // 5 times
+ assert!(while_loop(10) == 0, 0); // loop never executed
+ }
+}
+
+loop
Now let's imagine a scenario where the boolean expression is always true
. For example, if we literally passed true
to the while
condition. As you might expect, this would create an infinite loop, and this is almost what the loop
statement works like.
module book::infinite_while {
+ #[test]
+ fun test_infinite_while() {
+ let mut x = 0;
+
+ // This will loop forever.
+ while (true) {
+ x = x + 1;
+ };
+
+ // This line will never be executed.
+ assert!(x == 5, 0);
+ }
+}
+
+An infinite while
, or while
without a condition, is a loop
. The syntax for it is simple:
loop <expression>;
+
+Let's rewrite the previous example using loop
instead of while
:
module book::infinite_loop {
+ #[test]
+ fun test_infinite_loop() {
+ let mut x = 0;
+
+ // This will loop forever.
+ loop {
+ x = x + 1;
+ };
+
+ // This line will never be executed.
+ assert!(x == 5, 0);
+ }
+}
+
+
+Infinite loops on their own are not very useful in Move, since every operation in Move costs gas, and an infinite loop will lead to gas exhaustion. However, they can be used in combination with break
and continue
statements to create more complex loops.
As we already mentioned, infinite loops are rather useless on their own. And that's where we introduce the break
and continue
statements. They are used to exit a loop early, and to skip the rest of the current iteration, respectively.
Syntax for the break
statement is:
break;
+
+The break
statement is used to stop the execution of a loop and exit it early. It is often used in combination with a conditional statement to exit the loop when a certain condition is met. To illustrate this point, let's turn the infinite loop
from the previous example into something that looks and behaves more like a while
loop:
module book::break_loop {
+ #[test]
+ fun test_break_loop() {
+ let mut x = 0;
+
+ // This will loop until `x` is 5.
+ loop {
+ x = x + 1;
+
+ // If `x` is 5, then exit the loop.
+ if (x == 5) {
+ break; // Exit the loop.
+ }
+ };
+
+ assert!(x == 5, 0);
+ }
+}
+
+Almost identical to the while
loop, right? The break
statement is used to exit the loop when x
is 5. If we remove the break
statement, the loop will run forever, just like the previous example.
The continue
statement is used to skip the rest of the current iteration and start the next one. Similarly to break
, it is used in combination with a conditional statement to skip the rest of the iteration when a certain condition is met.
Syntax for the continue
statement is
continue;
+
+The example below skips odd numbers and prints only even numbers from 0 to 10:
+module book::continue_loop {
+ #[test]
+ fun test_continue_loop() {
+ let mut x = 0;
+
+ // This will loop until `x` is 10.
+ loop {
+ x = x + 1;
+
+ // If `x` is odd, then skip the rest of the iteration.
+ if (x % 2 == 1) {
+ continue; // Skip the rest of the iteration.
+ }
+
+ std::debug::print(&x);
+
+ // If `x` is 10, then exit the loop.
+ if (x == 10) {
+ break; // Exit the loop.
+ }
+ };
+
+ assert!(x == 10, 0); // 10
+ }
+}
+
+break
and continue
statements can be used in both while
and loop
loops.
The return
statement is used to exit a function early and return a value. It is often used in combination with a conditional statement to exit the function when a certain condition is met. The syntax for the return
statement is:
return <expression>;
+
+Here is an example of a function that returns a value when a certain condition is met:
+module book::return_statement {
+ // This function returns `true` if `x` is greater than 0 and not 5,
+ // otherwise it returns `false`.
+ fun is_positive(x: u8): bool {
+ if (x == 5) {
+ return false;
+ }
+
+ if (x > 0) {
+ return true;
+ };
+
+ false
+ }
+
+ #[test]
+ fun test_return() {
+ assert!(is_positive(5), false);
+ assert!(is_positive(0), false);
+ }
+}
+
+Unlike in other languages, the return
statement is not required for the last expression in a function. The last expression in a function block is automatically returned. However, the return
statement is useful when we want to exit a function early if a certain condition is met.
Type reflection is an important part of the language, and it is a crucial part of some of the advanced patterns.
+Type reflection is an important part of the language, and it is a crucial part of some of the more advanced patterns.
@@ -245,6 +245,22 @@This chapter covers the prerequisites for the Move language: how to set up your IDE, how to install the compiler and what is Move 2024. If you are already familiar with these topics or have a CLI installed, you can skip this chapter and proceed to the next one.
-Move is a compiled language, so you need to install a compiler to be able to write and run Move programs. The compiler is included into the Sui binary, which can be installed or downloaded using one of the methods below.
-You can download the latest Sui binary from the releases page. The binary is available for macOS, Linux and Windows. For education purposes and development, we recommend using the mainnet
version.
You can install Sui using the Homebrew package manager.
-brew install sui
-
-You can install and build Sui locally by using the Cargo package manager (requires Rust)
-cargo install --git https://github.com/MystenLabs/sui.git --bin sui --branch mainnet
-
-For troubleshooting the installation process, please refer to the Install Sui Guide.
-There are two most popular IDEs for Move development: VSCode and IntelliJ IDEA. Both of them provide basic features like syntax highlighting and error messages, though they differ in their additional features. Whatever IDE you choose, you'll need to use the terminal to run the Move CLI.
---IntelliJ Plugin does not support Move 2024 edition fully, some syntax won't get highlighted and not supported.
-
Web based IDE from Github, can be run right in the browser and provides almost a full-featured VSCode experience.
-Move 2024 is the new version of the Move language that is maintained by Mysten Labs. All of the examples in this book are written in Move 2024. If you're used to the pre-2024 version of Move, please, refer to the Move 2024 Migration Guide to learn about the differences between the two versions.
- -In this section you'll get to experience the Move language and the Move compiler first-hand. You'll learn how to create a package, write a simple module, test it and generate documentation. You can also use this section as a quick CLI reference for your own projects.
-This guide mentions topics which you will learn later in this book. If you are not familiar with some of the concepts, don't worry, you'll learn them later. Try to focus on the task at hand and don't get distracted by the details.
---It is important that you have a working Move environment. If you haven't set it up yet, please refer to the Install Sui section.
-
This section is divided into the following parts (in order):
- -It's time to write your first Move program. We'll start with the classic "Hello World" program which returns a String.
-First, you need to initialize a new project. Assuming you have Sui installed, run the following command:
-$ sui move new hello_world
-
-Sui CLI has a move
subcommand which is used to work with Move packages. The new
subcommand creates a new package with the given name in a new directory. In our case, the package name is hello_world
, and it is located in the hello_world
directory.
To make sure that the package was created successfully, we can check the contents of the current directory, and see that there is a new hello_world
path.
$ ls | grep hello_world
-hello_world
-
-
-If the output looks like this, then everything is fine, and we can proceed. The folder structure of the package is the folowing:
-hello_world
-├── Move.toml
-├── src/
-│ └── hello_world.move
-└── tests/
- └── hello_world_tests.move
-
-The address I'm using in this book is always 0x0
and the name for it is "book". You can use any address you want, but make sure to change it in the examples. To make the examples work as is, please, add the following address to the [addresses]
section in the Move.toml
:
# Move.toml
-[addresses]
-std = "0x1"
-book = "0x0"
-
-Let's create a new module called hello_world
. To do so, create a new file in the sources
folder called hello_world.move
. So that the structure looks like this:
sources/
- hello_world.move
-Move.toml
-
-And then add the following code to the hello_world.move
file:
// sources/hello_world.move
-module book::hello_world {
- use std::string::{Self, String};
-
- public fun hello_world(): String {
- string::utf8(b"Hello, World!")
- }
-}
-
-While it's not a hard restriction, it's is considered a good practice to name the module the same as the file. So, in our case, the module name is hello_world
and the file name is hello_world.move
.
The module name and function names should be in snake_case
- all lowercase letters with underscores between words. You can read more about coding conventions in the Coding Conventions section.
Let's take a closer look at the code we just wrote:
-module book::hello_world {
-}
-
-The first line of code declares a module called hello_world
stored at the address book
. The contents of the module go inside the curly braces {}
. The last line closes the module declaration with a closing curly brace }
. We will go through the module declaration in more detail in the Modules section.
Then we import two members of the std::string
module (which is part of the std
package). The string
module contains the String
type, and the Self
keyword imports the module itself, so we can use its functions.
use std::string::{Self, String};
-
-Then we define a hello_world
function using the keyword fun
which takes no arguments and returns a String
type. The public
keyword marks the visibility of the function - "public" functions can be accessed by other modules. The function body is inside the curly braces {}
.
--In the Function section we will learn more about functions.
-
public fun hello_world(): String {
- string::utf8(b"Hello, World!")
- }
-
-The function body consists of a single function call to the string::utf8
function and returns a String
type. The expression is a bytestring literal b"Hello World!"
.
To compile the package, run the following command:
-$ sui move build
-
-If you see this (or - for other binaries - similar) output, then everything is fine, and the code compiled successfully:
-> UPDATING GIT DEPENDENCY https://github.com/move-language/move.git
-> INCLUDING DEPENDENCY MoveStdlib
-> BUILDING Book
-
-Congratulations! You've just compiled your first Move program. Now, let's add a test and some logging so we see that it works.
-To run a Move program there needs to be an environment which stores packages and executes transactions. The best way to test a Move program is to write some tests and run them locally. Move has built-in testing functionality, and the tests are written in Move as well. In this section, we will learn how to write tests for our hello_world
module.
First, let's try to run tests. All of the Move binaries support the test
command, and this is the command we will use to run tests:
$ sui move test
-
-If you see similar output, then everything is fine, and the test command has run successfully:
-INCLUDING DEPENDENCY MoveStdlib
-BUILDING Book Samples
-Running Move unit tests
-Test result: OK. Total tests: 0; passed: 0; failed: 0
-
-As you can see, the test command has run successfully, but it didn't find any tests. Let's add some tests to our module.
-When the test command runs, it looks for all tests in all files in the directory. Tests can be either placed separate modules or in the same module as the code they test. First, let's add a test function to the hello_world
module:
module book::hello_world {
- use std::string::{Self, String};
-
- public fun hello_world(): String {
- string::utf8(b"Hello, World!")
- }
-
- #[test]
- fun test_is_hello_world() {
- let expected = string::utf8(b"Hello, World!");
- assert!(hello_world() == expected, 0)
- }
-}
-
-The test function is a function with a #[test]
attribute. Normally it takes no arguments (but it can take arguments in some cases - you'll learn more about it closer to the end of this book) and returns nothing. Tests placed in the same module as the code they test are called "unit tests". They can access all functions and types in the module. We'll go through them in more detail in the Test section.
#[test]
- fun test_is_hello_world() {
- let expected = string::utf8(b"Hello, World!");
- assert!(hello_world() == expected, 0)
- }
-
-Inside the test function, we define the expected outcome by creating a String with the expected value and assign it to the expected
variable. Then we use the special built-in assert!()
which takes two arguments: a conditional expression and an error code. If the expression evaluates to false
, then the test fails with the given error code. The equality operator ==
compares the actual
and expected
values and returns true
if they are equal. We'll learn more about expressions in the Expression and Scope section.
Now let's run the test command again:
-$ sui move test
-
-You should see this output, which means that the test has run successfully:
-...
-Test result: OK. Total tests: 1; passed: 1; failed: 0
-
-Try replacing the equality operator ==
inside the assert!
with the inequality operator !=
and run the test command again.
assert!(hello_world() != expected, 0)
-
-You should see this output, which means that the test has failed:
-Running Move unit tests
-[ FAIL ] 0x0::hello_world::test_is_hello_world
-
-Test failures:
-
-Failures in 0x0::hello_world:
-
-┌── test_is_hello_world ──────
-│ error[E11001]: test failure
-│ ┌─ ./sources/your-first-move/hello_world.move:14:9
-│ │
-│ 12 │ fun test_is_hello_world() {
-│ │ ------------------- In this function in 0x0::hello_world
-│ 13 │ let expected = string::utf8(b"Hello, World!");
-│ 14 │ assert!(hello_world() != expected, 0)
-│ │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Test was not expected to error, but it aborted with code 0 originating in the module 00000000000000000000000000000000::hello_world rooted here
-│
-│
-└──────────────────
-
-Test result: FAILED. Total tests: 1; passed: 0; failed: 1
-
-Tests are used to verify the execution of the code. If the code is correct, the test should pass, otherwise it should fail. In this case, the test failed because we intentionally made a mistake in the test code. However, normally you should write tests that check the correctness of the code, not the other way around!
-In the next section, we will learn how to debug Move programs and print intermediate values to the console.
-Now that we have a package with a module and a test, let's take a slight detour and learn how to debug Move programs. Move Compiler has a built-in debugging tool that allows you to print intermediate values to the console. This is especially useful when you are writing tests and want to see what's going on inside the program.
-To use the debug
module, we need to import it in our module. Imports are usually grouped together for readability and they are placed at the top of the module. Let's add the import statement to the hello_world
module:
module book::hello_world {
- use std::string::{Self, String};
- use std::debug; // the added import!
-
-Having imported the std::debug
module, we can now use its functions. Let's add a debug::print
function call to the hello_world
function. To achieve that we need to change the function body. Instead of returning the value right away we will assign it to a variable, print it to the console and then return it:
public fun hello_world(): String {
- let result = string::utf8(b"Hello, World!");
- debug::print(&result);
- result
- }
-
-First, run the build command:
-$ sui move build
-
-The output does not contain anything unusual, because our code was never executed. But running build
is an important part of the routine - this way we make sure that the changes we added can compile. Let's run the test command now:
$ sui move test
-
-The output of the test command now contains the "Hello, World!" string:
-INCLUDING DEPENDENCY MoveNursery
-INCLUDING DEPENDENCY MoveStdlib
-BUILDING Book Samples
-Running Move unit tests
-[debug] "Hello, World!"
-[ PASS ] 0x0::hello_world::test_is_hello_world
-Test result: OK. Total tests: 1; passed: 1; failed: 0
-
-Now every time the hello_world
function is run in tests, you'll see the "Hello, World!" string in the output.
Debug should only be used in local environment and never published on-chain. Usually, during the publish, the debug
module is either removed from the package or the publishing fails with an error. There's no way to use this functionality on-chain.
There's one trick that allows you to save some time while debugging. Instead of adding a module-level import, use a fully qualified function name. This way you don't need to add an import statement to the module, but you can still use the debug::print
function:
std::debug::print(&my_variable);
-
-Be mindful that the value passed into debug should be a reference (the &
symbol in front of the variable name). If you pass a value, the compiler will emit an error.
Move CLI has a built-in tool for generating documentation for Move modules. The tool is included into the binary and available out of the box. In this section we will learn how to generate documentation for our hello_world
module.
To generate documentation for a module, we need to add documentation comments to the module and its functions. Documentation comments are written in Markdown and start with ///
(three slashes). For example, let's add a documentation comment to the hello_world
module:
/// This module contains a function that returns a string "Hello, World!".
-module book::hello_world {
-
-Doc comments placed above the module are linked to the module itself, while doc comments placed above the function are linked to the function.
- /// As the name says: returns a string "Hello, World!".
- public fun hello_world(): String {
- string::utf8(b"Hello, World!")
- }
-
-If a documented member has an attribute, such as #[test]
in the example below, the doc comment must be placed after the attribute:
--While it is possible to document
-#[test]
functions, doc comments for tests will not be included in the generated documentation.
#[test]
- /// This is a test for the `hello_world` function.
- fun test_is_hello_world() {
- let expected = string::utf8(b"Hello, World!");
- let actual = hello_world();
-
- assert!(actual == expected, 0)
- }
-
-To generate documentation for a module, we need to run the sui move build
command with a --doc
flag. Let's run the command:
$ sui move build --doc
-...
-...
-BUILDING Book Samples
-
---Alternatively, you can use
-move test --doc
- this can be useful if you want to test and generate documentation at the same time. For example, as a part of your CI/CD pipeline.
Once the build is complete, the documentation will be available in the build/docs
directory. Each modile will have its own .md
file. The documentation for the hello_world
module will be available in the build/docs/hello_world.md
file.
<a name="0x0_hello_world"></a>
-
-# Module `0x0::hello_world`
-This module contains a function that returns a string "Hello, World!".
-- [Function `hello_world`](#0x0_hello_world_hello_world)
-<pre><code><b>use</b> <a href="">0x1::debug</a>;
-<b>use</b> <a href="">0x1::string</a>;
-</code></pre>
-<a name="0x0_hello_world_hello_world"></a>
-
-## Function `hello_world`
-As the name says: returns a string "Hello, World!".
-<pre><code><b>fun</b> <a href="hello_world.md#0x0_hello_world">hello_world</a>(): <a href="_String">string::String</a>
-</code></pre>
-<details>
-<summary>Implementation</summary>
-<pre><code><b>fun</b> <a href="hello_world.md#0x0_hello_world">hello_world</a>(): String {
- <b>let</b> result = <a href="_utf8">string::utf8</a>(b"Hello, World!");
- <a href="_print">debug::print</a>(&result);
- result
-}
-</code></pre>
-</details>
-
-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 for that, it will give you a good understanding of the basic concepts required to write Move programs on Sui.
-Move is a language for writing smart contracts - programs that 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. A published package can be interacted with by sending transactions calling its functions. It can also act as a dependency for other packages.
---To create a new package, use the
-sui move new
command. -To learn more about the command, runsui move new --help
.
Package consists of modules - separate scopes that contain functions, types, and other items.
-package 0x...
- module a
- struct A1
- fun hello_world()
- module b
- struct B1
- fun hello_package()
-
-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. Packages usually looks like this:
sources/
- my_module.move
- another_module.move
- ...
-tests/
- ...
-examples/
- using_my_module.move
-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 availably in tests. The examples
directory can be used for code examples, and is also not published on-chain.
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 on the blockchain containing its modules' bytecode. A published package becomes immutable and can be interacted with by sending transactions.
0x...
- my_module: <bytecode>
- another_module: <bytecode>
-
-The Move.toml
is a manifest file that describes the package and its dependencies. It is written in TOML format and contains multiple sections, the most important of which are [package]
, [dependencies]
and [addresses]
.
[package]
-name = "my_project"
-version = "0.0.0"
-edition = "2024"
-
-[dependencies]
-Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
-
-[addresses]
-std = "0x1"
-alice = "0xA11CE"
-
-[dev-addresses]
-alice = "0xB0B"
-
-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 edition for the compiler.
name
- the name of the package when it is imported;version
- the version of the package, can be used in release management;edition
- the edition of the Move language; currently, the only valid value is 2024
.The [dependencies]
section is used to specify the dependencies of the project. Each dependency is specified as a key-value pair, where the key is the name of the dependency, and the value is the dependency specification. The dependency specification can be a git repository URL or a path to the local directory.
# git repository
-Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
-
-# local directory
-MyPackage = { 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.
Sometimes dependencies have conflicting versions of the same package. For example, if you have two dependencies that use different versions of the Sui package, you can override the dependency in the [dependencies]
section. To do so, add the override
field to the dependency. The version of the dependency specified in the [dependencies]
section will be used instead of the one specified in the dependency itself.
[dependencies]
-Sui = { override = true, git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
-
-It is possible to add [dev-dependencies]
section to the manifest. It is used to override dependencies in the dev and test modes. For example, if you want to use a different version of the Sui package in the dev mode, you can add a custom dependency specification to the [dev-dependencies]
section.
The [addresses]
section is used to add aliases for the addresses. Any address can be specified in this section, and then used in the code as an alias. For example, if you add alice = "0xA11CE"
to this section, you can use alice
as 0xA11CE
in the code.
The [dev-addresses]
section is the same as [addresses]
, but only works for the test and dev modes. Important to note that it is impossible to introduce new aliases in this section, only override the existing ones. So in the example above, if you add alice = "0xB0B"
to this section, the alice
address will be 0xB0B
in the test and dev modes, and 0xA11CE
in the regular build.
The TOML format supports two styles for tables: inline and multiline. The examples above are using the inline style, but it is also possible to use the multiline style. You wouldn't want to use it for the [package]
section, but it can be useful for the dependencies.
# Inline style
-[dependencies]
-Sui = { override = true, git = "", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
-MyPackage = { local = "../my-package" }
-
-# Multiline style
-[dependencies.Sui]
-override = true
-git = "https://github.com/MystenLabs/sui.git"
-subdir = "crates/sui-framework/packages/sui-framework"
-rev = "framework/testnet"
-
-[dependencies.MyPackage]
-local = "../my-package"
-
-Address is a unique identifier of a location on the blockchain. It is used to identify packages, accounts, and objects. Address has a fixed size of 32 bytes and is usually represented as a hexadecimal string prefixed with 0x
. Addresses are case insensitive.
0xe51ff5cd221a81c3d6e22b9e670ddf99004d71de4f769b0312b68c7c4872e2f1
-
-The address above is an example of a valid address. It is 64 characters long (32 bytes) and is prefixed 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 left.
0x1 = 0x0000000000000000000000000000000000000000000000000000000000000001
-
-Here are some examples of reserved addresses:
-0x1
- address of the Sui Standard Library (alias std
)0x2
- address of the Sui Framework (alias sui
)0x5
- address of the Sui System object0x6
- address of the system Clock object0x403
- address of the DenyList system objectModule is the basic unit of organization in a package. A module is a separate scope that contains functions, types, and other items. A package consists of one or more modules.
-Accounts interact with the blockchain by sending transactions. Transactions can call functions in a package, and can also deploy new packages. On Sui, a single transaction can contain multiple operations, we call them "commands". A command can be a call to a function, a deployment of a new package, upgrade of an existing one, or a combination of these. Commands can return values, which can be used in subsequent commands.
- -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.
-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.
- -Transactions consist of:
-Sui does not have global storage. Instead, storage is split into a pool of objects. Some of the objects are owned by accounts and available only to them, and some are shared and can be accessed by anyone on the network. There's also a special kind of shared immutable objects, also called frozen, which can't be modified, and act as public chain-wide constants.
-Each object has a unique 32-byte identifier - UID, it is used to access and reference the object.
- -Sui object consists of:
-key
abilityshared
, account_address
, object_owner
or immutable
In this chapter we illustrate the concepts of Sui by building a simple application. Unlike the Hello World example which aims to illustrate Move Compiler, this application is focused on Sui specifics. It is also more complex - it uses objects, and we will publish and use it on Sui.
-The goal of this mini-project is to demonstrate the process of building, testing, and publishing a Sui application. The result is a simple but complete application that you can use as a starting point for your projects or as a playground to experiment with Sui as you learn.
-The chapter is split into the following parts (in order):
- -Additionally, there's a section with ideas for further development of the application which you may get back to as you progress through the book.
-Just like we did with the Hello World example, we will start by initializing a new package using the Sui CLI, then we will implement a simple application that creates a "Postcard" - a digital postcard that can be sent to a friend.
-Sui packages are no different to regular Move packages, and can be initialized using the sui
CLI. The following command will create a new package called postcard
:
$ sui new postcard
-
-This will create a new directory called postcard
with the following structure:
postcard
-├── Move.toml
-├── src/
-│ └── postcard.move
-└── tests/
- └── postcard_tests.move
-
-The package manifest - Move.toml
- already contains all required dependencies for Sui, and the src/postcard.move
file is pre-created with a simple module layout.
--In case the
-Move.toml
file does not feature theedition
field, please, add it manually. Theedition
field under the[package]
section should be set to2024.beta
.Like this:
-edition = "2024.beta"
The Postcard application will be a simple module that defines an object, and a set of functions to create, modify and send the postcard to any address.
-Let's start by inserting the code. Replace the contents of the src/postcard.move
file with the following:
module postcard::postcard {
- use std::string::String;
- use sui::object::UID;
- use sui::transfer;
- use sui::tx_context::TxContext;
-
- use fun sui::object::new as TxContext.new;
-
- /// The Postcard object.
- public struct Postcard has key {
- /// The unique identifier of the Object.
- /// Created using the `object::new()` function.
- id: UID,
- /// The message to be printed on the gift card.
- message: String,
- }
-
- /// Create a new Postcard with a message.
- public fun new(message: String, ctx: &mut TxContext): Postcard {
- Postcard {
- id: ctx.new(),
- message,
- }
- }
-
- /// Send the Postcard to the specified address.
- public fun send_to(card: Postcard, to: address) {
- transfer::transfer(card, to)
- }
-
- /// Keep the Postcard for yourself.
- public fun keep(card: Postcard, ctx: &TxContext) {
- transfer::transfer(card, ctx.sender())
- }
-
- /// Update the message on the Postcard.
- public fun update(card: &mut Postcard, message: String) {
- card.message = message
- }
-}
-
-To make sure that everything is working as expected, run this command:
-$ sui move build
-
-You should see this output, indicating that the package was built successfully. There shouldn't be any errors following the BUILDING postcard
line:
> $ sui move build
-UPDATING GIT DEPENDENCY https://github.com/MystenLabs/sui.git
-INCLUDING DEPENDENCY Sui
-INCLUDING DEPENDENCY MoveStdlib
-BUILDING postcard
-
-If you do see errors, please, double check the code and the steps you've taken to create the package. It's very likely a typo in one of the commands.
-In the next section we will take a closer look at the structure of the postcard.move
file and explain the code we've just inserted. We will also discuss the imports and the object definition in more detail.
Let's take a look at the code we've inserted into the postcard.move
file. We will discuss the structure of the module and the code in more detail, and explain the way the Postcard
object is created, used and stored.
First line of the file is the module declaration. The address of the module is package
- a name defined in the Move.toml
file. The module name is also postcard
. The module body is enclosed in curly braces {}
.
module postcard::postcard {
-
-In the top of the module we import types and other modules from the Standard Library (std) and from the Sui Framework (sui). The Sui Framework is required to define and create objects as it contains the UID
and TxContext
types - two essential types for objects.
We also import the sui::transfer
module - this module contains storage and transfer functions.
use std::string::String;
- use sui::object::UID;
- use sui::transfer;
- use sui::tx_context::TxContext;
-
-A public struct Postcard
, that goes after imports, is an object. A struct with the key
ability is an object on Sui. As such, its first field must be id
of type UID
(that we imported from the Sui Framework). The id
field is the unique identifier and an address of the object.
/// The Postcard object.
- public struct Postcard has key {
- /// The unique identifier of the Object.
- /// Created using the `object::new()` function.
- id: UID,
- /// The message to be printed on the gift card.
- message: String,
- }
-
-Sui has no global storage, and the objects are stored independently of their package. This is why we defined a single Postcard
and not a collection "Postcards". Objects have to be created and stored in the storage before they can be used.
The new
function is a public function that creates a new instance of the Postcard
object and returns it to the caller. It takes two arguments: the message of type String
, which is the message on the postcard, and the ctx
of type TxContext
, a standard type that is automatically inserted by the Sui runtime.
/// Create a new Postcard with a message.
- public fun new(message: String, ctx: &mut TxContext): Postcard {
- Postcard {
- id: ctx.new(),
- message,
- }
- }
-
-When initializing an instance of Postcard
we pass the fields of the struct as arguments, the id
is generated from the TxContext
argument via the ctx.new()
call. And the message
is taken as-is from the message
argument.
Objects can't be ignored, so when the function new
is called, the returned Postcard
needs to be stored. And here's when the sui::transfer
module comes into play. The sui::transfer::transfer
function is used to store the object at the specified address.
/// Send the Postcard to the specified address.
- public fun send_to(card: Postcard, to: address) {
- transfer::transfer(card, to)
- }
-
-The function takes the Postcard
as the first argument and a value of the address
type as the second argument. Both are passed into the transfer
function to send — and hence, store — the object to the specified address.
A very common scenario is transfering the object to the caller. This can be done by calling the send_to
function with the sender address. It can be read from the ctx
argument, which is a TxContext
type.
/// Keep the Postcard for yourself.
- public fun keep(card: Postcard, ctx: &TxContext) {
- transfer::transfer(card, ctx.sender())
- }
-
-The update
function is another public function that takes a mutable reference to the Postcard
and a String
argument. It updates the message
field of the Postcard
. Because the Postcard
is passed by a reference, the owner is not changed, and the object is not moved.
/// Update the message on the Postcard.
- public fun update(card: &mut Postcard, message: String) {
- card.message = message
- }
-
-In the next section we will write a simple test for the Postcard
module to see how it works. Later we will publish the package on Sui DevNet and learn how to use the Sui CLI to interact with the package.
Now that we know what a package, account and storage are, let's get to the basics and learn to write some code.
-This section covers:
-Module is the base unit of code organization in Move. Modules are used to group and isolate code, and all of the members of the module are private to the module by default. In this section you will learn how to define a module, how to declare its members and how to access them from other modules.
-Modules are declared using the module
keyword followed by the package address, module name and the module body inside the curly braces {}
. The module name should be in snake_case
- all lowercase letters with underscores between words. Modules 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. You can read more about coding conventions in the Coding Conventions section.
module book::my_module {
- // module body
-}
-
-Structs, functions and constants, imports and friend declarations are all part of the module:
- -Module address can be specified as both: an address literal (does not require the @
prefix) or a named address specified in the Package Manifest. In the example below, both are identical because there's a book = "0x0"
record in the [addresses]
section of the Move.toml
.
module book::my_module {
- // module body
-}
-
-module 0x0::address_literal_example {
- // module body
-}
-
-Module members are declared inside the module body. To illustrate that, let's define a simple module with a struct, a function and a constant:
-module book::my_module_with_members {
- // import
- use book::my_module;
-
- // friend declaration
- friend book::constants;
-
- // a constant
- const CONST: u8 = 0;
-
- // a struct
- public struct Struct {}
-
- // method alias
- public use fun function as Struct.struct_fun;
-
- // function
- fun function(_: &Struct) { /* function body */ }
-}
-
-Before the introduction of the address::module_name
syntax, modules were organized into address {}
blocks. This way of code organization is still available today, but is not used widely. Modern practices imply having a single module per file, so the address {}
block is rather a redundant construct.
--Module addresses can be omitted if modules are organized into
-address {}
blocks.
address book { // address block
-
-module another_module {
- // module body
-}
-
-module yet_another_module {
- // module body
-}
-}
-
-The modules defined in this code sample will be accessible as:
-book::another_module
book::yet_another_module
Comments are a way to add notes or document your code. They are ignored by the compiler and don't result in the Move bytecode. You can use comments to explain what your code does, to add notes to yourself or other developers, to temporarily remove a part of your code, or to generate documentation. There are three types of comments in Move: line comment, block comment, and doc comment.
-#[allow(unused_function)]
-module book::comments_line {
- fun some_function() {
- // this is a comment line
- }
-}
-
-You can use double slash //
to comment out the rest of the line. Everything after //
will be ignored by the compiler.
#[allow(unused_function, unused_variable)]
-module book::comments_line_2 {
- // let's add a note to everything!
- fun some_function_with_numbers() {
- let a = 10;
- // let b = 10 this line is commented and won't be executed
- let b = 5; // here comment is placed after code
- a + b; // result is 15, not 10!
- }
-}
-
-Block comments are used to comment out a block of code. They start with /*
and end with */
. Everything between /*
and */
will be ignored by the compiler. You can use block comments to comment out a single line or multiple lines. You can even use them to comment out a part of a line.
#[allow(unused_function)]
-module book::comments_block {
- fun /* you can comment everywhere */ go_wild() {
- /* here
- there
- everywhere */ let a = 10;
- let b = /* even here */ 10; /* and again */
- a + b;
- }
- /* you can use it to remove certain expressions or definitions
- fun empty_commented_out() {
-
- }
- */
-}
-
-This example is a bit extreme, but it shows how you can use block comments to comment out a part of a line.
-Documentation comments are special comments that are used to generate documentation for your code. They are similar to block comments, but they start with three slashes ///
and are placed before the definition of the item they document.
#[allow(unused_function, unused_const, unused_variable, unused_field)]
-/// Module has documentation!
-module book::comments_doc {
-
- /// This is a 0x0 address constant!
- const AN_ADDRESS: address = @0x0;
-
- /// This is a struct!
- public struct AStruct {
- /// This is a field of a struct!
- a_field: u8,
- }
-
- /// This function does something!
- /// And it's documented!
- fun do_something() {}
-}
-
-
-For simple values, Move has a number of built-in primitive types. They're the base that makes up all other types. The primitive types are:
-However, before we get to the types, let's first look at how to declare and assign variables in Move.
-Variables are declared using the let
keyword. They are immutable by default, but can be made mutable using the let mut
keyword. The syntax for the let mut
statement is:
let <variable_name>[: <type>] = <expression>;
-let mut <variable_name>[: <type>] = <expression>;
-
-Where:
-<variable_name>
- the name of the variable<type>
- the type of the variable, optional<expression>
- the value to be assigned to the variablelet x: bool = true;
-let mut y: u8 = 42;
-
-A mutable variable can be reassigned using the =
operator.
y = 43;
-
-Variables can also be shadowed by re-declaring.
-let x: u8 = 42;
-let x: u8 = 43;
-
-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, there's no need to explicitly specify the type - the compiler can infer it from the value.
let x = true;
-let y = false;
-
-Booleans are often used to store flags and to control the flow of the program. Please, refer to the Control Flow section for more information.
-Move supports unsigned integers of various sizes: from 8-bit to 256-bit. The integer types are:
-u8
- 8-bitu16
- 16-bitu32
- 32-bitu64
- 64-bitu128
- 128-bitu256
- 256-bitlet x: u8 = 42;
-let y: u16 = 42;
-// ...
-let z: u256 = 42;
-
-Unlike booleans, integer types need to be inferred. In most of the 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.
// Both are equivalent
-let x: u8 = 42;
-let x = 42u8;
-
-Move supports the standard arithmetic operations for integers: addition, subtraction, multiplication, division, and remainder. The syntax for these operations is:
-Syntax | Operation | Aborts If |
---|---|---|
+ | addition | Result is too large for the integer type |
- | subtraction | Result is less than zero |
* | multiplication | Result is too large for the integer type |
% | modular division | The divisor is 0 |
/ | truncating division | The divisor is 0 |
The type of the operands must match, otherwise, 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.
- - -as
Move supports explicit casting between integer types. The syntax for it is:
-(<expression> as <type>)
-
-Note, that it requires parentheses around the expression to prevent ambiguity.
-let x: u8 = 42;
-let y: u16 = (x as u16);
-
-A more complex example, preventing overflow:
-let x: u8 = 255;
-let y: u8 = 255;
-let z: u16 = (x as u16) + ((y as u16) * 2);
-
-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.
-let x = 255u8;
-let y = 1u8;
-
-// This will raise an error
-let z = x + y;
-
-To represent addresses, Move uses a special type called address
. It is a 32 byte value that can be used to represent any address on the blockchain. Addresses are used in two syntax forms: hexadecimal addresses prefixed with 0x
and named addresses.
// address literal
-let value: address = @0x1;
-
-// named address registered in Move.toml
-let value = @std;
-let other = @sui;
-
-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 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.
Sui Framework offers a set of helper functions to work with addresses. Given that the address type 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<u8>
type.
Example: Convert an address to a u256
type and back.
use sui::address;
-
-let addr_as_u256: u256 = address::to_u256(@0x1);
-let addr = address::from_u256(addr_as_u256);
-
-Example: Convert an address to a vector<u8>
type and back.
use sui::address;
-
-let addr_as_u8: vector<u8> = address::to_bytes(@0x1);
-let addr = address::from_bytes(addr_as_u8);
-
-Example: Convert an address into a string.
-use sui::address;
-use std::string;
-
-let addr_as_string: String = address::to_string(@0x1);
-
-In programming languages expression is a unit of code which returns a value, in Move, almost everything is an expression, - with the sole exception of let
statement which is a declaration. 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 an empty expression()
.
The very base of the expression is the empty expression. It is a valid expression that does nothing and returns nothing. An empty expression is written as empty parentheses ()
. It's rarely the case when you need to use an empty expression. The compiler automatically inserts empty expressions where needed, for example in an empty Scope. Though, it may be helpful to know that it exists. Parentheses are also used to group expressions to control the order of evaluation.
// variable `a` has no value;
-let a = ();
-
-// similarly, we could write:
-let a;
-
-In the Primitive Types section, we introduced the basic types of Move. And to illustrate them, we used literals. A literal is a notation for representing a fixed value in the source code. Literals are used to initialize variables and to pass arguments to functions. Move has the following literals:
-true
and false
for boolean values0
, 1
, 123123
or other numeric for integer values0x0
, 0x1
, 0x123
or other hexadecimal for integer valuesb"bytes_vector"
for byte vector valuesx"0A"
HEX literal for byte valueslet b = true; // true is a literal
-let n = 1000; // 1000 is a literal
-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
-
-Ariphmetic, logical, and bitwise operators are used to perform operations on values. The result of an operation is a value, so operators are also expressions.
-let sum = 1 + 2; // 1 + 2 is an expression
-let sum = (1 + 2); // the same expression with parentheses
-let is_true = true && false; // true && false is an expression
-let is_true = (true && false); // the same expression with parentheses
-
-A block is a sequence of statements and expressions, and it returns the value of the last expression in the block. A block is written as a pair of curly braces {}
. A block is an expression, so it can be used anywhere an expression is expected.
// block with an empty expression, however, the compiler will
-// insert an empty expression automatically: `let none = { () }`
-let none = {};
-
-// block with let statements and an expression.
-let sum = {
- let a = 1;
- let b = 2;
- a + b // last expression is the value of the block
-};
-
-let none = {
- let a = 1;
- let b = 2;
- a + b; // not returned - semicolon.
- // compiler automatically inserts an empty expression `()`
-};
-
-We go into detail about functions in the Functions section. However, we already used function calls in the 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.
-fun add(a: u8, b: u8): u8 {
- a + b
-}
-
-#[test]
-fun some_other() {
- let sum = add(1, 2); // add(1, 2) is an expression with type u8
-}
-
-Control flow expressions are used to control the flow of the program. They are also expressions, so they return a value. We cover control flow expressions in the Control Flow section. Here's a very brief overview:
-// if is an expression, so it returns a value; if there are 2 branches,
-// the types of the branches must match.
-if (bool_expr) expr1 else expr2;
-
-// while is an expression, but it returns `()`.
-while (bool_expr) expr;
-
-// loop is an expression, but returns `()` as well.
-loop expr;
-
-Move 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 just on the data level, but also in its behavior. 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.
--Note: Move does not support recursive structs, meaning a struct cannot contain itself as a field.
-
/// A struct representing an artist.
-public struct Artist {
- /// The name of the artist.
- name: String,
-}
-
-/// A struct representing a music record.
-public struct Record {
- /// The title of the record.
- title: String,
- /// The artist of the record. Uses the `Artist` type.
- artist: Artist,
- /// The year the record was released.
- year: u16,
- /// Whether the record is a debut album.
- is_debut: bool,
- /// The edition of the record.
- edition: Option<u16>,
-}
-
-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<u16>
. The edition
field is of type Option<u16>
to represent that the edition is optional.
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 for more information on different visibility modifiers.
---A struct by default is internal to the module it is defined in.
-
We described how struct definition works. 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 fields must be set.
// Create an instance of the `Artist` struct.
-let artist = Artist {
- name: string::utf8(b"The Beatles"),
-};
-
-In the example above, we create an instance of the Artist
struct and set the name
field to a string "The Beatles".
To access the fields of a struct, you can use the .
operator followed by the field name.
// Access the `name` field of the `Artist` struct.
-let artist_name = artist.name;
-
-// Access a field of the `Artist` struct.
-assert!(artist.name == string::utf8(b"The Beatles"), 0);
-
-// Mutate the `name` field of the `Artist` struct.
-artist.name = string::utf8(b"Led Zeppelin");
-
-// Check that the `name` field has been mutated.
-assert!(artist.name == string::utf8(b"Led Zeppelin"), 1);
-
-Only 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.
Structs are non-discardable by default, meaning that the initiated struct value must be used: either stored or unpacked. 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.
// Unpack the `Artist` struct and create a new variable `name`
-// with the value of the `name` field.
-let Artist { name } = artist;
-
-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.
// Unpack the `Artist` struct and create a new variable `name`
-// with the value of the `name` field. The variable is intentionally unused.
-let Artist { name: _ } = artist;
-
-Move has a unique type system which allows defining type abilities. In the previous section, 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. In this section, we introduce the first ability - drop
.
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
. In this section, we cover the first two abilities: copy
and drop
. The last two abilities are covered in the programmability chapter, when we introduce Objects and storage operations.
/// This struct has the `copy` and `drop` abilities.
-struct VeryAble has copy, drop {
- // field: Type1,
- // field2: Type2,
- // ...
-}
-
-A struct without abilities cannot be discarded, or copied, or stored in the storage. We call such a struct a Hot Potato. It is a joke, but it is also a good way to remember that a struct without abilities is like a hot potato - it needs to be passed around and handled properly. Hot Potato is one of the most powerful patterns in Move, we go in detail about it in the TODO: authorization patterns chapter.
-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.
module book::drop_ability {
-
- /// This struct has the `drop` ability.
- struct IgnoreMe has drop {
- a: u8,
- b: u8,
- }
-
- /// This struct does not have the `drop` ability.
- struct NoDrop {}
-
- #[test]
- // Create an instance of the `IgnoreMe` struct and ignore it.
- // Even though we constructed the instance, we don't need to unpack it.
- fun test_ignore() {
- let no_drop = NoDrop {};
- let _ = IgnoreMe { a: 1, b: 2 }; // no need to unpack
-
- // The value must be unpacked for the code to compile.
- let NoDrop {} = no_drop; // OK
- }
-}
-
-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.
A struct with a single drop
ability is called a Witness. We explain the concept of a Witness in the Witness and Abstract Implementation section.
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 your own code.
-Modules defined in the same package can import each other. The use
keyword is followed by the module path, which consists of the package address (or alias) and the module name separated by ::
.
File: sources/module_one.move
-// File: sources/module_one.move
-module book::module_one {
- /// Struct defined in the same module.
- public struct Character has drop {}
-
- /// Simple function that creates a new `Character` instance.
- public fun new(): Character { Character {} }
-}
-
-File: sources/module_two.move
-module book::module_two {
- use book::module_one; // importing module_one from the same package
-
- /// Calls the `new` function from the `module_one` module.
- public fun create_and_ignore() {
- let _ = module_one::new();
- }
-}
-
-You can also import specific members from a module. This is useful when you only need a single function or a single type from a module. The syntax is the same as for importing a module, but you add the member name after the module path.
-module book::more_imports {
- use book::module_one::new; // imports the `new` function from the `module_one` module
- use book::module_one::Character; // importing the `Character` struct from the `module_one` module
-
- /// Calls the `new` function from the `module_one` module.
- public fun create_character(): Character {
- new()
- }
-}
-
-Imports can be grouped into a single use
statement using the curly braces {}
. This is useful when you need to import multiple members from the same module. Move allows grouping imports from the same module and from the same package.
module book::grouped_imports {
- // imports the `new` function and the `Character` struct from
- /// the `module_one` module
- use book::module_one::{new, Character};
-
- /// Calls the `new` function from the `module_one` module.
- public fun create_character(): Character {
- new()
- }
-}
-
-Single function imports are less common in Move, since the function names can overlap and cause 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.
module book::self_imports {
- // imports the `Character` struct, and the `module_one` module
- use book::module_one::{Self, Character};
-
- /// Calls the `new` function from the `module_one` module.
- public fun create_character(): Character {
- module_one::new()
- }
-}
-
-When importing multiple members from different modules, it is possible to have name conflicts. For example, if you import two modules that both have a function with the same name, you will need to use the module path to access the function. It is also possible to have modules with the same name in different packages. To resolve the conflict and avoid ambiguity, Move offers the as
keyword to rename the imported member.
module book::conflict_resolution {
- // `as` can be placed after any import, including group imports
- use book::module_one::{Self as mod, Character as Char};
-
- /// Calls the `new` function from the `module_one` module.
- public fun create(): Char {
- mod::new_two()
- }
-}
-
-Every new package generated via the sui
binary features a Move.toml
file with a single dependency on the Sui Framework package. The Sui Framework depends on the Standard Library package. And both of these packages are available in default configuration. Package dependencies are defined in the Package Manifest as follows:
[dependencies]
-Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" }
-Local = { local = "../my_other_package" }
-
-The dependencies
section contains a list of package dependencies. The key is the name of the package, 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 package directory.
If a dependency is added to the Move.toml
file, the compiler will automatically fetch (and later refetch) the dependencies when building the package.
Normally, packages define their addresses in the [addresses]
section, so you can use the alias instead of the address. For example, instead of 0x2::coin
module, you would use sui::coin
. The sui
alias is defined in the Sui Framework package. Similarly, the std
alias is defined in the Standard Library package and can be used to access the standard library modules.
To import a module from another package, you use the use
keyword followed by the module path. The module path consists of the package address (or alias) and the module name separated by ::
.
module book::imports {
- use std::string; // std = 0x1, string is a module in the standard library
- use sui::coin; // sui = 0x2, coin is a module in the Sui Framework
-}
-
-The Move Standard Library provides functionality for native types and operations. It is a standard collection of modules which does utilize the storage model, and operates on native types. It is the only dependency of the Sui Framework, and is imported together with it.
-In this book we go into detail about most of the modules in the standard library, however, it is also helpful to give an overview of the features, so that you can get a sense of what is available and which module implements that.
-Module | Description | Chapter |
---|---|---|
std::debug | Contains debugging functions | Debugging |
std::type_name | Allows runtime type reflection | Generics |
std::string | Provides basic string operations | Strings |
std::ascii | Provides basic ASCII operations | Strings |
std::option | Implements an Option<T> | Option |
std::vector | Native operations on the vector type | Vector |
std::hash | Hashing functions: sha2_256 and sha3_256 | Cryptography and Hashing |
std::bcs | Contains the bcs::to_bytes() function | BCS |
std::address | Contains a single address::length function | Address |
std::bit_vector | Provides operations on bit vectors | - |
std::fixed_point32 | Provides the FixedPoint32 type | - |
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.
-MoveStdlib = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "framework/mainnet" }
-
-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.
The vector
type is defined 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 followed by square brackets containing the elements (or no elements for an empty vector).
/// An empty vector of bool elements.
-let empty: vector<bool> = vector[];
-
-/// A vector of u8 elements.
-let v: vector<u8> = vector[10, 20, 30];
-
-/// A vector of vector<u8> elements.
-let vv: vector<vector<u8>> = vector[
- vector[10, 20],
- vector[30, 40]
-];
-
-The vector
type is a built-in type in Move, and does not need to be imported from a module. However, vector operations are defined in the std::vector
module, and you need to import the module to use them.
The standard library provides methods to manipulate vectors. The following are some of the most commonly used operations:
-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.module book::play_vec {
-
- #[test]
- fun vector_methods_test() {
- let mut v = vector[10u8, 20, 30];
-
- assert!(v.length() == 3, 0);
- assert!(!v.is_empty(), 1);
-
- v.push_back(40);
- let last_value = v.pop_back();
-
- assert!(last_value == 40, 2);
- }
-}
-
-A vector of non-droppable types cannot be discarded. If you define a vector of types without drop
ability, the vector value cannot be ignored. However, if the vector is empty, compiler requires an explicit call to destroy_empty
function.
module book::non_droppable_vec {
- struct NoDrop {}
-
- #[test]
- fun test_destroy_empty() {
- let v = vector<NoDrop>[];
- // while we know that `v` is empty, we still need to call
- // the explicit `destroy_empty` function to discard the vector.
- v.destroy_empty();
- }
-}
-
-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, and is defined as follows:
/// Abstraction of a value that may or may not be present.
-struct Option<Element> has copy, drop, store {
- vec: vector<Element>
-}
-
-The Option
is a generic type which takes a type parameter Element
. It has a single field vec
which is a vector
of Element
. Vector can have length 0 or 1, and this is used to represent the presence or absence of a value.
Option type has two variants: Some
and None
. Some
variant contains a value and 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, and it is used to avoid the need for empty or undefined
values.
To showcase why 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.
module book::user_registry {
- use std::string::String;
- use std::option::Option;
-
- /// A struct representing a user record.
- struct User has copy, drop {
- first_name: String,
- middle_name: Option<String>,
- last_name: String,
- }
-
- /// Create a new `User` struct with the given fields.
- public fun register(
- first_name: String,
- middle_name: Option<String>,
- last_name: String,
- ): User {
- User { first_name, middle_name, last_name }
- }
-}
-
-In the example above, the middle_name
field is of type Option<String>
. This means that the middle_name
field can either contain a String
value or be empty. This makes it clear that the middle name is optional, and it avoids the need for extra checks to differentiate between an empty string and a missing middle name.
To use the Option
type, you need to import the std::option
module and use the Option
type. You can then create an Option
value using the some
or none
methods.
use std::option;
-
-// `option::some` creates an `Option` value with a value.
-let opt_name = option::some(b"Alice");
-
-// `option.is_some()` returns true if option contains a value.
-assert!(opt_name.is_some(), 1);
-
-// internal value can be `borrow`ed and `borrow_mut`ed.
-assert!(option.borrow() == &b"Alice", 0);
-
-// `option.extract` takes the value out of the option.
-let inner = opt_name.extract();
-
-// `option.is_none()` returns true if option is None.
-assert!(opt_name.is_none(), 2);
-
-While Move does not have a built-in to represent strings, it does have a string
module in the Standard Library that provides a String
type. The string
module represents UTF-8 encoded strings, and another module, ascii
, provides an ASCII-only String
type.
Sui execution environment also allows Strings as transaction arguments, so in many cases, String does not to be constructed in the Transaction Block.
-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 extra checks and functionality than a vector of bytes, but under the hood, they are just vectors of bytes.
module book::string_bytes {
- /// Anyone can implement a custom string-like type by wrapping a vector.
- struct MyString {
- bytes: vector<u8>,
- }
-
- /// Implement a `from_bytes` function to convert a vector of bytes to a string.
- public fun from_bytes(bytes: vector<u8>): MyString {
- MyString { bytes }
- }
-
- /// Implement a `bytes` function to convert a string to a vector of bytes.
- public fun bytes(self: &MyString): &vector<u8> {
- &self.bytes
- }
-}
-
-Both standard types provide conversions from and to vectors of bytes.
-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, and hence is more efficient than the ascii
module. To create a string or perform operations on it, you must import the string
module:
module book::strings {
- use std::string::{Self, String};
-
- #[test]
- fun using_strings() {
- // strings are normally created using the `utf8` function
- let mut hello = string::utf8(b"Hello");
- let world = string::utf8(b", World!");
-
- // strings can be concatenated using the `append_utf8` function
- let hello_world = hello.append_utf8(*world.bytes());
-
- // just like any other type, strings can be compared
- assert!(hello_world == string::utf8(b"Hello, World!"), 0x0);
- }
-}
-
-The default utf8
method is potentially unsafe, as it does not check that the bytes passed to it are valid UTF-8. If you are not sure that the bytes you are passing are valid UTF-8, you should use the try_utf8
method instead. It returns an Option<String>
, which is None
if the bytes are not valid UTF-8:
--The
-try_*
pattern is used throughout the standard library to indicate that a function may fail. For more information, see the Error Handling section.
module book::safe_strings {
- use std::string::{Self, String};
-
- #[test]
- fun safe_strings() {
- // this is a valid UTF-8 string
- let hello = string::try_utf8(b"Hello");
-
- assert!(hello.is_some(), 0); // abort if the value is not valid UTF-8
-
- // this is not a valid UTF-8 string
- let invalid = string::try_utf8(b"\xFF");
-
- assert!(invalid.is_none(), 0); // abort if the value is valid UTF-8
- }
-}
-
-TODO: ASCII strings
-TODO: summary
-Control flow statements are used to control the flow of execution in a program. They are used to make decisions, to repeat a block of code, and to exit a block of code early. Sui has the following control flow statements (explained in detail below):
-if
and else
expressionsloop
and while
loopsbreak
and continue
statementsreturn
statementConstants are immutable values that are defined at the module level. They often serve as a way to give names to 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 internal to the module and can not be accessed from other modules.
-module book::shop_price {
- use sui::coin::{Self, Coin};
- use sui::sui::SUI;
-
- /// The price of an item in the shop.
- const ITEM_PRICE: u64 = 100;
-
- /// An item sold in the shop.
- struct Item { /* ... */ }
-
- /// Purchase an item from the shop.
- public fun purchase(coin: Coin<SUI>): Item {
- assert!(coin.value() == ITEM_PRICE, 0);
-
- Item { /* ... */ }
- }
-}
-
-Constants are named using UPPER_SNAKE_CASE
. This is a convention that is used throughout the Move codebase. It's a way to make constants stand out from other identifiers in the code. Move compiler will error if the first letter of a constant is not an uppercase letter.
Constants can't be changed and assigned new values. They are part of the package bytecode, and inherently immutable.
-module book::immutable_constants {
- const ITEM_PRICE: u64 = 100;
-
- // emits an error
- fun change_price() {
- ITEM_PRICE = 200;
- }
-}
-
-The abort
keyword is used to abort the execution of a transaction. It is used in combination with an abort code, which will be returned to the caller of the transaction. The abort code is an integer of type u64
and can be any value.
let user_has_access = true;
-
-// abort with a predefined constant if `user_has_access` is false
-if (!user_has_access) {
- abort 0
-};
-
-// there's an alternative syntax using parenthesis`
-if (user_has_access) {
- abort(0)
-};
-
-/* ... */
-
-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
.
// aborts if `user_has_access` is false with abort code 0
-assert!(user_has_access, 0);
-
-// expands into:
-if (!user_has_access) {
- abort 0
-};
-
-To make error codes more descriptive, it is a good practice to define error constants. Error constants are defined as const
declarations and are usually prefixed with E
followed by a camel case name. Error constatns are no different from other constants and don't have special handling. So their addition is purely a practice for better code readability.
/// Error code for when the user has no access.
-const ENoAccess: u64 = 0;
-/// Trying to access a field that does not exist.
-const ENoField: u64 = 1;
-
-// asserts are way more readable now
-assert!(user_has_access, ENoAccess);
-assert!(field_exists, ENoField);
-
-We suggest reading the Better Error Handling guide to learn about best practices for error handling in Move.
-Functions are the building blocks of Move programs. They are called from user transactions 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.
module book::math {
- /// Function takes two arguments of type `u64` and returns their sum.
- /// The `public` visibility modifier makes the function accessible from
- /// outside the module.
- public fun add(a: u64, b: u64): u64 {
- a + b
- }
-
- #[test]
- fun test_add() {
- let sum = add(1, 2);
- assert!(sum == 3, 0);
- }
-}
-
-In this example, we define a function add
that takes two arguments of type u64
and returns their sum. The function is called from the test_add
function, which is a test function located in the same module. In the test we compare the result of the add
function with the expected value and abort the execution if the result is different.
--There's a convention to call functions in Move with the
-snake_case
naming convention. This means that the function name should be all lowercase with words separated by underscores. For example,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 in the function body is the return value of the function.
fun return_nothing() {
- // empty expression, function returns `()`
-}
-
-Just like any other module member, functions can be imported and accessed via a path. The path consists of the module path and the function name separated by ::
. For example, if you have a function called add
in the math
module in the book
package, the path to it will be book::math::add
, or, if the module is imported, math::add
.
module book::use_math {
- use book::math;
-
- fun call_add() {
- // function is called via the path
- let sum = math::add(1, 2);
- }
-}
-
-Move functions can return multiple values, which is useful when you need to return more than one value from a function. The return type of the function is a tuple of types. The return value is a tuple of expressions.
-fun get_name_and_age(): (vector<u8>, u8) {
- (b"John", 25)
-}
-
-Result of a function call with tuple return has to be unpacked into variables via let (tuple)
syntax:
// declare name and age as immutable
-let (name, age) = get_name_and_age();
-
-If any of the declared values need to be declared as mutable, the mut
keyword is placed before the variable name:
// declare name as mutable, age as immutable
-let (mut name, age) = get_name_and_age();
-
-If some of the arguments are not used, they can be ignored with the _
symbol:
// ignore the name, declare age as mutable
-let (_, mut age) = get_name_and_age();
-
-Move Compiler supports receiver syntax, which allows defining methods which can be called on instances of a struct. This is similar to the method syntax in other programming languages. It is a convenient way to define functions which operate on the fields of a struct.
-If the first argument of a function is a struct internal to the module, then the function can be called using the .
operator. If the function uses a struct from another module, then method won't be associated with the struct by default. In this case, the function can be called using the standard function call syntax.
When a module is imported, the methods are automatically associated with the struct.
-module book::hero {
- /// A struct representing a hero.
- struct Hero has drop {
- health: u8,
- mana: u8,
- }
-
- /// Create a new Hero.
- public fun new(): Hero { Hero { health: 100, mana: 100 } }
-
- /// A method which casts a spell, consuming mana.
- public fun heal_spell(hero: &mut Hero) {
- hero.health = hero.health + 10;
- hero.mana = hero.mana - 10;
- }
-
- /// A method which returns the health of the hero.
- public fun health(hero: &Hero): u8 { hero.health }
-
- /// A method which returns the mana of the hero.
- public fun mana(hero: &Hero): u8 { hero.mana }
-
- #[test]
- // Test the methods of the `Hero` struct.
- fun test_methods() {
- let mut hero = new();
- hero.heal_spell();
-
- assert!(hero.health() == 110, 1);
- assert!(hero.mana() == 90, 2);
- }
-}
-
-For modules that define multiple structs and their methods, it is possible to define method aliases to avoid name conflicts, or to provide a better-named method for a struct.
-The syntax for aliases is:
-// for local method association
-use fun <function_path> as <Type>.<method_name>;
-
-// exported alias
-public use fun <function_path> as <Type>.<method_name>;
-
---Public aliases are only allowed for structs defined in the same module. If a struct is defined in another module, an alias can still be created but cannot be made public.
-
In the example below, we changed the hero
module and added another type - Villain
. Both Hero
and Villain
have similar field names and methods. And to avoid name conflicts, we prefixed methods with hero_
and villain_
respectively. However, we can create aliases for these methods so that they can be called on the instances of the structs without the prefix.
module book::hero_and_villain {
- /// A struct representing a hero.
- struct Hero has drop {
- health: u8,
- }
-
- /// A struct representing a villain.
- struct Villain has drop {
- health: u8,
- }
-
- /// Create a new Hero.
- public fun new_hero(): Hero { Hero { health: 100 } }
-
- /// Create a new Villain.
- public fun new_villain(): Villain { Villain { health: 100 } }
-
- // Alias for the `hero_health` method. Will be imported automatically when
- // the module is imported.
- public use fun hero_health as Hero.health;
-
- public fun hero_health(hero: &Hero): u8 { hero.health }
-
- // Alias for the `villain_health` method. Will be imported automatically
- // when the module is imported.
- public use fun villain_health as Villain.health;
-
- public fun villain_health(villain: &Villain): u8 { villain.health }
-
- #[test]
- // Test the methods of the `Hero` and `Villain` structs.
- fun test_associated_methods() {
- let mut hero = new_hero();
- assert!(hero.health() == 100, 1);
-
- let mut villain = new_villain();
- assert!(villain.health() == 100, 3);
- }
-}
-
-As you can see, in the test function, we called the health
method on the instances of Hero
and Villain
without the prefix. The compiler will automatically associate the methods with the structs.
It is also possible to associate a function defined in another module with a struct from the current module. Following the same approach, we can create an alias for the method defined in another module. Let's use the bcs::to_bytes
method from the Standard Library and associate it with the Hero
struct. It will allow serializing the Hero
struct to a vector of bytes.
module book::hero_to_bytes {
- use std::bcs;
-
- // 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;
-
- /// A struct representing a hero.
- struct Hero has drop {
- health: u8,
- mana: u8,
- }
-
- /// Create a new Hero.
- public fun new(): Hero { Hero { health: 100, mana: 100 } }
-
- // Alias for the `bcs::to_string` method.
- public use fun bcs::to_bytes as Hero.to_bytes;
-
- #[test]
- // Test the methods of the `Hero` struct.
- fun test_hero_serialize() {
- let mut hero = new();
- let serialized = hero.to_bytes();
- assert!(serialized.length() == 3, 1);
- }
-}
-
-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 friend - visible in "friend" modules within the same package, or entry - can be called from a transaction but can't be called from other modules.
-A function or a struct defined in a module which has no visibility modifier is private.
-module book::internal_visbility {
- // This function can be called from other functions in the same module
- fun internal() { /* ... */ }
-
- // Same module -> can call internal()
- fun call_internal() {
- internal();
- }
-}
-
-Move compiler won't allow this code to compile:
- -module book::try_calling_internal {
- use book::internal_visbility;
-
- // Different module -> can't call internal()
- fun try_calling_internal() {
- internal_visbility::internal();
- }
-}
-
-TODO: public visibility
-TODO: friend visibility
-TODO: 2024 public(package)
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.
- -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. Once the function scope end, the variables defined in it are dropped or deallocated.
-module book::ownership {
- public fun owner() {
- let a = 1; // a is owned by the `owner` function
- } // a is dropped here
-
- #[test]
- fun test_owner() {
- owner();
- // a is not valid here
- }
-}
-
-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.
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.
module book::ownership {
- public fun owner(): u8 {
- let a = 1; // a defined here
- a // scope ends, a is returned
- }
-
- #[test]
- fun test_owner() {
- let a = owner();
- // a is valid here
- } // a is dropped here
-}
-
-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.
module book::ownership {
- public fun owner(): u8 {
- let a = 10;
- a
- } // a is returned
-
- public fun take_ownership(v: u8) {
- // v is owned by `take_ownership`
- } // v is dropped here
-
- #[test]
- fun test_owner() {
- let a = owner();
- take_ownership(a);
- // a is not valid here
- }
-}
-
-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.
-module book::ownership {
- public fun owner() {
- let a = 1; // a is owned by the `owner` function's scope
- {
- let b = 2; // b is owned by the block
- {
- let c = 3; // c is owned by the block
- }; // 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, shall we use the return value of a block, the ownership of the variable is transferred to the caller of the block.
-module book::ownership {
- public fun owner(): u8 {
- let a = 1; // a is owned by the `owner` function's scope
- let b = {
- let c = 2; // c is owned by the block
- c // c is returned
- }; // c is dropped here
- a + b // both a and b are valid here
- }
-}
-
-Some types in Move are copyable, which means that they can be copied without transferring the ownership. This is useful for types that are small and cheap to copy, such as integers and booleans. Move compiler will automatically copy these types when they are passed to a function or returned from a function, or when they're moved to a scope and then accessed in their original scope.
-In Move, the copy ability on a type indicates that the instance or the value of the type can be copied. While this behavior may feel very natural when working with numbers or other simple types, it is not the default for custom types in Move. This is because Move is designed to express digital assets and resources, and inability to copy is a key element of the resource model.
-However, Move type system allows you to define custom types with the copy ability.
-public struct Copyable has copy {}
-
-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.
let a = Copyable {};
-let b = a; // `a` is copied to `b`
-let c = *&b; // explicit copy via dereference operator
-
-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.
The copy
ability is closely related to drop
ability. If a type has the copy ability, very likely that it should have drop
too. This is because the drop ability is required to clean up the resources when the instance is no longer needed. If a type has only copy, then managing its instances gets more complicated, as the values cannot be ignored.
public struct Value has 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.
-In the previous section we explained the ownership and scope in Move. We showed how the value is moved to a new scope, and how it changes the owner. In this section, we will explain how to borrow a reference to a value to avoid moving it, and how Move's borrow checker ensures that the references are used correctly.
-References are a way to borrow a value without changing its owner. Immutable references allow the function to read the value without changing it or moving it. And mutable references allow the function to read and modify the value without moving it. To illustrate this, let's consider a simple example - an application for a metro (subway) pass. We will look at 4 different scenarios:
-module book::references {
-
- /// Error code for when the card is empty.
- const ENoUses: u64 = 0;
-
- /// Number of uses for a metro pass card.
- const USES: u8 = 3;
-
- /// A metro pass card
- struct Card { uses: u8 }
-
- /// Purchase a metro pass card.
- public fun purchase(/* pass a Coin */): Card {
- Card { uses: USES }
- }
-
- /// Show the metro pass card to the inspector.
- public fun show(card: &Card): bool {
- card.uses > 0
- }
-
- /// Use the metro pass card at the turnstile to enter the metro.
- public fun enter_metro(card: &mut Card) {
- assert!(card.uses > 0, ENoUses);
- card.uses = card.uses - 1;
- }
-
- /// Recycle the metro pass card.
- public fun recycle(card: Card) {
- assert!(card.uses == 0, ENoUses);
- let Card { uses: _ } = card;
- }
-
- #[test]
- fun test_card() {
- // declaring variable as mutable because we modify it
- let mut card = purchase();
-
- card.enter_metro(); // modify the card but don't move it
- assert!(card.show(), true); // read the card!
-
- card.enter_metro(); // modify the card but don't move it
- card.enter_metro(); // modify the card but don't move it
-
- card.recycle(); // move the card out of the scope
- }
-}
-
-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, such as collections, abstract implementations, and more.
-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.
/// Container for any type `T`.
-struct Container<T> has drop {
- value: T,
-}
-
-/// Function that creates a new `Container` with a generic value `T`.
-public fun new<T>(value: T): Container<T> {
- Container { value }
-}
-
-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 initialed with a concrete type, and generic functions must be called with a concrete type.
#[test]
-fun test_generic() {
- // these three lines are equivalent
- let container: Container<u8> = new(10); // type inference
- let container = new<u8>(10); // create a new `Container` with a `u8` value
- let container = new(10u8);
-
- assert!(container.value == 10, 0x0);
-}
-
-In the test function test_generic
we demonstrate three equivalent ways to create a new Container
with a u8
value. Because numeric types need to be inferred, we specify the type of the number literal.
You can define a type or function with multiple type parameters. The type parameters are then separated by commas.
-/// A pair of values of any type `T` and `U`.
-struct Pair<T, U> {
- first: T,
- second: U,
-}
-
-/// Function that creates a new `Pair` with two generic values `T` and `U`.
-public fun new_pair<T, U>(first: T, second: U): Pair<T, U> {
- Pair { first, second }
-}
-
-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 it should match the order of the type parameters in the type signature.
#[test]
-fun test_generic() {
- // these three lines are equivalent
- let pair: Pair<u8, bool> = new_pair(10, true); // type inference
- let pair = new_pair<u8, bool>(10, true); // create a new `Pair` with a `u8` and `bool` values
- let pair = new_pair(10u8, true);
-
- assert!(pair.first == 10, 0x0);
- assert!(pair.second, 0x0);
-}
-
-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.
#[test]
-fun test_swap_type_params() {
- let pair1: Pair<u8, bool> = new_pair(10u8, true);
- let pair2: Pair<bool, u8> = new_pair(true, 10u8);
-
- // this line will not compile
- // assert!(pair1 == pair2, 0x0);
-}
-
-Types for variables pair1
and pair2
are different, and the comparison will not compile.
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 is the 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.
-/// A user record with name, age, and some generic metadata
-struct User<T> {
- name: String,
- age: u8,
- /// Varies depending on application.
- metadata: T,
-}
-
-In the example above, User
is a generic type with a single type parameter T
, with shared fields name
and age
, and the generic metadata
field which can store any type. No matter what the metadata
is, all of the instances of User
will have the same fields and methods.
/// Updates the name of the user.
-public fun update_name<T>(user: &mut User<T>, name: String) {
- user.name = name;
-}
-
-/// Updates the age of the user.
-public fun update_age<T>(user: &mut User<T>, age: u8) {
- user.age = age;
-}
-
-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.
-/// A generic type with a phantom type parameter.
-struct Coin<phantom T> {
- value: u64
-}
-
-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
.
struct USD {}
-struct EUR {}
-
-#[test]
-fun test_phantom_type() {
- let coin1: Coin<USD> = Coin { value: 10 };
- let coin2: Coin<EUR> = Coin { value: 20 };
-}
-
-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. It will make sure that the USD
and EUR
coins are not mixed up.
Type parameters can be constrained to have certain abilities. This is useful when you need the inner type to allow certain behavior, such as copy or drop. The syntax for constraining a type parameter is T: <ability> + <ability>
.
/// A generic type with a type parameter that has the `drop` ability.
-struct Droppable<T: drop> {
- value: T,
-}
-
-/// A generic struct with a type parameter that has the `copy` and `drop` abilities.
-struct CopyableDroppable<T: copy + drop> {
- value: T, // T must have the `copy` and `drop` abilities
-}
-
-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.
/// Type without any abilities.
-struct NoAbilities {}
-
-#[test]
-fun test_constraints() {
- // Fails - `NoAbilities` does not have the `drop` ability
- let droppable = Droppable<NoAbilities> { value: 10 };
-
- // Fails - `NoAbilities` does not have the `copy` and `drop` abilities
- let copyable_droppable = CopyableDroppable<NoAbilities> { value: 10 };
-}
-
-TODO: add links to
-In programming languages reflection is the ability of a program to examine and modify its own structure and behavior. In Move, there's a limited form of reflection that allows you to inspect the type of a value at runtime. This is useful when you need to store type information in a homogeneous collection, or when you need to check if a type belongs to a package.
-Type reflection is implemented in the Standard Library module std::type_name
. Expressed very roughly, it gives a single function get<T>()
which returns the name of the type T
.
The module is pretty straightforward, and operations allowed on the result are limited to getting a string representation and extracting the module and address of the type.
-module book::type_reflection {
- use std::type_name;
-
- /// A function that returns the name of the type `T` and its module and address.
- public fun i_dont_know_you<T>(): (String, String, String) {
- let type_name: TypeName = type_name::get<T>();
-
- // there's a way to borrow
- let str: &String = type_name.borrow_string();
-
- let module_name: String = type_name.get_module();
- let address_str: String = type_name.get_address();
-
- // and a way to consume the value
- let str = type_name.into_string();
-
- (str, module_name, address_str)
- }
-
- #[test_only]
- struct MyType {}
-
- #[test]
- fun test_type_reflection() {
- let (type_name, module_name, address_str) = i_dont_know_you<MyType>();
-
- //
- assert!(module_name == b"type_reflection".to_string(), 1);
- }
-}
-
-Type reflection is an important part of the language, and it is a crucial part of some of the advanced patterns.
-In previous chapters we've covered the basics of Move and Sui Storage Model. Now it's time to dive deeper into the advanced topics of Sui programmability.
-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.
-Due to the object model and the data organization model of Sui, some operations can be performed in a more efficient and parallelized way. This is called the fast path. Transaction that touches shared state requires consensus because it can be accessed by multiple parties at the same time. However, if the transaction only touches the private state (owned objects), there is no need for consensus. This is the fast path.
-We have a favorite example for this: a coffee machine and a coffee cup. The coffee machine placed in the office is a shared resource - everyone can use it, but there can be only one user at a time. The coffee cup, on the other hand, is a private resource - it belongs to a specific person, and only that person can use it. To make coffee, one needs to use the coffee machine and wait if there's someone else using it. However, once the coffee is made and poured into the cup, the person can take the cup and drink the coffee without waiting for anyone else.
-The same principle applies to Sui. If a transaction only touches the private state (the cup with coffee), it can be executed without consensus. If it touches the shared state (the coffee machine), it requires consensus. This is the fast path.
-Consensus is only required for mutating the shared state. If the object is immutable, it is treated as a "constant" and can be accessed in parallel. Frozen objects can be used to share unchangable data between multiple parties without requiring consensus.
-module book::coffee_machine {
- use sui::object::{Self, UID};
- use sui::tx_context::TxContext;
-
- /// Coffee machine is a shared object, hence requires `key` ability.
- struct CoffeeMachine has key { id: UID, counter: u16 }
-
- /// Cup is an owned object.
- struct Cup has key, store { id: UID, has_coffee: bool }
-
- /// Initialize the module and share the `CoffeeMachine` object.
- fun init(ctx: &mut TxContext) {
- transfer::share_object(CoffeeMachine {
- id: object::new(ctx),
- counter: 0
- });
- }
-
- /// Take a cup out of thin air. This is a fast path operation.
- public fun take_cup(ctx: &mut TxContext): Cup {
- Cup { id: object::new(ctx), has_coffee: false }
- }
-
- /// Make coffee and pour it into the cup. Requires consensus.
- public fun make_coffee(mut machine: &mut CoffeeMachine, mut cup: &mut Cup) {
- machine.counter = machine.counter + 1;
- cup.has_coffee = true;
- }
-
- /// Drink coffee from the cup. This is a fast path operation.
- public fun drink_coffee(mut cup: &mut Cup) {
- cup.has_coffee = false;
- }
-
- /// Put the cup back. This is a fast path operation.
- public fun put_back(cup: Cup) {
- let Cup { id, has_coffee: _ } = cup;
- object::delete(id);
- }
-}
-
-The Clock
object with the reserved address 0x6
is a special case of a shared object which maintains the fast path. While being a shared object, it cannot be passed by a mutable reference in a regular transaction. An attempt to do so will not succeed, and the transaction will be rejected.
Every transaction has the execution context. The context is a set of pre-defined 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.
-The transaction context is available to the program through the TxContext
struct. The struct is defined in the sui::tx_context
module and contains the following fields:
File: sui-framework/tx_context.move
-/// Information about the transaction currently being executed.
-/// This cannot be constructed by a transaction--it is a privileged object created by
-/// the VM and passed in to the entrypoint of the transaction as `&mut TxContext`.
-struct TxContext has drop {
- /// The address of the user that signed the current transaction
- sender: address,
- /// Hash of the current transaction
- tx_hash: vector<u8>,
- /// The current epoch number
- epoch: u64,
- /// Timestamp that the epoch started at
- epoch_timestamp_ms: u64,
- /// Counter recording the number of fresh id's created while executing
- /// this transaction. Always 0 at the start of a transaction
- ids_created: u64
-}
-
-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 Block has access to the context and can pass it into the nested calls.
----
TxContext
has to be the last argument in the function signature.
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.
public fun some_action(ctx: &TxContext) {
- let _me = ctx.sender()
- let _epoch = ctx.epoch();
- let _digest = ctx.digest();
- // ...
-}
-
-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.
Internally, it is represented as the derive_id
function:
File: sui-framework/tx_context.move
-native fun derive_id(tx_hash: vector<u8>, ids_created: u64): address;
-
-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.
File: sui-framework/tx_context.move
-/// 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
-}
-
-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, and in this chapter we will cover the collection types offered by the Sui Framework.
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 makes it useful for storing a collection of unique items, such as a list of unique IDs or addresses.
module book::collections {
- use sui::vec_set::{Self, VecSet};
-
- struct App has drop {
- /// `VecSet` used in the struct definition
- subscribers: VecSet<address>
- }
-
- #[test]
- fun vec_set_playground() {
- let set = vec_set::empty(); // create an empty set
- let set = vec_set::sigleton(1); // create a set with a single item
-
- set.insert(2); // add an item to the set
- set.insert(3);
-
- assert!(set.contains(1), 0); // check if an item is in the set
- assert!(set.size() == 3, 1); // get the number of items in the set
- assert!(!set.is_empty(), 2); // check if the set is empty
-
- set.remove(2); // remove an item from the 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.
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.
module book::collections {
- use std::string::String;
- use sui::vec_map::{Self, VecMap};
-
- struct Metadata has drop {
- name: String
- /// `VecMap` used in the struct definition
- attributes: VecMap<String, String>
- }
-
- #[test]
- fun vec_map_playground() {
- let mut map = 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());
-
- assert!(map.contains(1), 0); // check if a key is in the map
-
- map.remove(&2); // remove a key-value pair from the map
- }
-}
-
-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, the types of attached objects can be any. This allows for a wide range of use cases, such as attaching an accessory to a character, or storing large, non-heterogeneous collections in a single object.
Dynamic fields are attached to an object via a key, which can be any type with the store, copy and drop abilities. The key is used to access the attached object, and can be used to update or remove it. The attached object can be any type, if it has the store ability.
- -Dynamic fields are defined in the sui::dynamic_field
module.
module book::accessories {
-
- struct Character has key {
- id: UID,
- name: String
- }
-
- /// An accessory that can be attached to a character.
- struct Accessory has store {
- type: String,
- name: String,
- }
-}
-
-TODO:
-TODO: explain how custom fields ca
-Dynamic fields are used for:
-#[test]
and #[test_only]
When publishing a package that is intented to be used (an NFT protocol or a library), it is important to showcase how this package can be used. This is where examples come in handy. There's no special functionality for examples in Move, however, there are some conventions that are used to mark examples. First of all, only sources are included into the package bytecode, so any code placed in a different directory will not be included, but will be tested!
-This is why placing examples into a separate examples/
directory is a good idea.
sources/
- protocol.move
- library.move
-tests/
- protocol_test.move
-examples/
- my_example.move
-Move.toml
-
-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.
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.
-Epoch can be read from the transaction context:
-public fun current_epoch(ctx: &TxContext) {
- let epoch = ctx.epoch();
- // ...
-}
-
-It is also possible to get the unix timestamp of the epoch start:
-public fun current_epoch_start(ctx: &TxContext) {
- let epoch_start = ctx.epoch_timestamp_ms();
- // ...
-}
-
-Normally, epochs are used in staking and system operations, however, in custom scenarios they can be used to emulate 24h periods. They are cricital if an application relies on the staking logic or needs to know the current validator set.
-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
.
Clock is a shared object and normally would require consensus to access. However, Clock
is special, and the system won't allow accessing it mutably, so that the only way to access it is immutably. This limitation allows parallel access to the Clock
object and makes it a fast path operation.
File: sui-framework/clock.move
-/// Singleton shared object that exposes time to Move calls. This
-/// object is found at address 0x6, and can only be read (accessed
-/// via an immutable reference) by entry functions.
-///
-/// Entry Functions that attempt to accept `Clock` by mutable
-/// reference or value will fail to verify, and honest validators
-/// will not sign or execute transactions that use `Clock` as an
-/// input parameter, unless it is passed by immutable reference.
-struct Clock has key {
- id: UID,
- /// The clock's timestamp, which is set automatically by a
- /// system transaction every time consensus commits a
- /// schedule, or by `sui::clock::increment_for_testing` during
- /// testing.
- timestamp_ms: u64,
-}
-
-There is only one public function available in the Clock
module - timestamp_ms
. It returns the current time in milliseconds since the Unix Epoch.
/// Clock needs to be passed as an immutable reference.
-public fun current_time(clock: &Clock) {
- let _time = clock.timestamp_ms();
- // ...
-}
-
-TODO: how to use Clock in tests.
-Some of the language features combined together can create patterns that are similar to other programming languages. The simplest example would be "getters and setters" - functions that get and set the value of a field. This pattern is possible, because struct fields are private by default, and can only be accessed through functions.
-However, there are more advanced patterns, such as the abstract class. An abstract class is a class that cannot be instantiated, but can be inherited from. While Move does not have inheritance, it has generic structs, which can be instantiated with different types. This allows us to create a generic struct that can be used as an abstract class. Combined with a set of Witness-gated functions, this allows us to create a generic struct with a generic implementation.
-Some of the methods in this approach will be shared and available to all implementations, while others will be abstract and will need to be implemented by the concrete implementations.
-While this approach imitates the abstract class pattern well, it is not the same as the abstract class in OOP. The main difference is that the abstract class in OOP and its implementors have different type. In Move, the base type stays the same, and the implementors set a generic type parameter. Another notable difference is that due to lack of dynamic dispatch and interfaces, the implemented methods are not available through the base type and can even be missing.
-The Sui Framework uses this pattern to implement the Coin
type and the underlying Balance
. Its variation is also used in the Closed Loop Token implementation, however, the latter is a bit more complex, because it uses the Request pattern to dynamically implement the interface.
This section contains a collection of guides that cover various aspects of programming on Sui. They are intended to provide a deeper understanding of Sui blockchain and Move language, while also aiming at practical challenges and solutions.
-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.
-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.alpha
.
edition = "2024.alpha";
-
-In Move 2024, structs get a visibility modifier. Just like functions, structs can be public, friend, or private.
-// Move 2020
-struct Book {}
-
-// Move 2024
-public struct Book {}
-
-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.
-public fun count(c: &Counter): u64 { /* ... */ }
-
-fun use_counter() {
- // move 2020
- let count = counter::count(&c);
-
- // move 2024
- let count = c.count();
-}
-
-The borrow
and borrow_mut
functions (when defined) can be accessed using the square brackets. Just like the method syntax, the borrowing functions are associated with the type.
fun play_vec() {
- let v = vector[1,2,3,4];
- let first = v[0]; // calls vector::borrow(v, 0)
- v[0] = 5; // calls vector::borrow_mut(v, 0)
-}
-
-In Move 2024, generic methods can be associated with types. The alias can be defined for any type privately to the module, or publicly, if the type is defined in the same module.
-use fun my_custom_function as vector.do_magic;
-
-Macros are introduced in Move 2024. And assert!
is no longer a built-in function - Instead, it's a macro.
// can be called as for!(0, 10, |i| call(i));
-macro fun for($start: u64, $stop: u64, $body: |u64|) {
- let mut i = $start;
- let stop = $stop;
- while (i < stop) {
- $body(i);
- i = i + 1
- }
-}
-
-To talk about best practices for upgradability, we need to first understand what can be upgraded in a package. The base premise of upgradability is that an upgrade should not break public compatibility with the previous version. The parts of the module which can be used in dependent packages should not change their static signature. This applies to modules - a module can not be removed from a package, public structs - they can be used in function signatures and public functions - they can be called from other packages.
-// module can not be removed from the package
-module book::upgradable {
- // dependencies can be changed
- use sui::tx_context::TxContext;
- use sui::object::UID;
-
- // public structs can not be removed and can't be changed
- public struct Book has key {
- id: UID
- }
-
- // public functions can not be removed and their signature can never change
- public fun create_book(ctx: &mut TxContext): Book {
- create_book_internal(ctx)
- }
-
- // friend-only functions can be removed and changed
- public(friend) fun create_book_friend(ctx: &mut TxContext): Book {
- create_book_internal(ctx)
- }
-
- // entry functions can be removed and changed as long they're not public
- entry fun create_book_entry(ctx: &mut TxContext): Book {
- create_book_internal(ctx)
- }
-
- // private functions can be removed and changed
- fun create_book_internal(ctx: &mut TxContext): Book {
- abort 0
- }
-}
-
-TODO: Add a section about entry and friend functions
-To discard previous versions of the package, the objects can be versioned. As long as the object contains a version field, and the code which uses the object expects and asserts a specific version, the code can be force-migrated to the new version. Normally, after an upgrade, admin functions can be used to update the version of the shared state, so that the new version of code can be used, and the old version aborts with a version mismatch.
-module book::versioned_state {
-
- const EVersionMismatch: u64 = 0;
-
- const VERSION: u8 = 1;
-
- /// The shared state (can be owned too)
- struct SharedState has key {
- id: UID,
- version: u8,
- /* ... */
- }
-
- public fun mutate(state: &mut SharedState) {
- assert!(state.version == VERSION, EVersionMismatch);
- // ...
- }
-}
-
-There's a common pattern in Sui which allows changing the stored configuration of an object while retaining the same object signature. This is done by keeping the base object simple and versioned and adding an actual configuration object as a dynamic field. Using this anchor pattern, the configuration can be changed with package upgrades while keeping the same base object signature.
-module book::versioned_config {
-
- /// The base object
- struct Config has key {
- id: UID,
- version: u16
- }
-
- /// The actual configuration
- struct ConfigV1 has store {
- data: Bag,
- metadata: VecMap<String, String>
- }
-
- // ...
-}
-
-TODO: add two patterns for modular architecture: object capability (SuiFrens) and witness registry (SuiNS)
-To guarantee the safety and security of the network, Sui has certain limits and restrictions. These limits are in place to prevent abuse and to ensure that the network remains stable and efficient. This guide provides an overview of these limits and restrictions, and how to build your application to work within them.
-The limits are defined in the protocol configuration and are enforced by the network. If any of the limits are exceeded, the transaction will either be rejected or aborted. The limits, being a part of the protocol, can only be changed through a network upgrade.
-The size of a transaction is limited to 128KB. This includes the size of the transaction payload, the size of the transaction signature, and the size of the transaction metadata. If a transaction exceeds this limit, it will be rejected by the network.
-The size of an object is limited to 256KB. This includes the size of the object data. If an object exceeds this limit, it will be rejected by the network. While a single object cannot bypass this limit, for more extensive storage options, one could use a combination of a base object with other attached to it using dynamic fields (eg Bag).
-The size of a single pure argument is limited to 16KB. A transaction argument bigger than this limit will result in execution failure. So in order to create a vector of more than ~500 addresses (given that a single address is 32 bytes), it needs to be joined dynamically either in Transaction Block or in a Move function. Standard functions like vector::append()
can join two vectors of ~16KB resulting in a ~32KB of data as a single value.
The maximum number of objects that can be created in a single transaction is 2048. If a transaction attempts to create more than 2048 objects, it will be rejected by the network. This also affects dynamic fields, as both the key and the value are objects. So the maximum number of dynamic fields that can be created in a single transaction is 1024.
-The maximum number of events that can be emitted in a single transaction is 1024. If a transaction attempts to emit more than 1024 events, it will be aborted.
-Whenever execution encounters an abort, transaction fails and abort code is returned to the caller. Move VM returns the module name that aborted the transaction and the abort code. This behavior is not fully transparent to the caller of the transaction, especially when a single function contains multiple calls to the same function which may abort. In this case, the caller will not know which call aborted the transaction, and it will be hard to debug the issue or provide meaningful error message to the user.
-module book::module_a {
- use book::module_b;
-
- public fun do_something() {
- let field_1 = module_b::get_field(1); // may abort with 0
- /* ... a lot of logic ... */
- let field_2 = module_b::get_field(2); // may abort with 0
- /* ... some more logic ... */
- let field_3 = module_b::get_field(3); // may abort with 0
- }
-}
-
-The example above illustrates the case when a single function contains multiple calls which may abort. If the caller of the do_something
function receives an abort code 0
, it will be hard to understand which call to module_b::get_field
aborted the transaction. To address this problem, there are common patterns that can be used to improve error handling.
It is considered a good practice to provide a safe "check" function that returns a boolean value indicating whether an operation can be performed safely. If the module_b
provides a function has_field
that returns a boolean value indicating whether a field exists, the do_something
function can be rewritten as follows:
module book::module_a {
- use book::module_b;
-
- const ENoField: u64 = 0;
-
- public fun do_something() {
- assert!(module_b::has_field(1), ENoField);
- let field_1 = module_b::get_field(1);
- /* ... */
- assert!(module_b::has_field(1), ENoField);
- let field_2 = module_b::get_field(2);
- /* ... */
- assert!(module_b::has_field(1), ENoField);
- let field_3 = module_b::get_field(3);
- }
-}
-
-By adding custom checks before each call to module_b::get_field
, the developer of the module_a
takes control over the error handling. And it allows implementing the second rule.
The second trick, once the abort codes are handled by the caller module, is to use different abort codes for different scenarios. This way, the caller module can provide a meaningful error message to the user. The module_a
can be rewritten as follows:
module book::module_a {
- use book::module_b;
-
- const ENoFieldA: u64 = 0;
- const ENoFieldB: u64 = 1;
- const ENoFieldC: u64 = 2;
-
- public fun do_something() {
- assert!(module_b::has_field(1), ENoFieldA);
- let field_1 = module_b::get_field(1);
- /* ... */
- assert!(module_b::has_field(1), ENoFieldB);
- let field_2 = module_b::get_field(2);
- /* ... */
- assert!(module_b::has_field(1), ENoFieldC);
- let field_3 = module_b::get_field(3);
- }
-}
-
-Now, the caller module can provide a meaningful error message to the user. If the caller receives an abort code 0
, it can be translated to "Field 1 does not exist". If the caller receives an abort code 1
, it can be translated to "Field 2 does not exist". And so on.
A developer is often tempted to add a public function that would assert all the conditions and abort the execution. However, it is a better practice to create a function that returns a boolean value instead. This way, the caller module can handle the error and provide a meaningful error message to the user.
-module book::some_app_assert {
-
- const ENotAuthorized: u64 = 0;
-
- public fun do_a() {
- assert_is_authorized();
- // ...
- }
-
- public fun do_b() {
- assert_is_authorized();
- // ...
- }
-
- /// Don't do this
- public fun assert_is_authorized() {
- assert!(/* some condition */ true, ENotAuthorized);
- }
-}
-
-This module can be rewritten as follows:
-module book::some_app {
- const ENotAuthorized: u64 = 0;
-
- public fun do_a() {
- assert!(is_authorized(), ENotAuthorized);
- // ...
- }
-
- public fun do_b() {
- assert!(is_authorized(), ENotAuthorized);
- // ...
- }
-
- public fun is_authorized(): bool {
- /* some condition */ true
- }
-
- // a private function can still be used to avoid code duplication for a case
- // when the same condition with the same abort code is used in multiple places
- fun assert_is_authorized() {
- assert!(is_authorized(), ENotAuthorized);
- }
-}
-
-Utilizing these three rules will make the error handling more transparent to the caller of the transaction, and it will allow other developers to use custom abort codes in their modules.
-public_*
transfer functions.id: UID
.public_*
transfer functions to accept them as arguments. It also enables the object to be stored as a dynamic field.copy
ability conflicts with the key
ability, and can not be used together with it.drop
ability cannot be used together with the key
ability, as objects are not allowed to be ignored.use 0x1::debug;\nuse 0x1::string;\n
\n ## Function `hello_world`\nAs the name says: returns a string \"Hello, World!\".\nfun hello_world(): string::String\n
\nfun hello_world(): String { let result = string::utf8(b\"Hello, World!\"); debug::print(&result); result\n}\n
\nuse 0x1::debug;\nuse 0x1::string;\n
\n ## Function `hello_world`\nAs the name says: returns a string \"Hello, World!\".\nfun hello_world(): string::String\n
\nfun hello_world(): String { let result = string::utf8(b\"Hello, World!\"); debug::print(&result); result\n}\n
\n