# The Aptos Book > A comprehensive guide to building on the Aptos blockchain with the Move programming language. Covers core language concepts, smart contract development, design patterns, standard libraries, and AI-assisted development with LLMs. # The Aptos Blockchain _by [Greg Nazario](https://github.com/gregnazario)_ --- Welcome to the official guide for developers building on the Aptos blockchain. The Aptos blockchain is a high-performance, scalable, and secure platform designed to support a wide range of decentralized applications (dApps) and services. It is built on the Move programming language, a safe and flexible language originally developed for the Diem project. ## Key Features - **Move Language**: A resource-oriented programming language with strong safety guarantees enforced at compile time. - **High Throughput**: Parallel execution via BlockSTM enables high transaction throughput. - **Low Latency**: Sub-second transaction finality. - **Upgradeable Contracts**: Compatible upgrade policies allow bug fixes and feature additions. - **On-Chain Randomness**: Built-in randomness for games, lotteries, and fair selection. - **AI-Ready**: Documentation published in llms.txt format, with a growing ecosystem of AI agent frameworks. ## Who This Book Is For This book is for developers who want to build on Aptos. It covers everything from setting up your development environment and learning the fundamentals of Move to mastering advanced topics and design patterns. Whether you are writing code by hand or using AI-assisted development tools, this book provides the comprehensive reference you need. Ready to get started? - **New to Aptos?** Begin with the [Introduction](intro.md) to learn more about the Aptos blockchain. - **Ready to code?** Jump right into [Getting Started](getting_started.md). - **Using AI tools?** See [Building on Aptos with LLMs](building_with_llms/intro.md) for AI-assisted development. # Introduction The Aptos Book extends the Move book, offering a single resource for design patterns, usage examples, and other information needed to build on Aptos. It provides context for building on Aptos and can be used entirely offline. ## Get Started You can jump to any section for a specific topic, but the material is arranged so that readers first learn Aptos fundamentals and then progress to writing Move contracts. We recommend working through the chapters in order. Alternatively, use the search bar to find specific topics or keywords. ## Book Structure The book is organized into several parts: 1. **Getting Started** - Installation, first contract, CLI basics 2. **Core Language** - Variables, types, functions, ownership, structs, enums, modules 3. **Standard Libraries** - Collections, error handling, generics, testing 4. **Blockchain Concepts** - Events, parallelization, data models, storage, gas 5. **Design Patterns** - Best practices and common patterns 6. **Building with LLMs** - Using AI tools for Aptos development 7. **Reference** - Standard libraries, glossary, appendix ## LLM-Optimized This book is optimized for consumption by Large Language Models (LLMs). It is published in the [llms.txt](https://llmstxt.org/) format alongside the standard HTML, making it directly usable as context for AI-assisted development. See the [Building on Aptos with LLMs](building_with_llms/intro.md) chapter for details. ## Contribution Contributions are welcome. Please open a GitHub issue or pull request and: 1. Identify the section being updated 2. Provide a concise description of the change 3. Note any gaps or missing areas # Getting Started Let's get you started with the Aptos Blockchain! We'll discuss: 1. [Installing the Aptos CLI](getting_started/installation.md) on macOS, Linux, and Windows. 2. [Hello Blockchain!](getting_started/hello_blockchain.md) - a simple contract that writes and reads data on-chain. 3. [Hello Aptos CLI!](getting_started/hello_aptos_cli.md) - a simple example of using the Aptos CLI to interact with the blockchain. # Installation The first step is to install the Aptos CLI. The CLI is a command-line interface(CLI) tool that allows you to interact with the Aptos blockchain, including compiling, testing, and deploying Move modules, managing accounts, and reading and writing data to the blockchain. > Note: If you are using an unsupported platform or configuration, or you prefer to build a specific version from > source, you can follow > the [Building from Source](https://aptos.dev/en/build/cli/install-cli/install-cli-specific-version) guide on the Aptos > developer docs. The following instructions will tell you how to install the latest version of the Aptos CLI. It is highly recommended to always use the latest version of the CLI, as it contains the latest features and bug fixes. ## Installing the Aptos CLI with Homebrew on macOS For macOS, it's recommended to use Homebrew. To install the Aptos CLI on macOS, if you have Homebrew installed, you can use the following command: ```bash brew install aptos ``` ## Installing the Aptos CLI on macOS and Linux To install the Aptos CLI on macOS and Linux, you can use the following command: ```bash curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh ``` ## Installing the Aptos CLI on Windows with Winget For Windows users, you can use the Windows Package Manager (Winget) to install the Aptos CLI. Open a command prompt or PowerShell and run the following command: ```powershell winget install aptos.aptos-cli ``` ## Installing the Aptos CLI on Windows To install the Aptos CLI on Windows, you can use the following command in PowerShell: ```powershell iwr "https://aptos.dev/scripts/install_cli.ps1" -useb | iex ``` ## Troubleshooting To check whether you have the Aptos CLI installed correctly, open a shell and enter this line: ```bash aptos --version ``` You should see output similar to the following, with your CLI version. ```bash aptos 7.6.0 ``` If you see this information, you have installed the Aptos CLI successfully! If you don’t see this information, check that the Aptos CLI is in your `%PATH%` system variable as follows. In Windows CMD, use: ```console echo %PATH% ``` In PowerShell, use: ```powershell echo $env:Path ``` In Linux and macOS, use: ```shell echo $PATH ``` ## Updating the Aptos CLI To update the Aptos CLI to the latest version, you can use the same command you used to install it. For example, if you installed the CLI using Homebrew, you can run: ```bash brew upgrade aptos ``` If you installed the CLI using the curl command, you can run the aptos update command: ```bash aptos update aptos ``` Alternatively, you can run the installation command again: ```bash curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh ``` ### Local Documentation The Aptos CLI also provides local documentation that you can access by running the following command: ```bash aptos --help ``` This command will display a list of available commands and options for the Aptos CLI, along with a brief description of each command. You can also access the documentation for a specific command by running: ```bash aptos --help ``` ### Text Editors and Integrated Development Environments This book makes no assumptions about what tools you use to author Move code. Just about any text editor will get the job done! However, many text editors and integrated development environments (IDEs) have built-in support for Move: - **VS Code**: Search for "Aptos Move Analyzer" in the VS Code Extensions Marketplace for syntax highlighting, go-to-definition, auto-completion, and error checking. - **IntelliJ IDEA**: Search for "Move Language" in the JetBrains Plugin Marketplace for IDE support. - **Vim/Neovim**: Community-maintained Move syntax highlighting plugins are available. - **Emacs**: Community-maintained `move-mode` packages are available. For the best development experience, VS Code with the Aptos Move Analyzer extension is recommended. # Hello Blockchain! Let's start with the simplest example, which shows you how to: 1. Build and publish a contract 2. Write data on-chain 3. Read data on-chain ## Deploying your first contract First, we'll create an account on devnet to deploy your contract, we'll name it the profile `tutorial`: ```sh aptos init --profile tutorial --network devnet --assume-yes ``` Next, open a new test folder, then call the command `aptos move init --template` to initialize a test. ```sh mkdir hello_blockchain cd hello_blockchain aptos move init --template hello-blockchain ``` This will create a folder structure like so: ``` hello_blockchain ├── Move.toml ├── sources ├── scripts └── tests ``` Then, the run next command to simply build and publish the contract: ```sh aptos move publish --profile tutorial --named-addresses hello_blockchain=tutorial ``` > Note that `named-addresses` sets the named address `hello_blockchain` in the `Move.toml` file to > the `tutorial` profile created in the CLI. Great! You've deployed your first contract! You can view it on the [Aptos Explorer](https://explorer.aptoslabs.com) by searching for your account address and selecting the appropriate network. ## Running the app Now that you've deployed your first contract, let's interact with it. Let's create your first message on-chain by calling an [entry function](../appendix/glossary.md#entry_function): ```sh aptos move run --profile tutorial --function-id tutorial::message::set_message --args 'string:Hello world!' ``` > Note that this runs the entry function `hello_blockchain::set_message` on the contract you just deployed. > It then provides the first non-signer argument as a string. Once this is run successfully, you can view the on-chain state, with a [view function](../appendix/glossary.md#view_function): ```sh aptos move view --profile tutorial --function-id tutorial::message::get_message --args address:tutorial ``` This will return the value `Hello world!` you just wrote on-chain. Congrats you've just written and read your first data on-chain! You can continue to run those two functions to write and read from the on-chain state respectively. For more information about this, let's dive into the full contract and a breakdown next. ## Full Contract Below is the full contract for hello_blockchain. ```move /// Writes a message to a single storage slot, all changes overwrite the previous. /// Changes are recorded in `MessageChange` events. module hello_blockchain::message { use std::error; use std::signer; use std::string::{Self, String}; use aptos_framework::event; #[test_only] use std::debug::print; /// A resource for a single storage slot, holding a message. struct MessageHolder has key { message: String, } #[event] /// Event representing a change in a message, records the old and new messages, and who wrote it. struct MessageChange has drop, store { account: address, from_message: String, to_message: String, } /// The address does not contain a MessageHolder const E_NO_MESSAGE: u64 = 1; #[view] /// Reads the message from storage slot public fun get_message(addr: address): String acquires MessageHolder { assert!(exists(addr), error::not_found(E_NO_MESSAGE)); MessageHolder[addr].message } /// Sets the message to the storage slot public entry fun set_message(account: signer, message: String) acquires MessageHolder { let account_addr = signer::address_of(&account); if (!exists(account_addr)) { move_to(&account, MessageHolder { message, }) } else { let message_holder = &mut MessageHolder[account_addr]; let from_message = message_holder.message; event::emit(MessageChange { account: account_addr, from_message, to_message: message, }); message_holder.message = message; } } #[test(account = @0x1)] fun sender_can_set_message(account: signer) acquires MessageHolder { let msg: String = string::utf8(b"Running test sender_can_set_message..."); print(&msg); let addr = signer::address_of(&account); aptos_framework::account::create_account_for_test(addr); set_message(account, string::utf8(b"Hello, Blockchain")); assert!(get_message(addr) == string::utf8(b"Hello, Blockchain")); } } ``` ## Breakdown ### Module The first 3 lines here, define documentation and the name of the module. Here you can see that the `///` represents a doc comment. Documentation can be generated from these comments, where `///` describes what's directly below it. `module hello_blockchain::message` represents the name of the address, and the name of the module. `hello_blockchain` is what we call a named address. This named address can be passed in at compile time, and determines where the contract is being deployed. `message` is the name of the module. By convention, these are lowercased. ```move /// Writes a message to a single storage slot, all changes overwrite the previous. /// Changes are recorded in `MessageChange` events. module hello_blockchain::message {} ``` ### Imports Next, we import some libraries that we will use in our contract. The `use` keyword is used to import modules, and are of the form `use ::`. There are three standard library addresses that we can use from Aptos: - `std` - The standard library, which contains basic functionality like strings, vectors, and events. - `aptos_std` - The Aptos standard library, which contains functionality specific to the Aptos blockchain, like string manipulation. - `aptos_framework` - The Aptos framework, which contains functionality specific to the Aptos framework, like events, objects, accounts and more. > Note that the `#[test_only]` attribute is used to indicate that the module is only for testing purposes, and will not > be compiled into the non-test bytecode. This is useful for debugging and testing purposes. ```move use std::error; use std::signer; use std::string::{Self, String}; use aptos_framework::event; #[test_only] use std::debug::print; ``` ### Structs Next, we define a struct that will hold our message. Structs are structured data that is a collection of other types. These types can be primitives (e.g. `u8`, `bool`, `address`) or other structs. In this case, the struct is called `MessageHolder`, and it has a single field to hold the message. ```move /// A resource for a single storage slot, holding a message. struct MessageHolder has key { message: String, } ``` ### Events Next, we define an event that will be emitted when the message is changed. Events are used to record changes to the blockchain in an easily indexable way. They are similar to events in other programming languages, and can be used to log changes to the blockchain. In this case, the event is called `MessageChange`, and it has three fields: `account`, which is the address of the account that changed the message, `from_message`, which is the old message, and `to_message`, which is the new message. The `has drop, store` attributes are required for events and indicate that the event can be dropped from scope and stored in the blockchain. Events are defined with the `#[event]` annotation, and is required to emit as an event. ```move #[event] /// Event representing a change in a message, records the old and new messages, and who wrote it. struct MessageChange has drop, store { account: address, from_message: String, to_message: String, } ``` > Note that the doc comments `///` must be directly above the struct, and not before the annotations. > This differs from Rust, which allows the annotation in either location. ### Constants and Error messages Next is specifically a constant. Constants in Move must have a type definition, and can only be primitive types. They can also be documented with a doc comment. In this case, it will be used as an `error`, which is can define a user defined message when aborting. By convention, these start with a `E_` or `E`, and the doc comment will define the abort error message. We'll show how these are used later. ```move /// The address does not contain a MessageHolder const E_NO_MESSAGE: u64 = 1; ``` ### View Functions and Reading State View functions are how external callers can easily read state from the blockchain. As you can see here the function `get_message` allows for outputting the message stored on chain. The `#[view]` annotation marks a function as callable from outside of the Move VM. Without this, the function won't be able to be called by `aptos move view` or the SDKs. You can see here we define a function as `public`, which means it can be called by other Move functions within the Move VM. You can see it takes a single `address` argument, which determines the location that the `hello_blockchain` message is stored. Additionally, you can see `String` which is the return value of the function. Lastly, in the function signature `acquires MessageHolder` shows that the function accesses global state of the `MessageHolder`. ```move #[view] /// Reads the message from storage slot public fun get_message(addr: address): String acquires MessageHolder { // ... } ``` The function body is fairly simple, only 2 lines. First, there's an `assert!` statement, which defines an error condition. The error condition shows that if the `MessageHolder` is not at the `address`, it will throw the error specified earlier in the constant. This is then followed by accessing the message directly from that on-chain state, and returning a copy to the user. ```move #[view] /// Reads the message from storage slot public fun get_message(addr: address): String acquires MessageHolder { assert!(exists(addr), error::not_found(E_NO_MESSAGE)); MessageHolder[addr].message } ``` > Note that return values of view functions must have the abilities `copy` and `drop` ### Entry Functions and Writing State Entry functions are the way that users can call a function as a standalone transaction. The `set_message` function, is denoted in the function signature as `entry` which means it can be called as an entry function payload, a standalone transaction. Similar to the `view` function, you will see here, that it has two input arguments. The first argujment is the `signer`, which can only be provided by the transaction signature or similar authorization mechanism. The `signer` authorizes the function to *move global state to* the address. The second argument is a `String` and is the message to be written to the blockchain. ```move /// Sets the message to the storage slot public entry fun set_message(account: signer, message: String) acquires MessageHolder { ``` > Note that an entry function does not always need to be public, it can be `private`, which > is without the `public` in front. This can be useful to ensure that the function cannot > be called from within another Move function. > Note also that `signer` can also be `&signer`, and as the first argument to the entry function. > Requiring more than one `signer` as arguments, means that it needs to be a multi-agent transaction, > or simply signed by more than one party. Now for the function body, we have a few parts. We can see that we get the `address` of the account calling the transaction, the `signer`. Once we get this address, we can check if the `MessageHolder` resource (aka struct) is stored in global storage for this account. If it hasn't been initialized (there is no resource), we `move_to` the account, a new `MessageHolder` resource. This adds the resource to the account to read later, with the initial message. ```move /// Sets the message to the storage slot public entry fun set_message(account: signer, message: String) acquires MessageHolder { let account_addr = signer::address_of(&account); if (!exists(account_addr)) { move_to(&account, MessageHolder { message, }) ``` Finally, if the resource already exists, we handle it smoothly by just updating the state of the resource to use the new message. When we do this, we also emit an event for easy indexing of changes of the message. ```move /// Sets the message to the storage slot public entry fun set_message(account: signer, message: String) acquires MessageHolder { let account_addr = signer::address_of(&account); if (!exists(account_addr)) { move_to(&account, MessageHolder { message, }) } else { let message_holder = &mut MessageHolder[account_addr]; let from_message = message_holder.message; event::emit(MessageChange { account: account_addr, from_message, to_message: message, }); message_holder.message = message; } } ``` > Note that the signer is not required to change existing state on a resource. > This is because only the module that owns the struct can modify the state of > the resource. However, the `signer` is always required to authorize storing > state into an address. ### Unit Tests Move has native built in unit testing. To add a test, simply add the `#[test]` annotation to a function. As you can see in the function signature, you can define predefined addresses to create signers for the test. In this case, the `account` name is shared, and it is creating a signer for the `0x1` address. The test can then simply call any of the other functions in the module, and assert behaviors afterwards. ``` #[test(account = @0x1)] fun sender_can_set_message(account: signer) acquires MessageHolder { let msg: String = string::utf8(b"Running test sender_can_set_message..."); print(&msg); let addr = signer::address_of(&account); aptos_framework::account::create_account_for_test(addr); set_message(account, string::utf8(b"Hello, Blockchain")); assert!(get_message(addr) == string::utf8(b"Hello, Blockchain")); } ``` > Note that tests can be placed into separate test files in the `tests` folder; However > private functions cannot be called outside of the same file. If you want to make test > only functions that can be used in other modules, simply add the `#[test_only]` annotation. # Hello Aptos CLI! ## Introduction The Aptos CLI is a powerful command-line interface that allows you to interact with the Aptos blockchain. It provides a wide range of commands to manage accounts, deploy smart contracts, and perform various operations on the blockchain. You will become familiar with it the longer you develop on the Aptos blockchain, as it is a key tool for compiling, testing, deploying, and interacting with contracts on the Aptos blockchain. ## Installation If you haven't already installed the Aptos CLI, please refer to the [Installation guide](installation.md) for instructions on how to install it on your system. The CLI is available for macOS, Linux, and Windows for both x86 and ARM systems. ## Hello Aptos CLI In this section, we will explore some basic commands of the Aptos CLI to interact with the blockchain. We will cover submitting transactions and querying accounts. ### Initializing the CLI First, determine whether you want a `workspace` style config or a `global` style config. By default, it uses `workspace` which means credentials and configuration will be based in your current directory. When you change directories, you will not use those credentials, and then will need to create different ones for each directory. If you want a `global` config that is shared among all directories, run the following command: ```sh aptos config set-global-config --config-type global ``` Once we've set that, we will need to create the `default` profile for the Aptos CLI. This profile will store your account keys and create an account for you to use with the CLI. To initialize the CLI, run the following command in your terminal: ```sh aptos init ``` This command will prompt you to create a new account if you have not already created one. It will look something like this: ```sh $ aptos init Configuring for profile default Choose network from [devnet, testnet, mainnet, local, custom | defaults to devnet] ``` You can choose the network you want to connect to. For this example, we will use the `devnet` network, which is a short lived test network that is reset once a week. It is a great place to start experimenting with the Aptos CLI and easy to ignore your early mistakes. You can just press `Enter` to select the default `devnet` network. Next, it will prompt you to enter your private key. If you do not have a private key, you can generate a new one by just pressing `Enter`. If you have a private key, you can enter it as a hex literal (starting with `0x`). The prompt will look like this: ```sh Enter your private key as a hex literal (0x...) [Current: Redacted | No input: Generate new key (or keep one if present)] ``` The CLI will then automatically fund and create an account for you on the `devnet` network. You will see output similar to the following: ```sh Account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a is not funded, funding it with 100000000 Octas Account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a funded successfully --- Aptos CLI is now set up for account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a as profile default! --- { "Result": "Success" } ``` This means that your account is now set up and ready to use with the Aptos CLI! Similarly, you can create additional profiles by running the `aptos init` command with the `--profile `. Then any of those accounts can be used with the CLI by specifying the `--profile ` flag in your commands. Additionally, if you want to change the network for your profile, you can use the `aptos init` command again to change the network: ```sh aptos init --profile default --network ``` ## Submitting a Transaction Now that we have initialized the CLI and created an account, we can submit a transaction to the blockchain. For this example, we will send some Octas (subunit of the native APT token of the Aptos blockchain) from our account to another account. To send Octas, we will use the `aptos account transfer` command. This command allows you to send funds to an account on the blockchain. The command syntax is as follows: ```sh aptos account transfer --account --amount ``` > Note that the `--account` flag is used to specify the recipient's address, and the `--amount` flag is used to specify > the amount to send in Octas. For example, to send 10 Octas to the account > `0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a`, you would run: ```sh aptos account transfer --account 0x78077fe8db589e1a3407170cf8af3bd60a8c95737918c15dd6f49dcbecc7900a --amount 10 ``` # Programming a Guessing Game We're going to dive deep into Move with a simple example, a single player guessing game. The rules are simple: 1. Pass in a number to guess 2. The same account will guess until they get the same number 3. When it's guessed, the game is over and can be reset. 4. All the guesses will be kept track of, and each number can only be guessed once. > Note that this is pretty trivial from a guessing game, as the number is on-chain and public information, but it can be > good for learning. # Guessing Game Contract ## Creating a module First, we need to create a *module*. A *module*, in Move is a collection of related code (structs, functions, constants) that is published at a specific address. Modules provide encapsulation and organization for your smart contract logic. ```move module module_addr::guessing_game { } ``` --- ## Contract Explanation and Breakdown ### Creating a Module A *module* in Move is a collection of related code (structs, functions, constants) that is published at a specific address. Modules provide encapsulation and organization for your smart contract logic. ```move module module_addr::guessing_game { // Module contents go here } ``` Here, `module_addr` is a named address (set at compile/deploy time), and `guessing_game` is the module's name. --- ### Error Codes The contract defines error constants to provide clear, descriptive error messages for different failure conditions: ```move const E_NO_GAME: u64 = 1; // No game initialized for this account const E_GAME_OVER: u64 = 2; // Game is over, must reset const E_GAME_NOT_OVER: u64 = 3; // Game is not over, must finish guessing const E_GAME_INITIALIZED: u64 = 4;// Game already initialized const E_ALREADY_GUESSED: u64 = 5; // Number already guessed ``` --- ### Game State The `Game` struct stores all state for a single game: ```move struct Game has key { number: u8, // The number to guess guesses: vector, // All previous guesses game_over: bool, // Whether the game is over } ``` - The `key` ability allows this struct to be stored in global storage under an account address. --- ### Game Functions #### Creating a Game ```move entry fun create_game(caller: &signer, number: u8) { let caller_addr = signer::address_of(caller); assert!(!exists(caller_addr), E_GAME_INITIALIZED); move_to(caller, Game { number, guesses: vector[], game_over: false, }) } ``` - Only one game per account is allowed at a time. - Stores the new game in the caller's account. #### Making a Guess ```move public entry fun guess(caller: &signer, number: u8) acquires Game { let caller_addr = signer::address_of(caller); assert!(exists(caller_addr), E_NO_GAME); let game = &mut Game[caller_addr]; assert!(!game.game_over, E_GAME_OVER); assert!(!game.guesses.contains(&number), E_ALREADY_GUESSED); game.guesses.push_back(number); if (number == game.number) { game.game_over = true; } } ``` - Checks that the game exists and is not over. - Prevents duplicate guesses. - Marks the game as over if the guess is correct. #### Resetting the Game ```move entry fun reset_game(caller: &signer, new_num: u8) acquires Game { let caller_addr = signer::address_of(caller); assert!(exists(caller_addr), E_NO_GAME); let game = &mut Game[caller_addr]; assert!(game.game_over, E_GAME_NOT_OVER); game.game_over = false; game.guesses = vector[]; game.number = new_num; } ``` - Only allowed if the game is over. - Resets guesses and sets a new number. #### Removing the Game State ```move entry fun remove_state(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); assert!(exists(caller_addr), E_NO_GAME); let Game { .. } = move_from(caller_addr); } ``` - Deletes the game from storage for the caller. --- ### View Functions These functions allow anyone to query the game state without modifying it: ```move #[view] public fun is_game_over(addr: address): bool acquires Game { assert!(exists(addr), E_NO_GAME); Game[addr].game_over } #[view] public fun number(addr: address): u8 acquires Game { assert!(exists(addr), E_NO_GAME); assert!(is_game_over(addr), E_GAME_NOT_OVER); Game[addr].number } #[view] public fun guesses(addr: address): vector acquires Game { assert!(exists(addr), E_NO_GAME); Game[addr].guesses } ``` - `is_game_over`: Returns whether the game is over. - `number`: Returns the answer, but only if the game is over (prevents cheating). - `guesses`: Returns all guesses made so far. --- ### Key Concepts Demonstrated - **State Management**: Uses a struct with the `key` ability for on-chain state. - **Access Control**: Only the account owner can modify their game. - **Error Handling**: Uses clear error codes and assertions. - **Resource Safety**: Ensures proper creation, update, and deletion of game state. - **View Functions**: Provides read-only access to game state. - **Game Logic**: Implements a simple, fair guessing game with validation. This contract is a practical example of how to build a stateful, interactive Move smart contract on Aptos, and demonstrates best practices for error handling, state management, and user interaction. Here is the full contract at the end: ```move /// Guessing game /// /// - Call `create_game` to start the game /// - Guess with `guess` /// - When the number is guessed correctly, the game is over /// - Can be reset with `reset_game` /// - Can be cleaned up with `remove_state` /// module module_addr::guessing_game { use std::signer; /// No game initialized for this account, please call create_game first const E_NO_GAME: u64 = 1; /// Game is over, please call reset_game const E_GAME_OVER: u64 = 2; /// Game is not over, please guess until the game is over const E_GAME_NOT_OVER: u64 = 3; /// Game is already initialized, please guess until the game is over const E_GAME_INITIALIZED: u64 = 4; /// Number is already guessed, please guess a different number const E_ALREADY_GUESSED: u64 = 5; /// State of the game struct Game has key { number: u8, guesses: vector, game_over: bool, } /// Creates a game entry fun create_game(caller: &signer, number: u8) { let caller_addr = signer::address_of(caller); assert!(!exists(caller_addr), E_GAME_INITIALIZED); move_to(caller, Game { number, guesses: vector[], game_over: false, }) } /// Guesses on the game public entry fun guess(caller: &signer, number: u8) acquires Game { let caller_addr = signer::address_of(caller); // Check that the game exists assert!(exists(caller_addr), E_NO_GAME); let game = &mut Game[caller_addr]; // Check that the game isn't over assert!(!game.game_over, E_GAME_OVER); // Check that I haven't guessed already assert!(!game.guesses.contains(&number), E_ALREADY_GUESSED); game.guesses.push_back(number); // Check win condition if (number == game.number) { game.game_over = true; } } /// Resets the game when it's done entry fun reset_game(caller: &signer, new_num: u8) acquires Game { let caller_addr = signer::address_of(caller); // Check that the game exists assert!(exists(caller_addr), E_NO_GAME); let game = &mut Game[caller_addr]; // Check that the game is over assert!(game.game_over, E_GAME_NOT_OVER); game.game_over = false; game.guesses = vector[]; game.number = new_num; } /// Deletes the game state entry fun remove_state(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); assert!(exists(caller_addr), E_NO_GAME); let Game { .. } = move_from(caller_addr); } #[view] public fun is_game_over(addr: address): bool acquires Game { assert!(exists(addr), E_NO_GAME); Game[addr].game_over } #[view] public fun number(addr: address): u8 acquires Game { assert!(exists(addr), E_NO_GAME); assert!(is_game_over(addr), E_GAME_NOT_OVER); Game[addr].number } #[view] public fun guesses(addr: address): vector acquires Game { assert!(exists(addr), E_NO_GAME); Game[addr].guesses } } ``` # Testing the Contract Move provides a unit testing framework that's really easy to test different functionality. The tests can simply be written in Move alongside the code in the same module. All unit tests can be run from the Aptos CLI by using the command `aptos move test`. For this example, we will specifically use the `--dev` flag to fill in the address, with the full command being `aptos move test --dev` ## Testing successful functionality To define a test, you simply need to add `#[test]` above a function. The function will only be used for testing, so it is best to keep them as private functions (no `public` or `entry`). ```move module module_addr::guessing_game { // ... contract definition ... #[test] fun test_flow(caller: &signer) acquires Game {} } ``` Within the `#[test]` annotation, you can add any number of `signer`s to be created for the test. In this example, we'll only create one `signer` named `caller`. This `signer` will be used to call the functions in the contract and store state at the associated address `0x1337`. > Note that you can use either named addresses e.g. `@module_addr` or an address literal `@0x1337`. `@` denotes that > the literal is an address. ```move module module_addr::guessing_game { // ... contract definition ... #[test(caller = @0x1337)] fun test_flow(caller: &signer) acquires Game {} } ``` Within the test, you can call any move functions that exist, and use assertions similar to in the regular code. > Note that you can skip the error codes for assertions in tests. Here, we'll test the basic flow of creating a game and > guessing correctly on the first try. You can see how the game is considered over, and the one guess is recorded. ```move module module_addr::guessing_game { // ... contract definition ... #[test(caller = @0x1337)] fun test_flow(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); create_game(caller, 1); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 1); assert!(is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[1]); } } ``` You can test this now by running the below command: ```sh aptos move test --dev ``` And this testing can be added to in order to make it more extensive and include resetting the game: ```move module module_addr::guessing_game { // ... contract definition ... #[test(caller = @0x1337)] fun test_flow(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); create_game(caller, 1); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 1); assert!(is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[1]); reset_game(caller, 2); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 3); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[3]); guess(caller, 4); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[3, 4]); guess(caller, 2); assert!(is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[3, 4, 2]); } } ``` ## Testing failures Failures can similarly be tested, by checking for abort codes. These can be added by adding the `#[expected_failure(abort_code = ...)]`. > Note that the abort code can either be a constant e.g. `E_ALREADY_GUESSED` or the number directly `5`. ```move module module_addr::guessing_game { // ... contract definition ... #[test(caller = @0x1337)] #[expected_failure(abort_code = E_ALREADY_GUESSED)] fun test_double_guess(caller: &signer) acquires Game {} } ``` If you run this test and there is no abort in the test, it will fail. Meanwhile, we can add the actual functionality into the test. When the below test is run, it will pass with a failure on the second `guess` call. ```move module module_addr::guessing_game { // ... contract definition ... #[test(caller = @0x1337)] #[expected_failure(abort_code = E_ALREADY_GUESSED)] fun test_double_guess(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); create_game(caller, 1); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 2); guess(caller, 2); } } ``` For more specific abort checking, you can add the location of the abort: ```move module module_addr::guessing_game { #[test(caller = @0x1337)] #[expected_failure(abort_code = E_ALREADY_GUESSED, location = module_addr::guessing_game)] fun test_double_guess(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); create_game(caller, 1); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 2); guess(caller, 2); } } ``` ## Full Example The full tests written are here: ```move module module_addr::guessing_game { // ... contract definition ... #[test(caller = @0x1337)] /// Tests the full flow of the game fun test_flow(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); create_game(caller, 1); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 1); assert!(is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[1]); reset_game(caller, 2); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 3); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[3]); guess(caller, 4); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[3, 4]); guess(caller, 2); assert!(is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[3, 4, 2]); } #[test(caller = @0x1337)] #[expected_failure(abort_code = E_ALREADY_GUESSED, location = module_addr::guessing_game)] /// Tests guessing the same number fun test_double_guess(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); create_game(caller, 1); assert!(!is_game_over(caller_addr)); assert!(guesses(caller_addr) == vector[]); guess(caller, 2); guess(caller, 2); } } ``` # Deploying the Contract To deploy the contract, we'll use the Aptos CLI. We'll assume you have already run the command `aptos init` for the default profile. If you have not, go check out the [hello_aptos_cli tutorial](../getting_started/hello_aptos_cli.md). In order to deploy the contract, we'll run the below command that will compile the contract, and then ask you if you're sure that you want to deploy it. ```sh aptos move deploy --named-addresses module_addr=default ``` Once it's deployed, you should be able to see your contract on your network and your address in the [Aptos Explorer](https://explorer.aptoslabs.com). This uploads your source code by default, so it's easily verifiable and easy to tell which functions are which. ## Upgrading the Contract To upgrade the contract, simply run the command again: ```sh aptos move deploy --named-addresses module_addr=default ``` # Interacting with the Contract Now that the guessing game contract is deployed, we can interact with it using the Aptos CLI. We'll assume you're using the `default` profile created earlier with `aptos init`. The guessing game module was published under your account's address, so all function calls can be made using: ``` --function-id default::guessing_game:: ``` The `default` profile automatically provides the signer argument, so we only need to supply the non-signer parameters. ## Creating a Game To start a new game, call the `create_game` entry function and provide a number (0–255) that players must guess: ```sh aptos move run \ --profile default \ --function-id default::guessing_game::create_game \ --args 'u8:5' ``` This initializes a new game for the signer with the answer set to `5`. ## Making a Guess Once a game is created, you can make guesses by calling the `guess` function: ```sh aptos move run \ --profile default \ --function-id default::guessing_game::guess \ --args 'u8:3' ``` You can continue guessing with different values: ```sh aptos move run \ --profile default \ --function-id default::guessing_game::guess \ --args 'u8:5' ``` If the guess matches the stored answer, the game is marked as over. ## Viewing Game State The contract provides several view functions, which allow anyone to inspect the game state without modifying it. ### Is the game over? ```sh aptos move view \ --profile default \ --function-id default::guessing_game::is_game_over \ --args address:default ``` ### View the answer (only allowed when the game is over) ```sh aptos move view \ --profile default \ --function-id default::guessing_game::number \ --args address:default ``` ### View all guesses made so far ```sh aptos move view \ --profile default \ --function-id default::guessing_game::guesses \ --args address:default ``` Here, `address:default` automatically resolves to the signer’s on-chain account address. ## Resetting or Removing the Game Once a game has been completed, it can be reset with a new number: ```sh aptos move run \ --profile default \ --function-id default::guessing_game::reset_game \ --args 'u8:7' ``` To remove the game state entirely from your account: ```sh aptos move run \ --profile default \ --function-id default::guessing_game::remove_state ``` This clears the `Game` resource from storage and allows a completely new game to be created. # Extending the Contract ## Adding Randomness Providing the number to guess is kind of silly, let's add a function to generate a random number and insert it into the contract. The backwards compatibility rules say that we cannot remove existing functions, so we will just abort on the existing functions. This should keep it clean to remove warnings, and allow for upgrading. ```move module module_addr::guessing_game { // ... contract ... /// Manual inputs are no longer supported const E_NO_LONGER_SUPPORTED: u64 = 6; entry fun create_game(_caller: &signer, _num: u8) { abort E_NO_LONGER_SUPPORTED } entry fun reset_game(_caller: &signer, _num: u8) { abort E_NO_LONGER_SUPPORTED } } ``` We will then add new functions `create_game_random` and `reset_game_random` which mirror the original functions but only use random inputs. ```move module module_addr::guessing_game { use aptos_framework::randomness; #[randomness] entry fun create_game_random(caller: &signer) { let caller_addr = signer::address_of(caller); assert!(!exists(caller_addr), E_GAME_INITIALIZED); let number = randomness::u8_integer(); move_to(caller, Game { number, guesses: vector[], game_over: false, }) } #[randomness] entry fun reset_game_random(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); // Check that the game exists assert!(exists(caller_addr), E_NO_GAME); let game = &mut Game[caller_addr]; // Check that the game is over assert!(game.game_over, E_GAME_NOT_OVER); game.game_over = false; game.guesses = vector[]; game.number = randomness::u8_integer(); } } ``` But, of course the tests now fail! We will need to update the existing tests given our expectations. This can be done by either adding a `#[test_only]` function, or by setting the seed for the randomness in tests. We can add a `#[test_only]` helper function that lets us create a game with a known number for testing: ```move module module_addr::guessing_game { // ... existing code ... #[test_only] fun create_game_for_test(caller: &signer, number: u8) { let caller_addr = signer::address_of(caller); assert!(!exists(caller_addr), E_GAME_INITIALIZED); move_to(caller, Game { number, guesses: vector[], game_over: false, }) } #[test_only] fun reset_game_for_test(caller: &signer, new_num: u8) acquires Game { let caller_addr = signer::address_of(caller); assert!(exists(caller_addr), E_NO_GAME); let game = &mut Game[caller_addr]; assert!(game.game_over, E_GAME_NOT_OVER); game.game_over = false; game.guesses = vector[]; game.number = new_num; } #[test(caller = @0x1337)] fun test_flow(caller: &signer) acquires Game { let caller_addr = signer::address_of(caller); create_game_for_test(caller, 1); assert!(!is_game_over(caller_addr)); guess(caller, 1); assert!(is_game_over(caller_addr)); reset_game_for_test(caller, 2); assert!(!is_game_over(caller_addr)); guess(caller, 2); assert!(is_game_over(caller_addr)); } } ``` The `#[test_only]` attribute ensures these helper functions are excluded from the production bytecode. They give us a deterministic way to test the game logic without relying on randomness. Once this is done, we can simply upgrade the contract by deploying again. ```sh aptos move deploy --named-addresses module_addr=default ``` # Conclusion The guessing game example has given a full demo of creating a contract, testing a contract, deploying, and extending a contract. To learn more, and check out more information about Move contracts, checkout the rest of the book and these resources and more: * [Aptos Learn](https://learn.aptoslabs.com) * [Aptos.dev](https://aptos.dev) * [Move Spiders](https://movespiders.com) # Common Programming Concepts ## Introduction This chapter covers the fundamental programming concepts that form the foundation of Move development on Aptos. These concepts are essential for writing safe, efficient, and maintainable smart contracts. **Definition 1.1 (Programming Concept)** A programming concept is a fundamental idea or principle that guides how we structure and organize code to solve problems effectively. ## Core Concepts Overview The common programming concepts in Move include: 1. **Variables**: Named storage locations that hold values 2. **Data Types**: Classifications that define the nature and operations of values 3. **Functions**: Reusable blocks of code that perform specific tasks 4. **Comments**: Documentation that explains code behavior 5. **Control Flow**: Mechanisms that determine the order of execution ## Move-Specific Considerations **Theorem 1.1 (Move Safety Properties)** Move enforces several safety properties that distinguish it from other programming languages: - **Resource Safety**: Resources cannot be duplicated or lost - **Type Safety**: All operations are type-checked at compile time - **Memory Safety**: No null pointers, dangling references, or buffer overflows - **Thread Safety**: All values are immutable by default ### Resource-Oriented Programming **Definition 1.2 (Resource)** A resource is a special type in Move that represents digital assets with strict ownership semantics. Resources: - Cannot be copied - Cannot be dropped - Must be explicitly moved or destroyed ```move // Example: A simple resource representing a digital coin struct Coin has key { value: u64, owner: address, } // Resources must be explicitly handled public fun create_coin(value: u64, owner: address): Coin { Coin { value, owner } } public fun destroy_coin(coin: Coin) { // Explicit destruction - no automatic cleanup let Coin { value: _, owner: _ } = coin; } ``` ### Abilities System **Definition 1.3 (Ability)** An ability is a property that can be attached to types to specify what operations are allowed: - **key**: Can be stored as a top-level value - **store**: Can be stored inside other values - **copy**: Can be copied - **drop**: Can be dropped **Theorem 1.2 (Ability Composition)** The abilities system ensures that: - Resources (no copy, no drop) maintain their safety properties - Ordinary values can be freely manipulated - Storage operations are explicit and controlled ## Learning Path This chapter is organized to build understanding progressively: 1. **Variables**: Start with basic value storage and manipulation 2. **Data Types**: Understand the type system and available types 3. **Functions**: Learn to organize code into reusable units 4. **Comments**: Document code for maintainability 5. **Control Flow**: Control program execution flow ## Best Practices **Principle 1.1 (Move Best Practices)** When writing Move code: 1. **Use Resources for Assets**: Represent digital assets as resources 2. **Minimize Abilities**: Only add abilities when necessary 3. **Explicit Ownership**: Make ownership transfers explicit 4. **Type Safety**: Leverage the type system for correctness 5. **Documentation**: Comment complex logic and invariants ## Common Pitfalls **Warning 1.1 (Common Mistakes)** Avoid these common mistakes in Move: 1. **Forgetting Abilities**: Not adding required abilities to types 2. **Resource Leaks**: Creating resources without proper cleanup 3. **Type Mismatches**: Using wrong types in operations 4. **Unsafe References**: Creating references that could become invalid 5. **Missing Error Handling**: Not handling potential failures ## Tools and Development **Definition 1.4 (Development Tools)** Essential tools for Move development: - **Move Compiler**: Type checking and compilation - **Move Prover**: Formal verification - **Aptos CLI**: Deployment and interaction - **IDE Support**: Syntax highlighting and error detection ## Conclusion Understanding these common programming concepts is essential for effective Move development. The Move language's unique features, particularly its resource system and abilities, provide powerful safety guarantees but require careful attention to detail. The following chapters will explore each concept in depth, providing practical examples and best practices for writing secure and efficient smart contracts. ## Exercises 1. **Exercise 1.1**: Create a simple resource type with appropriate abilities 2. **Exercise 1.2**: Identify the abilities required for different use cases 3. **Exercise 1.3**: Analyze the safety properties of a given Move program 4. **Exercise 1.4**: Compare Move's type system with other programming languages ## References 1. Move Language Documentation 2. Aptos Developer Resources 3. Move Prover User Guide 4. Smart Contract Security Best Practices # Variables Variables are fundamental building blocks in Move programs that allow you to store and manipulate data. Understanding how to create and manage variables is essential for writing effective Move code. ## Variable Declaration ### Basic Variable Declaration Variables in Move are declared using the `let` keyword, followed by the variable name and an optional type annotation. ```move module my_module::variables { public fun basic_variables() { // Variable with type inference let x = 42; // Variable with explicit type annotation let y: u64 = 100; // Variable with explicit type and suffix let z = 255u8; // Boolean variable let is_active = true; // String variable let message = string::utf8(b"Hello, Move!"); // Address variable let account_addr = @0x1; } } ``` ### Variable Declaration with Different Types ```move public fun variable_types() { // Integer types let small_number: u8 = 255; let medium_number: u16 = 65535; let large_number: u64 = 18446744073709551615; let very_large_number: u128 = 340282366920938463463374607431768211455u128; // Boolean type let flag: bool = true; // Address type let addr: address = @0xABCDEF; // Vector type let numbers: vector = vector[1, 2, 3, 4, 5]; // String type let text: String = string::utf8(b"Move programming"); // Tuple type let pair: (u64, String) = (42, string::utf8(b"answer")); } ``` ## Variable Assignment and Mutability ### Immutable Variables (Default) By default, variables in Move are immutable, meaning they cannot be changed after declaration. ```move public fun immutable_variables() { let x = 42; // x = 50; // This would cause a compilation error let message = string::utf8(b"Hello"); // message = string::utf8(b"World"); // This would cause a compilation error } ``` ### Mutable Variables To create a mutable variable, use the `mut` keyword. ```move public fun mutable_variables() { // Mutable variable declaration let mut x = 42; x = 50; // Now this is allowed let mut counter = 0u64; counter = counter + 1; counter = counter + 1; // Mutable vector let mut numbers = vector[1, 2, 3]; vector::push_back(&mut numbers, 4); vector::push_back(&mut numbers, 5); // Mutable string let mut message = string::utf8(b"Hello"); message = message + string::utf8(b" World"); } ``` ### Variable Reassignment ```move public fun variable_reassignment() { let mut value = 10; // Reassign with different value value = 20; // Reassign with calculation value = value * 2; // Reassign with function result value = add(value, 5); // Reassign with conditional if (value > 50) { value = 100; } else { value = 0; }; } fun add(a: u64, b: u64): u64 { a + b } ``` ## Variable Scoping ### Local Variables Variables declared within a function are local to that function and are automatically dropped when the function ends. ```move public fun local_variables() { let x = 10; // Local variable { let y = 20; // Local variable in inner scope let z = x + y; // Can access outer scope variable // z is dropped at end of inner scope }; // y is not accessible here // z is not accessible here // x is still accessible let result = x * 2; } ``` ### Variable Shadowing Move allows variable shadowing, where a new variable with the same name can be declared in an inner scope. ```move public fun variable_shadowing() { let x = 10; { let x = 20; // Shadows the outer x // x is 20 in this scope }; // x is 10 again in outer scope } ``` ### Variable Lifetime ```move public fun variable_lifetime() { let mut counter = 0u64; while (counter < 5) { let temp = counter * 2; // temp is created in each iteration counter = counter + 1; // temp is dropped at end of each iteration }; // counter is still available here let final_value = counter; } ``` ## Variable Initialization ### Immediate Initialization Variables must be initialized when declared. ```move public fun immediate_initialization() { // All variables must be initialized let x = 42; let y: u64 = 100; let flag = true; let message = string::utf8(b"Hello"); } ``` ### Conditional Initialization Variables can be initialized conditionally. ```move public fun conditional_initialization() { let condition = true; let value = if (condition) { 100 } else { 200 }; // value is now either 100 or 200 } ``` ### Initialization with Function Calls ```move public fun initialization_with_functions() { let result = calculate_value(10); let message = create_message(string::utf8(b"Hello")); let numbers = create_vector(5); } fun calculate_value(input: u64): u64 { input * 2 } fun create_message(prefix: String): String { prefix + string::utf8(b" World") } fun create_vector(size: u64): vector { let mut vec = vector::empty(); let i = 0; while (i < size) { vector::push_back(&mut vec, i); i = i + 1; }; vec } ``` ## Variable Patterns ### Destructuring Assignment Move supports destructuring assignment for tuples and structs. ```move public fun destructuring() { // Tuple destructuring let (x, y) = (10, 20); // Nested tuple destructuring let ((a, b), c) = ((1, 2), 3); // Struct destructuring (if you have a struct) // let Point { x, y } = Point { x: 10, y: 20 }; } ``` ### Multiple Variable Declaration ```move public fun multiple_variables() { // Declare multiple variables let x = 1; let y = 2; let z = 3; // Or use tuple destructuring let (a, b, c) = (1, 2, 3); // With different types let (number, text, flag) = (42, string::utf8(b"Hello"), true); } ``` ## Variable Best Practices ### Choose Descriptive Names ```move public fun good_naming() { // Good: Descriptive names let user_age = 25; let account_balance = 1000; let is_user_active = true; let user_name = string::utf8(b"Alice"); // Bad: Unclear names let x = 25; let y = 1000; let flag = true; let s = string::utf8(b"Alice"); } ``` ### Use Appropriate Types ```move public fun appropriate_types() { // Use u8 for small numbers let age: u8 = 25; // Use u64 for most calculations let balance: u64 = 1000000; // Use u128 for large financial values let total_supply: u128 = 1000000000000000000u128; // Use bool for flags let is_enabled: bool = true; // Use address for account identifiers let user_address: address = @0x1; } ``` ### Minimize Mutable Variables ```move public fun minimize_mutability() { // Good: Use immutable variables when possible let max_attempts = 3; let timeout_duration = 5000; // Only use mut when you need to change the value let mut current_attempt = 0; let mut elapsed_time = 0; while (current_attempt < max_attempts) { current_attempt = current_attempt + 1; elapsed_time = elapsed_time + 1000; }; } ``` ### Initialize Variables Close to Use ```move public fun initialize_close_to_use() { // Good: Initialize when needed let result = perform_calculation(10); if (result > 50) { let message = string::utf8(b"High result"); // Use message here } else { let message = string::utf8(b"Low result"); // Use message here }; // Bad: Initialize too early // let message = string::utf8(b""); // Unused variable // let result = perform_calculation(10); // if (result > 50) { // message = string::utf8(b"High result"); // } else { // message = string::utf8(b"Low result"); // }; } ``` ## Common Variable Patterns ### Counter Pattern ```move public fun counter_pattern() { let mut counter = 0u64; let max_count = 10; while (counter < max_count) { // Process something counter = counter + 1; }; } ``` ### Accumulator Pattern ```move public fun accumulator_pattern() { let numbers = vector[1, 2, 3, 4, 5]; let mut sum = 0u64; let i = 0; let len = vector::length(&numbers); while (i < len) { let current = *vector::borrow(&numbers, i); sum = sum + current; i = i + 1; }; // sum now contains the total } ``` ### Flag Pattern ```move public fun flag_pattern() { let numbers = vector[1, 2, 3, 4, 5]; let mut found = false; let mut found_value = 0u64; let target = 3; let i = 0; let len = vector::length(&numbers); while (i < len && !found) { let current = *vector::borrow(&numbers, i); if (current == target) { found = true; found_value = current; }; i = i + 1; }; } ``` ### Temporary Variable Pattern ```move public fun temporary_variable_pattern() { let x = 10; let y = 20; // Use temporary variable for complex calculation let temp_sum = x + y; let result = temp_sum * 2; // Or for readability let base_value = calculate_base(x, y); let adjusted_value = apply_adjustment(base_value); let final_result = apply_discount(adjusted_value); } fun calculate_base(a: u64, b: u64): u64 { a + b } fun apply_adjustment(value: u64): u64 { value * 2 } fun apply_discount(value: u64): u64 { value - (value / 10) } ``` ## Variable Safety ### Type Safety Move's type system prevents many common programming errors. ```move public fun type_safety() { let x: u64 = 42; let y: u8 = 255; // Type conversion is explicit let z: u64 = x + (y as u64); // This would cause a compilation error: // let invalid = x + y; // Cannot add u64 and u8 directly } ``` ### Bounds Checking ```move public fun bounds_checking() { let mut index = 0u64; let max_index = 10; // Safe bounds checking if (index < max_index) { index = index + 1; }; // Safe array access (if you had an array) // if (index < vector::length(&array)) { // let element = *vector::borrow(&array, index); // }; } ``` ### Null Safety Move doesn't have null values, which prevents null pointer errors. ```move public fun null_safety() { // Move doesn't have null, so no null pointer errors let value: u64 = 42; // Always has a value // Use Option for optional values // let optional_value: Option = option::some(42); } ``` ## Performance Considerations ### Variable Reuse ```move public fun variable_reuse() { let mut temp = 0u64; // Reuse variable for different purposes temp = calculate_value(10); // Use temp... temp = calculate_value(20); // Use temp again... temp = calculate_value(30); // Use temp one more time... } fun calculate_value(input: u64): u64 { input * 2 } ``` ### Avoid Unnecessary Variables ```move public fun avoid_unnecessary_variables() { // Good: Direct return let result = add(10, 20); // Bad: Unnecessary intermediate variable // let temp = add(10, 20); // let result = temp; } fun add(a: u64, b: u64): u64 { a + b } ``` By understanding and following these variable management practices, you can write more readable, maintainable, and efficient Move code. Variables are the foundation of data manipulation in Move, and proper variable management is essential for building robust smart contracts. # Data Types Move is a statically typed language, which means every variable and expression has a type that is known at compile time. Understanding the available data types is fundamental to writing effective Move code. ## Primitive Types ### Integer Types Move provides several integer types with different sizes and characteristics. #### Unsigned Integers ```move module my_module::integers { // 8-bit unsigned integer (0 to 255) public fun u8_example(): u8 { 255u8 } // 16-bit unsigned integer (0 to 65,535) public fun u16_example(): u16 { 65535u16 } // 32-bit unsigned integer (0 to 4,294,967,295) public fun u32_example(): u32 { 4294967295u32 } // 64-bit unsigned integer (0 to 18,446,744,073,709,551,615) public fun u64_example(): u64 { 18446744073709551615u64 } // 128-bit unsigned integer (0 to 2^128 - 1) public fun u128_example(): u128 { 340282366920938463463374607431768211455u128 } // 256-bit unsigned integer (0 to 2^256 - 1) public fun u256_example(): u256 { 115792089237316195423570985008687907853269984665640564039457584007913129639935u256 } } ``` #### Integer Literals ```move public fun integer_literals() { // Default integer literals are u64 let default_int: u64 = 42; // Explicit type annotations let small_int: u8 = 255u8; let medium_int: u16 = 65535u16; let large_int: u32 = 4294967295u32; let very_large_int: u128 = 340282366920938463463374607431768211455u128; // Hex literals let hex_u8: u8 = 0xFFu8; let hex_u64: u64 = 0xFFFFFFFFu64; } ``` #### Integer Operations ```move public fun integer_operations() { let a: u64 = 10; let b: u64 = 3; // Arithmetic operations let sum = a + b; // 13 let difference = a - b; // 7 let product = a * b; // 30 let quotient = a / b; // 3 let remainder = a % b; // 1 // Bitwise operations let bitwise_and = a & b; // 2 let bitwise_or = a | b; // 11 let bitwise_xor = a ^ b; // 9 let left_shift = a << 2; // 40 let right_shift = a >> 1; // 5 // Comparison operations let is_equal = a == b; // false let is_not_equal = a != b; // true let is_greater = a > b; // true let is_less = a < b; // false let is_greater_equal = a >= b; // true let is_less_equal = a <= b; // false } ``` ### Boolean Type The boolean type represents true or false values. ```move public fun boolean_examples() { // Boolean literals let true_value: bool = true; let false_value: bool = false; // Boolean operations let and_result = true && false; // false let or_result = true || false; // true let not_result = !true; // false // Comparison results let comparison_result: bool = 5 > 3; // true let equality_result: bool = 5 == 5; // true } ``` ### Address Type Addresses represent 32-byte identifiers used for accounts, modules, and resources. ```move public fun address_examples() { // Address literals let account_address: address = @0x1; let custom_address: address = @0xABCDEF; let full_address: address = @0x0000000000000000000000000000000000000000000000000000000000000001; // Special addresses let zero_address: address = @0x0; let framework_address: address = @0x1; // Aptos framework let token_address: address = @0x3; // Legacy token standard let object_address: address = @0x4; // Object standard } ``` ### Vector Type Vectors are dynamic arrays that can hold multiple values of the same type. ```move public fun vector_examples() { // Creating vectors let empty_vector: vector = vector::empty(); let literal_vector: vector = vector[1, 2, 3, 4, 5]; let string_vector: vector = vector[ string::utf8(b"hello"), string::utf8(b"world") ]; // Vector operations let mut vec = vector[1, 2, 3]; vector::push_back(&mut vec, 4); let length = vector::length(&vec); let first_element = *vector::borrow(&vec, 0); let last_element = vector::pop_back(&mut vec); } ``` ### String Type Strings are UTF-8 encoded text stored as vectors of bytes. ```move public fun string_examples() { // Creating strings let empty_string: String = string::utf8(b""); let hello_string: String = string::utf8(b"Hello, World!"); let unicode_string: String = string::utf8(b"Hello 世界"); // String operations let length = string::length(&hello_string); let is_empty = string::is_empty(&empty_string); let concatenated = string::utf8(b"Hello") + string::utf8(b" World"); // String comparison let are_equal = hello_string == string::utf8(b"Hello, World!"); let are_not_equal = hello_string != string::utf8(b"Goodbye"); } ``` ### Signer Type The signer type represents the account that signed the transaction. ```move public fun signer_examples(account: &signer) { // Getting the address of the signer let account_address = signer::address_of(account); // Signer is used for resource operations // move_to(account, MyResource { data: 42 }); } ``` ## Type Abilities Move types have abilities that determine how they can be used. The main abilities are: - **copy**: Can be copied - **drop**: Can be dropped (destroyed) - **store**: Can be stored in global storage - **key**: Can be used as a key in global storage ### Ability Examples ```move module my_module::abilities { // Integer types have copy, drop, and store abilities public fun integer_abilities() { let x: u64 = 42; let y = x; // Copy // x is still available due to copy ability } // Vector has copy, drop, and store abilities public fun vector_abilities() { let v1: vector = vector[1, 2, 3]; let v2 = v1; // Copy // v1 is still available } // String has copy, drop, and store abilities public fun string_abilities() { let s1: String = string::utf8(b"hello"); let s2 = s1; // Copy // s1 is still available } // Signer has drop ability only public fun signer_abilities(account: &signer) { let addr = signer::address_of(account); // Cannot copy signer, but can drop it } } ``` ## Type Annotations Move uses type annotations to specify the type of variables and function parameters. ```move public fun type_annotations() { // Explicit type annotations let explicit_u64: u64 = 42; let explicit_bool: bool = true; let explicit_address: address = @0x1; let explicit_vector: vector = vector[1, 2, 3]; let explicit_string: String = string::utf8(b"hello"); // Type inference (when possible) let inferred_u64 = 42; // Inferred as u64 let inferred_bool = true; // Inferred as bool let inferred_vector = vector[1, 2, 3]; // Inferred as vector } ``` ## Type Conversion Move is strict about type conversions and requires explicit casting. ```move public fun type_conversion() { // Explicit casting between integer types let small: u8 = 255; let medium: u16 = (small as u16); let large: u64 = (medium as u64); // Casting to smaller types (may truncate) let large_num: u64 = 1000; let small_num: u8 = (large_num as u8); // Will be 232 (1000 % 256) // Boolean to integer conversion let bool_val: bool = true; let int_val: u64 = (bool_val as u64); // 1 for true, 0 for false } ``` ## Generic Types Move supports generic types for creating reusable code. ```move public fun generic_examples() { // Generic vector operations let int_vector: vector = vector[1, 2, 3]; let string_vector: vector = vector[ string::utf8(b"hello"), string::utf8(b"world") ]; // Generic function example let int_length = vector::length(&int_vector); let string_length = vector::length(&string_vector); } ``` ## Type Safety Move's type system prevents many common programming errors. ```move public fun type_safety_examples() { // This would cause a compilation error: // let x: u64 = "hello"; // Cannot assign string to u64 // This would cause a compilation error: // let y: u8 = 256; // Value too large for u8 // This would cause a compilation error: // let z = x + "world"; // Cannot add u64 and string // Correct usage let x: u64 = 42; let y: u8 = 255; let z: u64 = x + (y as u64); } ``` ## Best Practices ### Choose Appropriate Types ```move public fun choose_types() { // Use u8 for small numbers (0-255) let age: u8 = 25; // Use u64 for most integer operations let balance: u64 = 1000000; // Use u128 for large financial calculations let total_supply: u128 = 1000000000000000000u128; // Use u256 for cryptographic operations let private_key: u256 = 0x1234567890ABCDEFu256; } ``` ### Use Type Annotations for Clarity ```move public fun clear_types() { // Explicit types make code more readable let user_id: u64 = 12345; let is_active: bool = true; let user_address: address = @0xABCDEF; let user_name: String = string::utf8(b"Alice"); // Avoid ambiguous literals let small_number: u8 = 42u8; // Clear that this is u8 let large_number: u128 = 1000000000000000000u128; // Clear that this is u128 } ``` ### Handle Type Conversions Carefully ```move public fun safe_conversions() { let large: u64 = 1000; // Check bounds before converting to smaller types if (large <= 255) { let small: u8 = (large as u8); // Safe conversion } else { // Handle overflow case // abort 0 } // Use appropriate types for calculations let a: u64 = 1000000; let b: u64 = 2000000; let result: u64 = a * b; // May overflow u64 // Consider using u128 for large calculations let safe_result: u128 = (a as u128) * (b as u128); } ``` ### Use Vectors Efficiently ```move public fun vector_best_practices() { // Pre-allocate when you know the size let mut numbers = vector::empty(); vector::push_back(&mut numbers, 1); vector::push_back(&mut numbers, 2); vector::push_back(&mut numbers, 3); // Use appropriate element types let small_numbers: vector = vector[1, 2, 3, 4, 5]; let large_numbers: vector = vector[1000000, 2000000, 3000000]; // Consider storage costs for large vectors // Each vector element in global storage uses a storage slot } ``` ## Common Type Patterns ### Error Handling with Types ```move enum Result { Ok(T), Err(String), } public fun safe_divide(a: u64, b: u64): Result { if (b == 0) { return Result::Err(string::utf8(b"Division by zero")) }; Result::Ok(a / b) } ``` ### Option Type Pattern ```move enum Option { Some(T), None, } public fun find_element(numbers: vector, target: u64): Option { let i = 0; let len = vector::length(&numbers); while (i < len) { let current = *vector::borrow(&numbers, i); if (current == target) { return Option::Some(current) }; i = i + 1; }; Option::None } ``` Understanding these data types and their characteristics is essential for writing efficient and correct Move code. The type system helps prevent errors and makes code more maintainable and readable. # Functions Functions are the building blocks of Move programs. They allow you to encapsulate logic, reuse code, and create modular, maintainable programs. Move provides several types of functions with different visibility levels and purposes. ## Function Types and Visibility ### Public Functions Public functions can be called from outside the module. ```move module my_module::functions { /// Public functions can be called from outside the module. public fun add(x: u64, y: u64): u64 { x + y } /// Public functions can also be entry functions. public entry fun public_entry(account: &signer, value: u64) { let addr = signer::address_of(account); // Process the transaction } } ``` ### Private Functions Private functions can only be called from within the same module. Functions are private by default. ```move module my_module::private_functions { /// Private functions can only be called from within the module. fun multiply(x: u64, y: u64): u64 { x * y } /// Public function that uses private function public fun calculate_area(width: u64, height: u64): u64 { multiply(width, height) // Call private function } /// Private helper function fun validate_input(value: u64): bool { value > 0 && value < 1000 } public fun process_data(input: u64): u64 { if (validate_input(input)) { // Call private function input * 2 } else { 0 } } } ``` ### Friend Functions Friend functions can be called from other modules that declare this module as a friend. ```move module my_module::friend_example { /// Friend functions can be called from other modules that declare this module as a friend. friend fun sum_vector(vec: vector): u64 { let sum = 0u64; let i = 0; let len = vector::length(&vec); while (i < len) { sum = sum + *vector::borrow(&vec, i); i = i + 1; }; sum } /// Alternative syntax for friend functions public(friend) fun do_something_friend(x: u64): u64 { x + 1 } } /// Module that declares my_module::friend_example as a friend module my_module::friend_caller { friend my_module::friend_example; public fun call_friend_function(): u64 { let numbers = vector[1, 2, 3, 4, 5]; my_module::friend_example::sum_vector(numbers) } } ``` ### Package Functions Package functions can be called from within the package but not from outside. ```move module my_module::package_functions { /// Package functions can be called from within the package, but not from outside. package fun square(x: u64): u64 { x * x } /// Alternative syntax for package functions public(package) fun cube(x: u64): u64 { x * x * x } /// Public function that uses package function public fun calculate_power(base: u64, power: u8): u64 { if (power == 2) { square(base) // Call package function } else if (power == 3) { cube(base) // Call package function } else { base } } } ``` ## Entry Functions Entry functions are special functions that can be called as standalone transactions from outside the module. ```move module my_module::entry_functions { use std::signer; /// Entry functions can be called as standalone transactions. entry fun create_account(account: &signer) { let addr = signer::address_of(account); // Initialize account data } /// Entry functions can also be public, friend, or package. public entry fun transfer_funds( from: &signer, to: address, amount: u64 ) { let from_addr = signer::address_of(from); // Transfer logic here } /// Entry functions with multiple parameters entry fun update_profile( account: &signer, name: String, age: u8 ) { let addr = signer::address_of(account); // Update profile logic } /// Entry functions can return values (though they're typically void) entry fun calculate_and_return(x: u64, y: u64): u64 { x + y } } ``` ### Entry Function Best Practices ```move module my_module::entry_best_practices { use std::signer; use std::error; const EINSUFFICIENT_FUNDS: u64 = 1; const EINVALID_AMOUNT: u64 = 2; /// Good: Entry function with proper validation entry fun safe_transfer( from: &signer, to: address, amount: u64 ) { // Validate input assert!(amount > 0, error::invalid_argument(EINVALID_AMOUNT)); assert!(to != @0x0, error::invalid_argument(3)); let from_addr = signer::address_of(from); // Check balance (simplified) // let balance = get_balance(from_addr); // assert!(balance >= amount, error::invalid_state(EINSUFFICIENT_FUNDS)); // Perform transfer // transfer_funds(from_addr, to, amount); } /// Good: Entry function with clear parameter names entry fun create_user_profile( account: &signer, user_name: String, user_age: u8, user_email: String ) { let user_address = signer::address_of(account); // Validate input assert!(user_age >= 13, error::invalid_argument(4)); assert!(string::length(&user_name) > 0, error::invalid_argument(5)); // Create profile // create_profile(user_address, user_name, user_age, user_email); } } ``` ## View Functions View functions are read-only functions that can be called without a transaction. They are marked with the `#[view]` attribute. ```move module my_module::view_functions { use std::signer; struct UserProfile has key, store { name: String, age: u8, email: String, } /// View function - can be called without a transaction #[view] public fun get_user_name(user_addr: address): String { assert!(exists(user_addr), 0); borrow_global(user_addr).name } /// View function that returns multiple values #[view] public fun get_user_profile(user_addr: address): (String, u8, String) { assert!(exists(user_addr), 0); let profile = borrow_global(user_addr); (profile.name, profile.age, profile.email) } /// View function with complex logic #[view] public fun is_user_adult(user_addr: address): bool { if (!exists(user_addr)) { return false }; let profile = borrow_global(user_addr); profile.age >= 18 } /// View function that doesn't access global storage #[view] public fun calculate_discount(price: u64, discount_percent: u8): u64 { let discount = (price * (discount_percent as u64)) / 100; price - discount } } ``` ### View Function Best Practices ```move module my_module::view_best_practices { /// Good: View function with proper error handling #[view] public fun get_user_balance(user_addr: address): u64 { if (!exists(user_addr)) { return 0 }; borrow_global(user_addr).amount } /// Good: View function that validates input #[view] public fun calculate_compound_interest( principal: u64, rate: u64, time: u64 ): u64 { // Validate input parameters assert!(principal > 0, 0); assert!(rate <= 100, 0); // Rate as percentage assert!(time > 0, 0); // Calculate compound interest let rate_decimal = rate / 100; let amount = principal * ((1 + rate_decimal) ^ time); amount - principal } } ``` ## Test-Only Functions Test-only functions are only available during testing and are marked with the `#[test_only]` attribute. ```move module my_module::test_functions { use std::signer; /// Test-only functions are only available during testing. #[test_only] fun create_test_user(account: &signer, name: String): address { let addr = signer::address_of(account); // Create test user profile // move_to(account, UserProfile { name, age: 25, email: string::utf8(b"test@example.com") }); addr } /// Test-only functions can be public #[test_only] public fun setup_test_environment(): vector { vector[1, 2, 3, 4, 5] } /// Test function that tests other functions #[test_only] public fun test_add_function() { let result = add(2, 3); assert!(result == 5, 0); } /// Test function with test account #[test(account = @0x1)] public fun test_with_account(account: signer) { let addr = signer::address_of(&account); // Test logic here } /// Private function used by tests fun add(x: u64, y: u64): u64 { x + y } } ``` ### Test Function Best Practices ```move module my_module::test_best_practices { use std::signer; use std::error; const EINVALID_INPUT: u64 = 1; /// Good: Comprehensive test function #[test_only] public fun test_validation_function() { // Test valid input let result1 = validate_input(50); assert!(result1 == true, 0); // Test invalid input let result2 = validate_input(0); assert!(result2 == false, 0); let result3 = validate_input(1001); assert!(result3 == false, 0); } /// Good: Test function with multiple test cases #[test_only] public fun test_calculation_function() { // Test case 1: Normal calculation let result1 = calculate_discount(100, 10); assert!(result1 == 90, 0); // Test case 2: No discount let result2 = calculate_discount(100, 0); assert!(result2 == 100, 0); // Test case 3: Full discount let result3 = calculate_discount(100, 100); assert!(result3 == 0, 0); } /// Good: Test function with test account #[test(account = @0x1)] public fun test_account_operations(account: signer) { let addr = signer::address_of(&account); // Test account creation // create_account(&account); // assert!(exists(addr), 0); // Test account operations // update_profile(&account, string::utf8(b"Test User"), 25); // let profile = get_user_profile(addr); // assert!(profile.0 == string::utf8(b"Test User"), 0); } /// Helper functions for tests fun validate_input(value: u64): bool { value > 0 && value <= 1000 } fun calculate_discount(price: u64, discount_percent: u8): u64 { let discount = (price * (discount_percent as u64)) / 100; price - discount } } ``` ## Inline Functions Inline functions are small functions that can be inlined by the compiler for better performance. ```move module my_module::inline_functions { /// Inline function - compiler may inline this for better performance #[inline] public fun max(a: u64, b: u64): u64 { if (a > b) a else b } /// Inline function with multiple parameters #[inline] public fun clamp(value: u64, min: u64, max: u64): u64 { if (value < min) { min } else if (value > max) { max } else { value } } /// Inline function used in calculations #[inline] public fun is_even(n: u64): bool { n % 2 == 0 } /// Public function that uses inline functions public fun process_numbers(a: u64, b: u64, c: u64): u64 { let max_value = max(a, b); // Inline function call let clamped = clamp(c, 0, 100); // Inline function call if (is_even(max_value)) { // Inline function call max_value + clamped } else { max_value - clamped } } } ``` ### Inline Function Best Practices ```move module my_module::inline_best_practices { /// Good: Small, simple inline functions #[inline] public fun square(x: u64): u64 { x * x } /// Good: Inline function for frequently used operations #[inline] public fun is_positive(n: u64): bool { n > 0 } /// Good: Inline function for type conversions #[inline] public fun u8_to_u64(value: u8): u64 { (value as u64) } /// Avoid: Large inline functions (compiler may ignore inline hint) #[inline] public fun complex_calculation(a: u64, b: u64, c: u64): u64 { let temp1 = a * b; let temp2 = b * c; let temp3 = c * a; let result = temp1 + temp2 + temp3; result / 3 } } ``` ## Function Overloading and Generics Move doesn't support function overloading, but you can use generics to create flexible functions. ```move module my_module::generic_functions { /// Generic function that works with any type that has copy, drop abilities public fun identity(x: T): T { x } /// Generic function with constraints public fun add_generic(a: T, b: T): T { // Note: This is a simplified example // In practice, you'd need to implement specific logic for each type a } /// Generic function for vector operations public fun get_first(vec: &vector): T { assert!(vector::length(vec) > 0, 0); *vector::borrow(vec, 0) } /// Generic function with multiple type parameters public fun create_pair(first: T, second: U): (T, U) { (first, second) } } ``` ## Function Abilities and Constraints Functions can have ability constraints that determine what types they can work with. ```move module my_module::function_abilities { /// Function that requires copy ability public fun duplicate(x: T): (T, T) { (x, x) } /// Function that requires drop ability public fun consume(x: T) { // x is automatically dropped at the end } /// Function that requires store ability public fun store_value(x: T) { // x can be stored in global storage } /// Function with multiple ability constraints public fun process_value(x: T): T { // x can be copied, dropped, and stored x } } ``` ## Best Practices Summary ### Function Visibility 1. **Start with private**: Make functions private by default 2. **Use public sparingly**: Only expose what's necessary 3. **Use friend for related modules**: When modules need to share functionality 4. **Use package for internal APIs**: For functions used within the package ### Entry Functions 1. **Validate input**: Always validate parameters in entry functions 2. **Use clear names**: Make function names descriptive 3. **Handle errors gracefully**: Use proper error handling 4. **Keep them simple**: Delegate complex logic to private functions ### View Functions 1. **Mark with `#[view]`**: Always mark read-only functions with `#[view]` 2. **Don't modify state**: View functions should be pure 3. **Handle missing data**: Return sensible defaults for missing data 4. **Optimize for queries**: Make view functions efficient ### Test Functions 1. **Use `#[test_only]`**: Mark test functions appropriately 2. **Test edge cases**: Include boundary condition tests 3. **Use descriptive names**: Make test names clear 4. **Test error conditions**: Include tests for error cases ### Inline Functions 1. **Keep them small**: Inline functions should be simple 2. **Use for hot paths**: Inline frequently called functions 3. **Don't overuse**: Let the compiler decide when to inline 4. **Profile first**: Measure performance before optimizing By understanding and using these different function types effectively, you can create well-structured, maintainable, and efficient Move programs. # Comments Comments are essential for documenting your Move code and making it more readable and maintainable. Move supports several types of comments that serve different purposes. ## Types of Comments ### 1. Documentation Comments (`///`) Documentation comments use three forward slashes (`///`) and are used to generate documentation for your code. These comments describe what's directly below them. ```move /// Writes a message to a single storage slot, all changes overwrite the previous. /// Changes are recorded in `MessageChange` events. module hello_blockchain::message { /// A resource for a single storage slot, holding a message. struct MessageHolder has key { message: String, } /// Event representing a change in a message, records the old and new messages, and who wrote it. #[event] struct MessageChange has drop, store { account: address, from_message: String, to_message: String, } /// The address does not contain a MessageHolder const ENO_MESSAGE: u64 = 0; /// Reads the message from storage slot #[view] public fun get_message(addr: address): String acquires MessageHolder { assert!(exists(addr), error::not_found(ENO_MESSAGE)); borrow_global(addr).message } /// Sets the message to the storage slot public entry fun set_message(account: signer, message: String) acquires MessageHolder { let account_addr = signer::address_of(&account); if (!exists(account_addr)) { move_to(&account, MessageHolder { message, }) } else { let old_message_holder = borrow_global_mut(account_addr); let from_message = old_message_holder.message; event::emit(MessageChange { account: account_addr, from_message, to_message: copy message, }); old_message_holder.message = message; } } } ``` ### 2. Single-Line Comments (`//`) Single-line comments use two forward slashes (`//`) and are used for brief explanations or notes within your code. ```move module my_module::example { // This is a single-line comment public fun add(x: u64, y: u64): u64 { x + y // Add the two numbers together } public fun process_data(data: vector): u64 { let sum = 0u64; let i = 0; let len = vector::length(&data); while (i < len) { sum = sum + *vector::borrow(&data, i); // Add current element to sum i = i + 1; // Increment counter }; sum } // TODO: Implement error handling for edge cases // FIXME: This function needs optimization for large datasets public fun complex_calculation(input: u64): u64 { // Validate input assert!(input > 0, 0); // Perform calculation let result = input * 2; result } } ``` ### 3. Multi-Line Comments (`/* */`) Multi-line comments use `/*` to start and `*/` to end. They can span multiple lines and are useful for longer explanations. ```move module my_module::complex_example { /* * This is a multi-line comment that can span * multiple lines. It's useful for longer explanations * or when you need to comment out large blocks of code. */ public fun complex_function(): u64 { /* * This function performs a complex calculation: * 1. Initialize variables * 2. Perform iterative computation * 3. Apply final transformation * 4. Return result */ let result = 0u64; /* * Commented out code block: * let temp = 100u64; * result = result + temp; */ result } } ``` ## Best Practices for Comments ### Documentation Comments (`///`) 1. **Use for public APIs**: Always document public functions, structs, and modules 2. **Be descriptive**: Explain what the code does, not how it does it 3. **Include examples**: When helpful, include usage examples 4. **Document parameters and return values**: Explain what each parameter does and what the function returns 5. **Use consistent formatting**: Keep documentation style consistent across your codebase ```move /// Calculates the sum of two unsigned 64-bit integers. /// /// # Arguments /// * `a` - The first number to add /// * `b` - The second number to add /// /// # Returns /// The sum of `a` and `b` /// /// # Example /// ``` /// let result = add(5, 3); // Returns 8 /// ``` public fun add(a: u64, b: u64): u64 { a + b } ``` ### Single-Line Comments (`//`) 1. **Explain complex logic**: Use comments to explain non-obvious code 2. **Avoid obvious comments**: Don't comment on what the code obviously does 3. **Use TODO/FIXME**: Mark areas that need attention 4. **Keep comments up to date**: Ensure comments reflect the current code ```move // Good: Explains complex logic let hash = sha3_256::hash(&data); // Hash the data for verification // Bad: Obvious comment let sum = a + b; // Add a and b // Good: Marks future work // TODO: Add input validation for edge cases public fun process_input(input: u64): u64 { input * 2 } ``` ### Multi-Line Comments (`/* */`) 1. **Use for block comments**: When you need to comment out large sections 2. **Document complex algorithms**: Explain multi-step processes 3. **Temporary code removal**: Comment out code you might need later ```move /* * This algorithm implements the following steps: * 1. Validate input parameters * 2. Initialize data structures * 3. Perform iterative computation * 4. Apply post-processing * 5. Return final result */ public fun complex_algorithm(input: vector): u64 { // Implementation here 0 } ``` ## Comment Guidelines ### What to Comment 1. **Public APIs**: Always document public functions, structs, and modules 2. **Complex logic**: Explain non-obvious algorithms or business logic 3. **Assumptions**: Document any assumptions your code makes 4. **Limitations**: Note any limitations or edge cases 5. **Dependencies**: Explain dependencies on external modules or systems ### What Not to Comment 1. **Obvious code**: Don't comment on what the code obviously does 2. **Outdated information**: Don't leave comments that are no longer accurate 3. **Implementation details**: Focus on what, not how (unless the how is complex) ### Comment Style 1. **Be concise**: Keep comments brief but informative 2. **Use proper grammar**: Write comments in clear, grammatically correct language 3. **Be consistent**: Use consistent terminology and style throughout your codebase 4. **Update with code**: When you change code, update related comments ## Examples in Context ### Module Documentation ```move /// A simple banking module that allows users to deposit and withdraw funds. /// /// This module provides basic banking functionality including: /// * Account creation and management /// * Deposit and withdrawal operations /// * Balance checking /// * Transaction history tracking /// /// # Security /// All operations require proper authorization and validation. module my_bank::banking { use std::signer; use std::error; /// Error codes for banking operations const EINSUFFICIENT_FUNDS: u64 = 1; const EACCOUNT_NOT_FOUND: u64 = 2; /// Represents a user's bank account struct Account has key, store { balance: u64, owner: address, } /// Creates a new bank account for the given signer public entry fun create_account(account: &signer) { let account_addr = signer::address_of(account); move_to(account, Account { balance: 0, owner: account_addr, }); } /// Deposits the specified amount into the account public entry fun deposit(account: &signer, amount: u64) acquires Account { let account_addr = signer::address_of(account); let account_ref = borrow_global_mut(account_addr); account_ref.balance = account_ref.balance + amount; } } ``` ### Function Documentation ```move /// Calculates the factorial of a given number. /// /// The factorial of a number n is the product of all positive integers /// less than or equal to n. For example, 5! = 5 × 4 × 3 × 2 × 1 = 120. /// /// # Arguments /// * `n` - The number to calculate factorial for (must be <= 20) /// /// # Returns /// The factorial of n /// /// # Panics /// Panics if n > 20 due to u64 overflow /// /// # Example /// ``` /// let result = factorial(5); // Returns 120 /// ``` public fun factorial(n: u64): u64 { if (n <= 1) { return 1 }; let result = 1u64; let i = 2u64; while (i <= n) { result = result * i; i = i + 1; }; result } ``` ## Documentation Generation Move documentation comments can be used to generate documentation for your modules. The `///` comments are processed by documentation generators to create readable documentation for your codebase. When writing documentation comments, remember that they should be: - **Clear and concise**: Easy to understand - **Complete**: Cover all important aspects - **Accurate**: Reflect the actual behavior of the code - **Helpful**: Provide value to developers using your code By following these guidelines, you'll create well-documented, maintainable Move code that's easy for others to understand and use. # Control Flow Control flow statements allow you to control the execution path of your Move programs. Move provides several control flow constructs that enable conditional execution, loops, and pattern matching. ## If Statements If statements allow you to execute different code blocks based on whether a condition is true or false. ### Basic If Statement ```move module my_module::control_flow { public fun basic_if(x: u64): u64 { if (x > 10) { x * 2 } else { x } } } ``` ### If-Else Statement ```move public fun if_else_example(age: u64): String { if (age < 18) { string::utf8(b"Minor") } else if (age < 65) { string::utf8(b"Adult") } else { string::utf8(b"Senior") } } ``` ### If Statement with Multiple Conditions ```move public fun complex_condition(x: u64, y: u64): bool { if (x > 0 && y > 0 && x + y < 100) { true } else { false } } ``` ### If Statement with Function Calls ```move public fun validate_input(input: u64): bool { if (input == 0) { return false }; if (input > 1000) { return false }; true } ``` ## While Loops While loops execute a block of code repeatedly as long as a condition remains true. ### Basic While Loop ```move public fun count_down(n: u64): u64 { let counter = n; while (counter > 0) { counter = counter - 1; }; counter } ``` ### While Loop with Break ```move public fun find_first_positive(numbers: vector): u64 { let i = 0; let len = vector::length(&numbers); let result = 0u64; while (i < len) { let current = *vector::borrow(&numbers, i); if (current > 0) { result = current; break }; i = i + 1; }; result } ``` ### While Loop with Continue ```move public fun sum_positive_numbers(numbers: vector): u64 { let i = 0; let len = vector::length(&numbers); let sum = 0u64; while (i < len) { let current = *vector::borrow(&numbers, i); i = i + 1; if (current == 0) { continue }; sum = sum + current; }; sum } ``` ### Infinite While Loop with Break ```move public fun find_element(numbers: vector, target: u64): bool { let i = 0; let len = vector::length(&numbers); while (true) { if (i >= len) { break false }; let current = *vector::borrow(&numbers, i); if (current == target) { break true }; i = i + 1; } } ``` ## For Loops For loops allow you to iterate over ranges or collections. Move supports for loops with range syntax. ### Basic For Loop with Range ```move public fun sum_range(start: u64, end: u64): u64 { let sum = 0u64; for (i in start..end) { sum = sum + i; }; sum } ``` ### For Loop with Step ```move public fun sum_even_numbers(max: u64): u64 { let sum = 0u64; for (i in 0..max) { if (i % 2 == 0) { sum = sum + i; }; }; sum } ``` ### For Loop with Vector Iteration ```move public fun sum_vector_elements(numbers: vector): u64 { let sum = 0u64; let len = vector::length(&numbers); for (i in 0..len) { let element = *vector::borrow(&numbers, i); sum = sum + element; }; sum } ``` ### For Loop with Early Exit ```move public fun find_index(numbers: vector, target: u64): u64 { let len = vector::length(&numbers); for (i in 0..len) { let current = *vector::borrow(&numbers, i); if (current == target) { return i }; }; len // Return length if not found } ``` ### Nested For Loops ```move public fun matrix_sum(matrix: vector>): u64 { let sum = 0u64; let rows = vector::length(&matrix); for (i in 0..rows) { let row = vector::borrow(&matrix, i); let cols = vector::length(row); for (j in 0..cols) { let element = *vector::borrow(row, j); sum = sum + element; }; }; sum } ``` ## Match Statements Match statements provide pattern matching capabilities, primarily used with enums. They allow you to execute different code based on the variant of an enum. ### Basic Match Statement ```move module my_module::enums { enum Status { Active, Inactive, Pending, } public fun get_status_message(status: Status): String { match (status) { Status::Active => string::utf8(b"User is active"), Status::Inactive => string::utf8(b"User is inactive"), Status::Pending => string::utf8(b"User status is pending"), } } } ``` ### Match Statement with Data-Carrying Enums ```move enum Result { Ok(T), Err(String), } public fun handle_result(result: Result): String { match (result) { Result::Ok(value) => string::utf8(b"Success: ") + std::to_string(value), Result::Err(message) => string::utf8(b"Error: ") + message, } } ``` ### Match Statement with Complex Patterns ```move enum Shape { Circle(u64), // radius Rectangle(u64, u64), // width, height Square(u64), // side } public fun calculate_area(shape: Shape): u64 { match (shape) { Shape::Circle(radius) => { // Approximate area calculation (π * r²) radius * radius * 3 }, Shape::Rectangle(width, height) => { width * height }, Shape::Square(side) => { side * side }, } } ``` ### Match Statement with Guards ```move enum Number { Zero, Positive(u64), Negative(u64), } public fun classify_number(num: Number): String { match (num) { Number::Zero => string::utf8(b"Zero"), Number::Positive(value) => { if (value < 10) { string::utf8(b"Small positive") } else { string::utf8(b"Large positive") } }, Number::Negative(value) => { if (value > 10) { string::utf8(b"Large negative") } else { string::utf8(b"Small negative") } }, } } ``` ### Match Statement with Default Case ```move enum Direction { North, South, East, West, } public fun get_direction_name(direction: Direction): String { match (direction) { Direction::North => string::utf8(b"North"), Direction::South => string::utf8(b"South"), Direction::East => string::utf8(b"East"), Direction::West => string::utf8(b"West"), } } ``` ### Match Statement in Error Handling ```move enum ValidationResult { Valid, InvalidAge, InvalidName, InvalidEmail, } public fun validate_user(age: u64, name: String, email: String): ValidationResult { if (age < 18 || age > 120) { return ValidationResult::InvalidAge }; if (string::length(&name) == 0) { return ValidationResult::InvalidName }; if (string::length(&email) == 0) { return ValidationResult::InvalidEmail }; ValidationResult::Valid } public fun get_validation_message(result: ValidationResult): String { match (result) { ValidationResult::Valid => string::utf8(b"User data is valid"), ValidationResult::InvalidAge => string::utf8(b"Age must be between 18 and 120"), ValidationResult::InvalidName => string::utf8(b"Name cannot be empty"), ValidationResult::InvalidEmail => string::utf8(b"Email cannot be empty"), } } ``` ## Best Practices ### If Statements 1. **Use early returns**: Return early to reduce nesting 2. **Keep conditions simple**: Break complex conditions into multiple if statements 3. **Use meaningful variable names**: Make conditions self-documenting ```move // Good: Early return public fun validate_input(input: u64): bool { if (input == 0) { return false }; if (input > 1000) { return false }; true } // Bad: Deep nesting public fun validate_input_bad(input: u64): bool { if (input != 0) { if (input <= 1000) { return true } else { return false } } else { return false } } ``` ### While Loops 1. **Ensure termination**: Make sure loops will eventually terminate 2. **Use break for early exit**: Use break instead of complex conditions 3. **Initialize variables properly**: Initialize loop variables before the loop ```move // Good: Clear termination condition public fun safe_loop(max_iterations: u64): u64 { let i = 0; while (i < max_iterations) { i = i + 1; }; i } // Bad: Potential infinite loop public fun unsafe_loop(): u64 { let i = 0; while (i >= 0) { // This will never be false for u64 i = i + 1; }; i } ``` ### For Loops 1. **Use for loops for known ranges**: Prefer for loops when you know the iteration count 2. **Avoid modifying loop variables**: Don't modify the loop variable inside the loop 3. **Use meaningful variable names**: Use descriptive names for loop variables ```move // Good: Clear iteration public fun process_array(data: vector): u64 { let sum = 0u64; let len = vector::length(&data); for (index in 0..len) { let element = *vector::borrow(&data, index); sum = sum + element; }; sum } ``` ### Match Statements 1. **Handle all cases**: Ensure all enum variants are covered 2. **Use meaningful patterns**: Use descriptive variable names in patterns 3. **Keep match arms simple**: Extract complex logic into separate functions ```move // Good: All cases handled public fun process_status(status: Status): String { match (status) { Status::Active => string::utf8(b"Active"), Status::Inactive => string::utf8(b"Inactive"), Status::Pending => string::utf8(b"Pending"), } } // Bad: Missing case (this would cause a compilation error) public fun process_status_bad(status: Status): String { match (status) { Status::Active => string::utf8(b"Active"), Status::Inactive => string::utf8(b"Inactive"), // Missing Status::Pending case } } ``` ## Common Patterns ### Error Handling Pattern ```move enum OperationResult { Success(u64), Failure(String), } public fun safe_divide(a: u64, b: u64): OperationResult { if (b == 0) { return OperationResult::Failure(string::utf8(b"Division by zero")) }; OperationResult::Success(a / b) } public fun handle_division_result(result: OperationResult): String { match (result) { OperationResult::Success(value) => { string::utf8(b"Result: ") + std::to_string(value) }, OperationResult::Failure(error) => { string::utf8(b"Error: ") + error }, } } ``` ### State Machine Pattern ```move enum GameState { Waiting, Playing, Paused, GameOver, } public fun update_game_state(current_state: GameState, action: String): GameState { match (current_state) { GameState::Waiting => { if (action == string::utf8(b"start")) { GameState::Playing } else { GameState::Waiting } }, GameState::Playing => { if (action == string::utf8(b"pause")) { GameState::Paused } else if (action == string::utf8(b"end")) { GameState::GameOver } else { GameState::Playing } }, GameState::Paused => { if (action == string::utf8(b"resume")) { GameState::Playing } else if (action == string::utf8(b"end")) { GameState::GameOver } else { GameState::Paused } }, GameState::GameOver => GameState::GameOver, } } ``` By understanding and using these control flow constructs effectively, you can write more expressive and maintainable Move code that handles complex logic and decision-making scenarios. # Understanding Ownership ## Introduction Ownership is one of the most distinctive and powerful features of the Move programming language. Unlike most programming languages that use garbage collection or manual memory management, Move enforces ownership rules at compile time, ensuring memory safety and preventing common programming errors. **Definition 2.1 (Ownership)** Ownership is a set of rules that govern how values are managed in memory, ensuring that each value has exactly one owner at any given time. ## Why Ownership Matters **Theorem 2.1 (Ownership Benefits)** Move's ownership system provides several critical benefits: 1. **Memory Safety**: No dangling references, use-after-free, or double-free errors 2. **Thread Safety**: Values cannot be accessed from multiple threads simultaneously 3. **Resource Management**: Resources are explicitly managed and cannot be lost 4. **Compile-Time Guarantees**: All ownership violations are caught at compile time ### Comparison with Other Languages **Definition 2.2 (Memory Management Approaches)** Different programming languages use various approaches to memory management: - **Garbage Collection (Java, Python)**: Automatic memory cleanup - **Manual Management (C, C++)**: Developer responsible for allocation/deallocation - **Ownership System (Move, Rust)**: Compile-time enforcement of ownership rules **Theorem 2.2 (Move vs. Other Languages)** Move's ownership system is more restrictive than garbage-collected languages but provides stronger safety guarantees: - **No Runtime Overhead**: No garbage collection pauses - **Deterministic Behavior**: Predictable resource management - **Explicit Control**: Developer has full control over resource lifecycle ## Core Ownership Rules **Principle 2.1 (Move Ownership Rules)** Move enforces three fundamental ownership rules: 1. **Single Owner**: Each value has exactly one owner 2. **Move Semantics**: When a value is assigned or passed to a function, ownership is transferred 3. **Explicit Destruction**: Values must be explicitly destroyed when no longer needed ### Rule 1: Single Owner ```move // Each value has exactly one owner let x = 5; // x owns the value 5 let y = x; // Ownership of 5 moves from x to y // x is no longer valid here - cannot be used ``` **Theorem 2.3 (Single Owner Invariant)** At any point in program execution, each value has exactly one owner, preventing: - Multiple mutable references to the same data - Data races in concurrent code - Use-after-free errors ### Rule 2: Move Semantics **Definition 2.3 (Move Operation)** A move operation transfers ownership of a value from one variable to another, invalidating the original variable. ```move struct Resource has key { value: u64, } fun transfer_ownership(resource: Resource) -> Resource { // resource is moved into this function // The caller no longer owns it resource // Return transfers ownership back } fun example() { let r = Resource { value: 42 }; let r2 = transfer_ownership(r); // r is moved, r2 now owns the resource // r is no longer valid here } ``` ### Rule 3: Explicit Destruction **Definition 2.4 (Destruction)** Values must be explicitly destroyed when they go out of scope or are no longer needed. ```move fun destroy_example() { let resource = Resource { value: 100 }; // At the end of this function, resource must be destroyed // Move will enforce this at compile time } ``` ## Ownership and References **Definition 2.5 (Reference)** A reference is a borrowed view of a value that does not transfer ownership. ```move fun reference_example() { let x = 5; let y = &x; // y is a reference to x, x still owns the value // x is still valid here } ``` **Theorem 2.4 (Reference Rules)** References in Move follow strict rules: 1. **Immutable References**: Multiple immutable references can exist simultaneously 2. **Mutable References**: Only one mutable reference can exist at a time 3. **No Dangling References**: References cannot outlive the value they reference ### Immutable References ```move fun immutable_references() { let x = 10; let ref1 = &x; // Immutable reference let ref2 = &x; // Another immutable reference // Both ref1 and ref2 can be used simultaneously } ``` ### Mutable References ```move fun mutable_references() { let mut x = 10; let ref1 = &mut x; // Mutable reference // let ref2 = &mut x; // Error: cannot borrow x as mutable more than once // let ref3 = &x; // Error: cannot borrow x as immutable while borrowed as mutable } ``` ## Ownership in Practice ### Function Parameters and Return Values **Algorithm 2.1 (Ownership Transfer in Functions)** ``` 1. When a value is passed to a function: - Ownership is transferred to the function - The caller no longer owns the value 2. When a value is returned from a function: - Ownership is transferred to the caller - The function no longer owns the value 3. If a value is not returned: - It must be destroyed within the function ``` ```move fun take_ownership(resource: Resource) { // resource is owned by this function // It will be destroyed when the function ends } fun give_ownership() -> Resource { let resource = Resource { value: 42 }; resource // Ownership is transferred to the caller } fun borrow_reference(resource: &Resource) { // resource is borrowed, not owned // The caller still owns the original value } ``` ### Structs and Ownership **Definition 2.6 (Struct Ownership)** When a struct is moved, all of its fields are moved with it. ```move struct Person has key { name: vector, age: u64, } fun struct_ownership() { let person = Person { name: b"Alice", age: 30, }; let person2 = person; // person is moved to person2 // person is no longer valid } ``` ## Common Ownership Patterns ### Clone When Needed **Definition 2.7 (Clone Operation)** Creating a copy of a value when you need to use it in multiple places. ```move fun clone_example() { let x = 5; let y = x; // x is moved to y let z = y; // y is moved to z // Need to clone if we want multiple copies } ``` ### Borrow Instead of Move **Principle 2.2 (Borrowing Strategy)** When possible, borrow values instead of taking ownership to avoid unnecessary moves. ```move fun process_data(data: &vector) { // Process data without taking ownership } fun caller() { let data = b"Hello, World!"; process_data(&data); // Borrow data // data is still valid here } ``` ## Ownership and Abilities **Theorem 2.5 (Ownership and Abilities Relationship)** The ownership system works in conjunction with the abilities system: - **copy ability**: Allows values to be copied instead of moved - **drop ability**: Allows values to be automatically dropped - **key ability**: Allows values to be stored as top-level resources - **store ability**: Allows values to be stored inside other values ```move struct CopyableValue has copy, drop { value: u64, } fun copyable_example() { let x = CopyableValue { value: 10 }; let y = x; // x is copied to y, both are valid let z = x; // x is copied to z, all three are valid } ``` ## Best Practices **Principle 2.3 (Ownership Best Practices)** 1. **Prefer Borrowing**: Use references when you don't need ownership 2. **Explicit Moves**: Make ownership transfers explicit and clear 3. **Minimize Copies**: Avoid unnecessary copying of large data structures 4. **Plan Resource Lifecycle**: Think about when resources should be created and destroyed 5. **Use Abilities Appropriately**: Only add abilities when necessary ## Common Pitfalls **Warning 2.1 (Ownership Mistakes)** Common mistakes to avoid: 1. **Trying to Use Moved Values**: Using variables after they've been moved 2. **Multiple Mutable References**: Creating multiple mutable references to the same data 3. **Dangling References**: Creating references that outlive their referents 4. **Forgetting to Destroy**: Not properly destroying resources 5. **Unnecessary Moves**: Moving values when borrowing would suffice ## Conclusion Ownership is a fundamental concept in Move that provides powerful safety guarantees. While it may seem restrictive at first, understanding and working with the ownership system leads to safer, more predictable code. The ownership system, combined with Move's type system and abilities, creates a robust foundation for building secure smart contracts. The next chapters will explore how ownership interacts with other Move features and provide practical examples of ownership patterns. ## Exercises 1. **Exercise 2.1**: Create a function that takes ownership of a resource and returns it 2. **Exercise 2.2**: Implement a function that borrows a value instead of taking ownership 3. **Exercise 2.3**: Identify ownership violations in a given Move program 4. **Exercise 2.4**: Design a struct that demonstrates different ownership patterns ## References 1. Move Language Specification 2. Move Ownership and References Guide 3. Smart Contract Security Best Practices 4. Move Prover and Formal Verification # Ownership Basics Understanding the basics of ownership is crucial for writing Move code. This section covers the fundamental rules and concepts that govern how ownership works in Move. ## What is Ownership? Ownership is Move's most distinctive feature and the key to its memory safety guarantees. The ownership system consists of a set of rules that the compiler checks at compile time. No runtime overhead is incurred for any of the ownership features. ## The Rules of Ownership 1. **Each value has a variable that's called its owner** 2. **There can only be one owner at a time** 3. **When the owner goes out of scope, the value will be dropped** Let's explore these rules through examples. ## Variable Scope A scope is the range within a program for which an item is valid. Let's look at an example: ```move module my_module::scope_example { public fun scope_demo() { // s is not valid here, it's not yet declared let s = string::utf8(b"hello"); // s is valid from this point forward // do stuff with s let len = string::length(&s); } // this scope is now over, and s is no longer valid } ``` In other words, there are two important points in time here: - When `s` comes into scope, it is valid - It remains valid until it goes out of scope ## The String Type To illustrate the rules of ownership, we need a more complex data type than the ones we covered in the previous sections. The types covered previously are all a known size, can be stored on the stack and popped off the stack when their scope is over, and can be quickly and trivially copied to make a new, independent instance if another part of code needs to use the same value in a different scope. But we want to look at data that is stored on the heap and explore how Move knows when to clean up that data, and the `String` type is a perfect example. We'll concentrate on the parts of `String` that relate to ownership. These aspects also apply to other complex data types, whether they are provided by the standard library or created by you. ## Memory and Allocation In the case of a string literal, we know the contents at compile time, so the text is hardcoded directly into the final executable. This is why string literals are fast and efficient. But these properties only come from the string literal's immutability. Unfortunately, we can't put a blob of memory into the binary for each piece of text whose size is unknown at compile time and whose size might change while running the program. With the `String` type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the contents. This means: - The memory must be requested from the memory allocator at runtime - We need a way of returning this memory to the allocator when we're done with our `String` The first part is done by us: when we call `string::utf8()`, its implementation requests the memory it needs. This is pretty much universal in programming languages. However, the second part is different. In languages with a garbage collector (GC), the GC keeps track of and cleans up memory that isn't being used anymore, and we don't need to think about it. In most languages without a GC, it's our responsibility to identify when memory is no longer being used and call code to explicitly return it, just as we did to request it. Doing this correctly has historically been a difficult programming problem. If we forget, we'll waste memory. If we do it too early, we'll have an invalid variable. If we do it twice, that's a bug too. We need to pair exactly one `allocate` with exactly one `free`. Move takes a different path: the memory is automatically returned once the variable that owns it goes out of scope. Here's a version of our scope example using a `String` instead of a string literal: ```move module my_module::string_scope { public fun string_scope_demo() { let s = string::utf8(b"hello"); // s comes into scope // do stuff with s let len = string::length(&s); } // this scope is now over, and s is no longer valid } ``` There is a natural point at which we can return the memory our `String` needs to the allocator: when `s` goes out of scope. When a variable goes out of scope, Move calls a special function for us. This function is called `drop`, and it's where the author of `String` can put the code to return the memory. Move calls `drop` automatically at the closing curly bracket. ## Ways Variables and Data Interact: Move Multiple variables can interact with the same data in different ways in Move. Let's look at an example using an integer: ```move module my_module::move_example { public fun move_demo() { let x = 5; let y = x; } } ``` We can probably guess what this is doing: "bind the value `5` to `x`; then make a copy of the value in `x` and bind it to `y`." We now have two variables, `x` and `y`, and both equal `5`. This is indeed what is happening, because integers are simple values with a known, fixed size, and these two `5` values are pushed onto the stack. Now let's look at the `String` version: ```move module my_module::string_move { public fun string_move_demo() { let s1 = string::utf8(b"hello"); let s2 = s1; } } ``` This looks very similar to the previous code, so we might assume that the second line would make a copy of the value in `s1` and bind it to `s2`. But this isn't quite what happens. To explain what happens, we need to look at what a `String` looks like under the hood. A `String` is made up of three parts, shown on the left: a pointer to the memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. On the right is the memory on the heap that holds the contents. When we assign `s1` to `s2`, the `String` data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to. In other words, the data representation in memory looks like this: ``` s1: [ptr | len | cap] -> "hello" (on heap) s2: [ptr | len | cap] -> "hello" (on heap) ``` The representation is not like this, which is what memory would look like if Move instead copied the heap data as well: ``` s1: [ptr | len | cap] -> "hello" (on heap) s2: [ptr | len | cap] -> "hello" (on heap) // This is NOT what happens ``` If Move had done this, the operation `let s2 = s1;` could be very expensive in terms of runtime performance if the data on the heap was large. Earlier, we said that when a variable goes out of scope, Move automatically calls the `drop` function and cleans up the heap memory for that variable. But Figure 4-2 shows both data pointers pointing to the same location. This is a problem: when `s2` and `s1` go out of scope, they will both try to free the same memory. This is known as a double free error and is one of the memory safety bugs we mentioned previously. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities. To ensure memory safety, after the line `let s2 = s1;`, Move considers `s1` as no longer valid. Therefore, Move doesn't need to free anything when `s1` goes out of scope. Check out what happens when you try to use `s1` after `s2` is created; it won't work: ```move module my_module::invalid_use { public fun invalid_use_demo() { let s1 = string::utf8(b"hello"); let s2 = s1; // This would cause a compilation error: // let len = string::length(&s1); } } ``` If you've heard the terms shallow copy and deep copy while working with other languages, the concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Move also invalidates the first variable, instead of being called a shallow copy, it's known as a move. In this example, we would say that `s1` was moved into `s2`. So what actually happens is shown in Figure 4-4. That solves our problem! With only `s2` valid, when it goes out of scope, it alone will free the memory, and we're done. In addition, there's a design choice that's implied by this: Move will never automatically create "deep" copies of your data. Therefore, any automatic copying can be assumed to be inexpensive in terms of runtime performance. ## Variables and Data Interacting with Clone If we do want to deeply copy the heap data of the `String`, not just the stack data, we can use a common method called `clone`. We'll discuss method syntax in Chapter 5, but because methods are a common feature in many programming languages, you've probably seen them before. Here's an example of the `clone` method in action: ```move module my_module::clone_example { public fun clone_demo() { let s1 = string::utf8(b"hello"); let s2 = string::clone(&s1); // Now both s1 and s2 are valid let len1 = string::length(&s1); let len2 = string::length(&s2); } } ``` This works just fine and explicitly produces the behavior shown in Figure 4-3, where the heap data does get copied. When you see a call to `clone`, you know that some arbitrary code is being executed and that code may be expensive. It's a visual indicator that something different is going on. ## Stack-Only Data: Copy There's another wrinkle we haven't talked about yet. This code using integers works and is valid: ```move module my_module::copy_example { public fun copy_demo() { let x = 5; let y = x; // Both x and y are valid here let sum = x + y; } } ``` But this code seems to contradict what we just learned: we don't have a call to `clone`, but `x` is still valid and wasn't moved into `y`. The reason is that types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. That means there's no reason we would want to prevent `x` from being valid after we create the variable `y`. In other words, there's no difference between deep and shallow copying here, so calling `clone` wouldn't create anything different from the usual shallow copying, and we can leave it out. Move has a special annotation called the `copy` ability that we can place on types like integers that are stored on the stack (we'll talk more about abilities in Chapter 10). If a type has the `copy` ability, an older variable is still usable after assignment. Move won't let us annotate a type with `copy` if the type, or any of its parts, has implemented the `drop` ability. If the type needs something special to happen when the value goes out of scope and we add the `copy` annotation to that type, we'll get a compile-time error. So what types have the `copy` ability? You can check the documentation for the given type to be sure, but as a general rule, any group of simple scalar values can have `copy`, and nothing that requires allocation or is some form of resource can have `copy`. Here are some types that have `copy`: - All the integer types, such as `u32` - The Boolean type, `bool` - The address type, `address` - All floating point types, such as `f64` - Tuples, if they only contain types that also have `copy`. For example, `(u32, u64)` has `copy`, but `(u32, String)` does not ## Ownership and Functions The semantics for passing a value to a function are similar to those for assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does. Here's an example with some annotations showing where variables go into and out of scope: ```move module my_module::function_ownership { public fun function_ownership_demo() { let s = string::utf8(b"hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // x would move into the function, // but u64 is Copy, so it's okay to still // use x afterward } // Here, x goes out of scope, then s. But because s's value was moved, nothing // special happens. fun takes_ownership(some_string: String) { // some_string comes into scope let len = string::length(&some_string); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fun makes_copy(some_integer: u64) { // some_integer comes into scope let doubled = some_integer * 2; } // Here, some_integer goes out of scope. Nothing special happens. } ``` If we tried to use `s` after the call to `takes_ownership`, Move would throw a compile-time error. These static checks protect us from mistakes. Try adding code to `main` that uses `s` and `x` to see where you can use them and where the ownership rules prevent you from doing so. ## Return Values and Scope Returning values can also transfer ownership. Here's an example with similar annotations: ```move module my_module::return_ownership { public fun return_ownership_demo() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = string::utf8(b"hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fun gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = string::utf8(b"hello"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns one fun takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function } } ``` The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it. When a variable that includes data on the heap goes out of scope, the value will be cleaned up by `drop` unless ownership of the data has been moved to another variable. While this works, taking ownership and then returning ownership with every function is a bit tedious. What if we want to let a function use a value but not take ownership? It's quite annoying that anything we pass in also needs to be passed back if we want to use it again, in addition to any data resulting from the body of the function that we might want to return as well. It's possible to return multiple values using tuples, like this: ```move module my_module::tuple_return { public fun tuple_return_demo() { let s1 = string::utf8(b"hello"); let (s2, len) = calculate_length(s1); } fun calculate_length(s: String) -> (String, u64) { let length = string::length(&s); // len() returns the length of a String (s, length) // return both the String and the length } } ``` But this is too much ceremony and a lot of work for a concept that should be common. Luckily for us, Move has a feature for using a value without transferring ownership, called references. # Move Types and Ownership Understanding how different Move types interact with ownership is crucial for writing correct and efficient code. This section explores how various types behave with respect to ownership, copying, and moving. ## Type Abilities and Ownership Move's type system is built around abilities that determine how types can be used. These abilities directly affect ownership behavior: - **`copy`**: The type can be duplicated - **`drop`**: The type can be destroyed - **`store`**: The type can be stored in global storage - **`key`**: The type can be used as a key in global storage ## Primitive Types Primitive types in Move have simple ownership behavior because they are stored on the stack and have known sizes. ### Integer Types All integer types (`u8`, `u16`, `u32`, `u64`, `u128`, `u256`) have the `copy` and `drop` abilities: ```move module my_module::integer_ownership { public fun integer_demo() { let x: u64 = 42; let y = x; // Copy - x is still valid // Both x and y are valid let sum = x + y; // Both are automatically dropped at end of function } } ``` ### Boolean Type The `bool` type has `copy` and `drop` abilities: ```move module my_module::boolean_ownership { public fun boolean_demo() { let flag = true; let flag_copy = flag; // Copy // Both flags are valid let result = flag && flag_copy; } } ``` ### Address Type The `address` type has `copy` and `drop` abilities: ```move module my_module::address_ownership { public fun address_demo() { let addr1 = @0x1; let addr2 = addr1; // Copy // Both addresses are valid let is_equal = addr1 == addr2; } } ``` ## Vector Type The `vector` type has `copy`, `drop`, and `store` abilities, but its behavior depends on the element type: ```move module my_module::vector_ownership { public fun vector_demo() { // Vector of copyable types let numbers = vector[1, 2, 3, 4, 5]; let numbers_copy = numbers; // Copy - both are valid // Vector of non-copyable types (like String) let strings = vector[ string::utf8(b"hello"), string::utf8(b"world") ]; let strings_moved = strings; // Move - strings is no longer valid // This would cause a compilation error: // let len = vector::length(&strings); } } ``` ## String Type The `String` type has `copy`, `drop`, and `store` abilities, but copying can be expensive: ```move module my_module::string_ownership { public fun string_demo() { let s1 = string::utf8(b"hello"); let s2 = s1; // Move - s1 is no longer valid // This would cause a compilation error: // let len = string::length(&s1); // s2 is still valid let len = string::length(&s2); } public fun string_copy_demo() { let s1 = string::utf8(b"hello"); let s2 = string::clone(&s1); // Explicit copy - both are valid // Both strings are valid let len1 = string::length(&s1); let len2 = string::length(&s2); } } ``` ## Struct Types Struct ownership behavior depends on the abilities declared for the struct: ### Copyable Structs ```move module my_module::copyable_struct { struct Point has copy, drop, store { x: u64, y: u64, } public fun copyable_demo() { let p1 = Point { x: 10, y: 20 }; let p2 = p1; // Copy - p1 is still valid // Both points are valid let sum_x = p1.x + p2.x; let sum_y = p1.y + p2.y; } } ``` ### Non-Copyable Structs ```move module my_module::non_copyable_struct { struct Message has drop, store { content: String, timestamp: u64, } public fun non_copyable_demo() { let msg1 = Message { content: string::utf8(b"Hello"), timestamp: 1234567890, }; let msg2 = msg1; // Move - msg1 is no longer valid // This would cause a compilation error: // let content = msg1.content; // msg2 is still valid let content = msg2.content; } } ``` ### Resource Structs Resources are special structs that cannot be copied or dropped: ```move module my_module::resource_struct { struct Coin has key, store { value: u64, } public fun resource_demo() { let coin = Coin { value: 100 }; // Resources cannot be copied // let coin_copy = coin; // This would cause a compilation error // Resources cannot be dropped automatically // They must be explicitly moved to global storage or returned // Move to global storage // move_to(account, coin); // Or return the resource coin } } ``` ## Tuple Types Tuple ownership behavior depends on the types of its elements: ```move module my_module::tuple_ownership { public fun tuple_demo() { // Tuple with all copyable elements let t1 = (42, true, @0x1); let t2 = t1; // Copy - t1 is still valid // Tuple with non-copyable elements let t3 = (string::utf8(b"hello"), 42); let t4 = t3; // Move - t3 is no longer valid // This would cause a compilation error: // let (s, n) = t3; // t4 is still valid let (s, n) = t4; } } ``` ## Reference Types References provide controlled access to values without transferring ownership: ### Immutable References ```move module my_module::immutable_references { public fun immutable_ref_demo() { let s = string::utf8(b"hello"); let len = calculate_length(&s); // Pass immutable reference // s is still valid let is_empty = string::is_empty(&s); } fun calculate_length(s: &String): u64 { string::length(s) // Can read but not modify } } ``` ### Mutable References ```move module my_module::mutable_references { public fun mutable_ref_demo() { let mut s = string::utf8(b"hello"); append_world(&mut s); // Pass mutable reference // s is still valid and has been modified let len = string::length(&s); } fun append_world(s: &mut String) { *s = *s + string::utf8(b" world"); // Can modify the value } } ``` ## Global Storage and Ownership Global storage in Move has special ownership rules: ### Storing Resources ```move module my_module::global_storage { struct UserProfile has key, store { name: String, age: u8, } public fun store_profile(account: &signer, name: String, age: u8) { let profile = UserProfile { name, age }; move_to(account, profile); // Transfer ownership to global storage } public fun get_profile_name(addr: address): String { let profile = borrow_global(addr); profile.name // Return a copy of the name } public fun update_profile_age(addr: address, new_age: u8) { let profile = borrow_global_mut(addr); profile.age = new_age; // Modify the stored value } } ``` ### Moving Resources Out of Storage ```move module my_module::move_from_storage { struct Coin has key, store { value: u64, } public fun withdraw_coin(account: &signer, amount: u64): Coin { let coin = move_from(signer::address_of(account)); // Split the coin if needed if (coin.value > amount) { let (withdrawn, remaining) = split_coin(coin, amount); // Return the remaining coin to storage move_to(account, remaining); withdrawn } else { coin } } fun split_coin(coin: Coin, amount: u64): (Coin, Coin) { // Implementation of coin splitting // This is a simplified example (Coin { value: amount }, Coin { value: coin.value - amount }) } } ``` ## Type Abilities and Constraints Understanding abilities helps you design types with the right ownership behavior: ### Designing Copyable Types ```move module my_module::copyable_design { // Good: All fields are copyable struct Config has copy, drop, store { max_retries: u8, timeout: u64, enabled: bool, } // Bad: Contains non-copyable field // struct BadConfig has copy, drop, store { // max_retries: u8, // name: String, // String doesn't have copy ability // } } ``` ### Designing Resource Types ```move module my_module::resource_design { // Good: Resource with key and store abilities struct Token has key, store { id: u64, owner: address, metadata: String, } // Bad: Resource with copy ability (contradiction) // struct BadToken has key, store, copy { // id: u64, // owner: address, // } } ``` ## Ownership Patterns ### Value Semantics ```move module my_module::value_semantics { struct Point has copy, drop, store { x: u64, y: u64, } public fun value_demo() { let p1 = Point { x: 10, y: 20 }; let p2 = p1; // Copy - value semantics // Modifying p2 doesn't affect p1 p2.x = 30; // p1.x is still 10 let x1 = p1.x; let x2 = p2.x; // 30 } } ``` ### Reference Semantics ```move module my_module::reference_semantics { struct Counter has key, store { value: u64, } public fun reference_demo(account: &signer) { let counter = Counter { value: 0 }; move_to(account, counter); // All operations work on the same instance increment_counter(signer::address_of(account)); increment_counter(signer::address_of(account)); let final_value = get_counter_value(signer::address_of(account)); // final_value is 2 } fun increment_counter(addr: address) { let counter = borrow_global_mut(addr); counter.value = counter.value + 1; } fun get_counter_value(addr: address): u64 { let counter = borrow_global(addr); counter.value } } ``` ## Best Practices ### Choose Appropriate Abilities ```move module my_module::ability_best_practices { // Use copy for small, simple data struct SmallData has copy, drop, store { id: u64, flag: bool, } // Use drop for temporary data struct TempData has drop { buffer: vector, } // Use store for data that needs persistence struct PersistentData has key, store { data: String, } // Use key for resources struct Resource has key, store { value: u64, } } ``` ### Avoid Unnecessary Copies ```move module my_module::avoid_copies { public fun efficient_processing(data: &vector): u64 { let sum = 0u64; let i = 0; let len = vector::length(data); while (i < len) { sum = sum + *vector::borrow(data, i); i = i + 1; }; sum } // Less efficient - creates unnecessary copies // public fun inefficient_processing(data: vector): u64 { // // data is moved into the function // // and would need to be returned if needed elsewhere // } } ``` ### Use References for Read-Only Access ```move module my_module::reference_best_practices { public fun read_only_operations(data: &String): (u64, bool) { let length = string::length(data); let is_empty = string::is_empty(data); (length, is_empty) } public fun modify_data(data: &mut String) { *data = *data + string::utf8(b" modified"); } } ``` Understanding how different types interact with ownership is essential for writing efficient and correct Move code. The ability system provides a clear way to express ownership semantics, and following best practices helps you avoid common pitfalls. # Transfer and Borrowing Understanding how to transfer ownership and borrow values is essential for writing efficient Move code. This section covers the mechanics of ownership transfer and the borrowing system that allows controlled access to values without transferring ownership. ## Ownership Transfer Ownership transfer occurs when a value is moved from one owner to another. This is the default behavior in Move for most types. ### Basic Ownership Transfer ```move module my_module::basic_transfer { public fun basic_transfer_demo() { let s1 = string::utf8(b"hello"); let s2 = s1; // Ownership transferred from s1 to s2 // s1 is no longer valid // s2 is now the owner let len = string::length(&s2); } // s2 goes out of scope and is dropped } ``` ### Transfer in Function Calls ```move module my_module::function_transfer { public fun function_transfer_demo() { let s = string::utf8(b"hello"); // Ownership transferred to the function takes_ownership(s); // s is no longer valid here // This would cause a compilation error: // let len = string::length(&s); } fun takes_ownership(some_string: String) { // some_string is now owned by this function let len = string::length(&some_string); } // some_string goes out of scope and is dropped } ``` ### Transfer with Return Values ```move module my_module::return_transfer { public fun return_transfer_demo() { let s1 = gives_ownership(); // Ownership transferred from function to s1 let s2 = string::utf8(b"world"); let s3 = takes_and_gives_back(s2); // s2 moved in, s3 moved out } fun gives_ownership() -> String { let some_string = string::utf8(b"hello"); some_string // Ownership transferred to caller } fun takes_and_gives_back(a_string: String) -> String { // a_string comes into scope a_string // Ownership transferred back to caller } } ``` ### Transfer with Structs ```move module my_module::struct_transfer { struct Person has drop, store { name: String, age: u8, } public fun struct_transfer_demo() { let person1 = Person { name: string::utf8(b"Alice"), age: 30, }; let person2 = person1; // Ownership transferred // person1 is no longer valid // person2 is now the owner let age = person2.age; } } ``` ## Borrowing Borrowing allows you to use a value without taking ownership of it. Borrowing is done using references (`&` for immutable references, `&mut` for mutable references). ### Immutable Borrowing ```move module my_module::immutable_borrowing { public fun immutable_borrow_demo() { let s = string::utf8(b"hello"); // Borrow s immutably let len = calculate_length(&s); let is_empty = string::is_empty(&s); // s is still valid and can be used let first_char = string::sub_string(&s, 0, 1); } fun calculate_length(s: &String) -> u64 { string::length(s) // Can read but not modify } } ``` ### Mutable Borrowing ```move module my_module::mutable_borrowing { public fun mutable_borrow_demo() { let mut s = string::utf8(b"hello"); // Borrow s mutably change_string(&mut s); // s is still valid and has been modified let len = string::length(&s); } fun change_string(s: &mut String) { *s = *s + string::utf8(b" world"); // Can modify the value } } ``` ### Multiple Immutable Borrows ```move module my_module::multiple_immutable_borrows { public fun multiple_immutable_demo() { let s = string::utf8(b"hello"); // Multiple immutable borrows are allowed let len1 = string::length(&s); let len2 = string::length(&s); let is_empty = string::is_empty(&s); // All borrows can be used simultaneously let result = len1 + len2; } } ``` ### Mutable and Immutable Borrows ```move module my_module::mutable_immutable_conflict { public fun conflict_demo() { let mut s = string::utf8(b"hello"); let len = string::length(&s); // Immutable borrow // This would cause a compilation error: // change_string(&mut s); // Mutable borrow while immutable borrow exists // But this is fine: let is_empty = string::is_empty(&s); // Another immutable borrow } fun change_string(s: &mut String) { *s = *s + string::utf8(b" world"); } } ``` ## Borrowing Rules Move enforces strict borrowing rules to prevent data races and ensure memory safety: 1. **At any given time, you can have either one mutable reference or any number of immutable references to a particular value** 2. **References must always be valid** ### Borrowing Rule Examples ```move module my_module::borrowing_rules { public fun borrowing_rules_demo() { let mut s = string::utf8(b"hello"); // Rule 1: Multiple immutable borrows allowed let r1 = &s; let r2 = &s; let r3 = &s; // All immutable borrows can be used let len1 = string::length(r1); let len2 = string::length(r2); let len3 = string::length(r3); // Rule 2: Cannot have mutable borrow while immutable borrows exist // This would cause a compilation error: // let r4 = &mut s; // Rule 3: Cannot have multiple mutable borrows // let r5 = &mut s; // let r6 = &mut s; // This would cause a compilation error } } ``` ## Dangling References Move prevents dangling references (references to data that no longer exists): ```move module my_module::dangling_references { // This would cause a compilation error - dangling reference // fun dangle() -> &String { // let s = string::utf8(b"hello"); // &s // s goes out of scope, so this reference would be invalid // } // Instead, return the value itself fun no_dangle() -> String { let s = string::utf8(b"hello"); s // Ownership transferred to caller } } ``` ## Borrowing in Structs Structs can contain references, but they must follow the borrowing rules: ```move module my_module::struct_borrowing { struct StringWrapper has drop, store { content: String, } public fun struct_borrow_demo() { let wrapper = StringWrapper { content: string::utf8(b"hello"), }; // Borrow the struct immutably let len = get_length(&wrapper); // Borrow the struct mutably append_world(&mut wrapper); // Both operations work on the same struct let final_len = string::length(&wrapper.content); } fun get_length(wrapper: &StringWrapper) -> u64 { string::length(&wrapper.content) } fun append_world(wrapper: &mut StringWrapper) { wrapper.content = wrapper.content + string::utf8(b" world"); } } ``` ## Borrowing with Collections Borrowing works with collections like vectors: ```move module my_module::collection_borrowing { public fun vector_borrow_demo() { let mut numbers = vector[1, 2, 3, 4, 5]; // Immutable borrow let sum = calculate_sum(&numbers); // Mutable borrow double_values(&mut numbers); // Both operations work on the same vector let new_sum = calculate_sum(&numbers); } fun calculate_sum(numbers: &vector) -> u64 { let sum = 0u64; let i = 0; let len = vector::length(numbers); while (i < len) { sum = sum + *vector::borrow(numbers, i); i = i + 1; }; sum } fun double_values(numbers: &mut vector) { let i = 0; let len = vector::length(numbers); while (i < len) { let value = *vector::borrow(numbers, i); *vector::borrow_mut(numbers, i) = value * 2; i = i + 1; }; } } ``` ## Borrowing in Global Storage Borrowing is essential for working with global storage: ```move module my_module::global_storage_borrowing { struct Counter has key, store { value: u64, } public fun global_borrow_demo(account: &signer) { let counter = Counter { value: 0 }; move_to(account, counter); let addr = signer::address_of(account); // Immutable borrow from global storage let current_value = get_counter_value(addr); // Mutable borrow from global storage increment_counter(addr); // Verify the change let new_value = get_counter_value(addr); } fun get_counter_value(addr: address) -> u64 { let counter = borrow_global(addr); counter.value } fun increment_counter(addr: address) { let counter = borrow_global_mut(addr); counter.value = counter.value + 1; } } ``` ## Borrowing Patterns ### Read-Only Operations ```move module my_module::read_only_pattern { public fun read_only_demo(data: &vector) -> (u64, u64, bool) { let sum = calculate_sum(data); let average = calculate_average(data); let is_empty = vector::is_empty(data); (sum, average, is_empty) } fun calculate_sum(data: &vector) -> u64 { let sum = 0u64; let i = 0; let len = vector::length(data); while (i < len) { sum = sum + *vector::borrow(data, i); i = i + 1; }; sum } fun calculate_average(data: &vector) -> u64 { let sum = calculate_sum(data); let len = vector::length(data); if (len == 0) { 0 } else { sum / len } } } ``` ### Modification Operations ```move module my_module::modification_pattern { public fun modification_demo(data: &mut vector) { // Sort the data sort_vector(data); // Remove duplicates remove_duplicates(data); // Double all values double_all(data); } fun sort_vector(data: &mut vector) { // Implementation of sorting // This is a simplified example } fun remove_duplicates(data: &mut vector) { // Implementation of duplicate removal // This is a simplified example } fun double_all(data: &mut vector) { let i = 0; let len = vector::length(data); while (i < len) { let value = *vector::borrow(data, i); *vector::borrow_mut(data, i) = value * 2; i = i + 1; }; } } ``` ## Best Practices ### Prefer Borrowing Over Transfer ```move module my_module::borrowing_best_practices { // Good: Use borrowing for read-only access public fun efficient_processing(data: &vector) -> u64 { let sum = 0u64; let i = 0; let len = vector::length(data); while (i < len) { sum = sum + *vector::borrow(data, i); i = i + 1; }; sum } // Less efficient: Transfer ownership // public fun inefficient_processing(data: vector) -> (vector, u64) { // let sum = calculate_sum(&data); // (data, sum) // Must return data to avoid moving // } } ``` ### Use Appropriate Reference Types ```move module my_module::reference_types { // Use immutable references for read-only access public fun read_operations(data: &String) -> (u64, bool) { let length = string::length(data); let is_empty = string::is_empty(data); (length, is_empty) } // Use mutable references only when modification is needed public fun modification_operations(data: &mut String) { *data = *data + string::utf8(b" modified"); } } ``` ### Avoid Borrowing Conflicts ```move module my_module::avoid_conflicts { public fun avoid_conflict_demo() { let mut data = vector[1, 2, 3, 4, 5]; // Good: Use immutable borrows first let sum = calculate_sum(&data); let average = calculate_average(&data); // Then use mutable borrow modify_data(&mut data); // Now can use immutable borrows again let new_sum = calculate_sum(&data); } fun calculate_sum(data: &vector) -> u64 { // Implementation 0 } fun calculate_average(data: &vector) -> u64 { // Implementation 0 } fun modify_data(data: &mut vector) { // Implementation } } ``` Understanding ownership transfer and borrowing is fundamental to writing safe and efficient Move code. The borrowing system provides a powerful way to control access to data while maintaining Move's safety guarantees. # Reference Restrictions Move's reference system includes strict restrictions that prevent common programming errors like data races, dangling references, and use-after-free bugs. Understanding these restrictions is crucial for writing safe Move code. ## The Borrowing Rules Move enforces three fundamental borrowing rules that must be followed at all times: 1. **At any given time, you can have either one mutable reference or any number of immutable references to a particular value** 2. **References must always be valid** 3. **References cannot outlive the data they refer to** ## Rule 1: Mutable vs Immutable References This rule prevents data races by ensuring that data cannot be modified while it's being read. ### Multiple Immutable References Allowed ```move module my_module::multiple_immutable { public fun multiple_immutable_demo() { let s = string::utf8(b"hello"); // Multiple immutable references are allowed let r1 = &s; let r2 = &s; let r3 = &s; // All can be used simultaneously let len1 = string::length(r1); let len2 = string::length(r2); let len3 = string::length(r3); // This is safe because no one can modify the data let total = len1 + len2 + len3; } } ``` ### Only One Mutable Reference ```move module my_module::single_mutable { public fun single_mutable_demo() { let mut s = string::utf8(b"hello"); // Only one mutable reference allowed let r1 = &mut s; // This would cause a compilation error: // let r2 = &mut s; // Second mutable reference // Use the mutable reference *r1 = *r1 + string::utf8(b" world"); } } ``` ### Mutable and Immutable Reference Conflict ```move module my_module::mutable_immutable_conflict { public fun conflict_demo() { let mut s = string::utf8(b"hello"); // Create immutable reference let r1 = &s; let len = string::length(r1); // This would cause a compilation error: // let r2 = &mut s; // Mutable reference while immutable exists // But this is fine - another immutable reference let r3 = &s; let is_empty = string::is_empty(r3); } } ``` ## Rule 2: References Must Always Be Valid Move prevents dangling references by ensuring references always point to valid data. ### Preventing Dangling References ```move module my_module::dangling_prevention { // This would cause a compilation error - dangling reference // fun dangle() -> &String { // let s = string::utf8(b"hello"); // &s // s goes out of scope, making this reference invalid // } // Instead, return the value itself fun no_dangle() -> String { let s = string::utf8(b"hello"); s // Ownership transferred to caller } // Or return a reference to data that lives long enough struct StringHolder has key, store { content: String, } fun get_reference(holder: &StringHolder) -> &String { &holder.content // Reference to data that outlives the function } } ``` ### Reference Lifetime in Structs ```move module my_module::struct_lifetime { // This would cause a compilation error - reference in struct // struct StringRef { // content: &String, // Reference without lifetime // } // Instead, store the value itself struct StringWrapper has drop, store { content: String, } // Or use a different design pattern struct StringHolder has key, store { content: String, } fun create_wrapper(s: String) -> StringWrapper { StringWrapper { content: s } } } ``` ## Rule 3: References Cannot Outlive Data This rule ensures that references are always valid throughout their lifetime. ### Scope and Lifetime ```move module my_module::scope_lifetime { public fun scope_demo() { let mut s = string::utf8(b"hello"); { let r = &mut s; // Mutable reference to s *r = *r + string::utf8(b" world"); } // r goes out of scope here // s is still valid and can be used let len = string::length(&s); } } ``` ### Function Parameter Lifetimes ```move module my_module::function_lifetime { public fun function_demo() { let s = string::utf8(b"hello"); // Pass reference to function let len = calculate_length(&s); // s is still valid after the function call let is_empty = string::is_empty(&s); } fun calculate_length(s: &String) -> u64 { string::length(s) // Reference is valid for the duration of the function } } ``` ## Reference Restrictions in Practice ### Working with Collections ```move module my_module::collection_restrictions { public fun vector_restrictions() { let mut numbers = vector[1, 2, 3, 4, 5]; // Immutable borrow let sum = calculate_sum(&numbers); // Mutable borrow double_values(&mut numbers); // Can use immutable borrow again after mutable borrow ends let new_sum = calculate_sum(&numbers); } fun calculate_sum(numbers: &vector) -> u64 { let sum = 0u64; let i = 0; let len = vector::length(numbers); while (i < len) { sum = sum + *vector::borrow(numbers, i); i = i + 1; }; sum } fun double_values(numbers: &mut vector) { let i = 0; let len = vector::length(numbers); while (i < len) { let value = *vector::borrow(numbers, i); *vector::borrow_mut(numbers, i) = value * 2; i = i + 1; }; } } ``` ### Global Storage Restrictions ```move module my_module::global_storage_restrictions { struct Counter has key, store { value: u64, } public fun global_storage_demo(account: &signer) { let counter = Counter { value: 0 }; move_to(account, counter); let addr = signer::address_of(account); // Immutable borrow from global storage let current_value = get_counter_value(addr); // Mutable borrow from global storage increment_counter(addr); // Can use immutable borrow again let new_value = get_counter_value(addr); } fun get_counter_value(addr: address) -> u64 { let counter = borrow_global(addr); counter.value // Return copy of value, not reference } fun increment_counter(addr: address) { let counter = borrow_global_mut(addr); counter.value = counter.value + 1; } } ``` ## Common Reference Errors and Solutions ### Error: Multiple Mutable References ```move module my_module::multiple_mutable_error { public fun multiple_mutable_error() { let mut s = string::utf8(b"hello"); // This would cause a compilation error: // let r1 = &mut s; // let r2 = &mut s; // Second mutable reference // Solution: Use one mutable reference at a time { let r1 = &mut s; *r1 = *r1 + string::utf8(b" world"); } // r1 goes out of scope { let r2 = &mut s; *r2 = *r2 + string::utf8(b"!"); } // r2 goes out of scope } } ``` ### Error: Mutable and Immutable Reference Conflict ```move module my_module::conflict_error { public fun conflict_error() { let mut s = string::utf8(b"hello"); // This would cause a compilation error: // let r1 = &s; // Immutable reference // let r2 = &mut s; // Mutable reference while immutable exists // Solution: Use immutable references first, then mutable let len = string::length(&s); let is_empty = string::is_empty(&s); // Now use mutable reference let r2 = &mut s; *r2 = *r2 + string::utf8(b" world"); } } ``` ### Error: Dangling Reference ```move module my_module::dangling_error { // This would cause a compilation error: // fun dangling_reference() -> &String { // let s = string::utf8(b"hello"); // &s // s goes out of scope, making reference invalid // } // Solution: Return the value itself fun return_value() -> String { let s = string::utf8(b"hello"); s // Ownership transferred } // Or use a different design pattern struct StringHolder has key, store { content: String, } fun return_reference_to_stored_data(addr: address) -> &String { let holder = borrow_global(addr); &holder.content // Reference to data that lives in global storage } } ``` ## Reference Restrictions and Performance ### Avoiding Unnecessary Copies ```move module my_module::performance_restrictions { // Good: Use references to avoid copying public fun efficient_processing(data: &vector) -> u64 { let sum = 0u64; let i = 0; let len = vector::length(data); while (i < len) { sum = sum + *vector::borrow(data, i); i = i + 1; }; sum } // Less efficient: Copy the entire vector // public fun inefficient_processing(data: vector) -> u64 { // // data is moved into the function // // processing happens // // data would need to be returned if needed elsewhere // } } ``` ### Minimizing Borrowing Conflicts ```move module my_module::minimize_conflicts { public fun minimize_conflicts_demo() { let mut data = vector[1, 2, 3, 4, 5]; // Group immutable operations together let sum = calculate_sum(&data); let average = calculate_average(&data); let max = find_max(&data); // Then do mutable operations modify_data(&mut data); // Can use immutable operations again let new_sum = calculate_sum(&data); } fun calculate_sum(data: &vector) -> u64 { // Implementation 0 } fun calculate_average(data: &vector) -> u64 { // Implementation 0 } fun find_max(data: &vector) -> u64 { // Implementation 0 } fun modify_data(data: &mut vector) { // Implementation } } ``` ## Best Practices for Reference Restrictions ### Plan Your Borrowing Strategy ```move module my_module::borrowing_strategy { public fun borrowing_strategy_demo() { let mut data = vector[1, 2, 3, 4, 5]; // Strategy 1: Read-only operations first let analysis = analyze_data(&data); // Strategy 2: Single mutable operation modify_data(&mut data); // Strategy 3: Read-only operations after modification let new_analysis = analyze_data(&data); } fun analyze_data(data: &vector) -> (u64, u64, u64) { let sum = calculate_sum(data); let average = calculate_average(data); let max = find_max(data); (sum, average, max) } fun modify_data(data: &mut vector) { // Single modification operation double_all_values(data); } fun calculate_sum(data: &vector) -> u64 { // Implementation 0 } fun calculate_average(data: &vector) -> u64 { // Implementation 0 } fun find_max(data: &vector) -> u64 { // Implementation 0 } fun double_all_values(data: &mut vector) { // Implementation } } ``` ### Use Appropriate Reference Types ```move module my_module::appropriate_references { // Use immutable references for read-only access public fun read_operations(data: &String) -> (u64, bool) { let length = string::length(data); let is_empty = string::is_empty(data); (length, is_empty) } // Use mutable references only when modification is needed public fun modification_operations(data: &mut String) { *data = *data + string::utf8(b" modified"); } // Avoid unnecessary mutable references // public fun unnecessary_mutable(data: &mut String) -> u64 { // string::length(data) // Doesn't need mutable reference // } } ``` ### Handle Borrowing Conflicts Gracefully ```move module my_module::graceful_conflicts { public fun graceful_conflict_demo() { let mut data = vector[1, 2, 3, 4, 5]; // Extract values before modification to avoid conflicts let current_sum = calculate_sum(&data); let current_average = calculate_average(&data); // Now modify the data modify_data(&mut data); // Use the extracted values let new_sum = calculate_sum(&data); let sum_difference = new_sum - current_sum; } fun calculate_sum(data: &vector) -> u64 { // Implementation 0 } fun calculate_average(data: &vector) -> u64 { // Implementation 0 } fun modify_data(data: &mut vector) { // Implementation } } ``` Understanding and following Move's reference restrictions is essential for writing safe and correct code. These restrictions prevent common programming errors and ensure that your Move programs are memory-safe and free from data races. # Using Structs to Structure Related Data A *struct* is a custom data type that lets you package together and name multiple related values that make up a meaningful group. If you're familiar with an object-oriented language, a struct is like the data attributes of an object. In this chapter, we'll compare and contrast structs, demonstrate how to use them, and discuss how structs in Move become powerful resources that represent digital assets. ## Why Structs? When building smart contracts, you frequently need to group related data together. For example, a user profile might include a name, age, and email address. Rather than managing these as separate variables, a struct lets you combine them into a single, named type. Move structs are more powerful than structures in many other languages because of the **abilities system**. By assigning different abilities to a struct, you control whether it can be: - Stored in global storage (`key`) - Embedded within other structures (`store`) - Copied (`copy`) - Automatically destroyed (`drop`) This fine-grained control is what makes Move's type system uniquely suited for representing digital assets on the blockchain. ## What You'll Learn In this chapter, we'll cover: 1. **[Defining and Instantiating Structs](basics.md)** - How to declare structs and create instances 2. **[Structs and How They Become Resources](resources.md)** - How abilities transform structs into resources 3. **[Structs and Abilities](abilities.md)** - A deep dive into the four abilities 4. **[Example Program Using Structs](example.md)** - A practical, end-to-end example 5. **[Struct Method Syntax](methods.md)** - How to define methods on structs # Defining and Instantiating Structs Structs are the primary way to create custom data types in Move. They group related fields together under a single name, much like structs in Rust or C. ## Defining a Struct A struct is defined using the `struct` keyword followed by the struct name, optional abilities, and the fields inside curly braces. ```move module my_addr::profiles { use std::string::String; struct UserProfile has key, store { name: String, age: u8, balance: u64, } } ``` Key points about struct definitions: - The struct name must start with an uppercase letter by convention. - Each field has a name and a type, separated by a colon. - Fields are separated by commas. - Abilities (`key`, `store`, `copy`, `drop`) are declared after `has`. ## Creating Struct Instances You create a struct instance by specifying the struct name and providing values for each field. ```move module my_addr::profiles { use std::string::{Self, String}; struct UserProfile has key, store, drop { name: String, age: u8, balance: u64, } public fun create_profile(name: String, age: u8): UserProfile { UserProfile { name, age, balance: 0, } } } ``` ### Field Init Shorthand When a variable has the same name as the struct field, you can use the shorthand syntax. In the example above, `name` and `age` are used directly without writing `name: name` and `age: age`. ## Accessing Struct Fields You access struct fields using dot notation: ```move public fun get_name(profile: &UserProfile): &String { &profile.name } public fun get_age(profile: &UserProfile): u8 { profile.age } ``` ## Modifying Struct Fields To modify fields, you need a mutable reference to the struct: ```move public fun update_age(profile: &mut UserProfile, new_age: u8) { profile.age = new_age; } public fun add_balance(profile: &mut UserProfile, amount: u64) { profile.balance = profile.balance + amount; } ``` ## Destructuring Structs You can destructure a struct to extract its fields. This is required when you want to destroy a struct that does not have the `drop` ability. ```move public fun destroy_profile(profile: UserProfile): (String, u8, u64) { let UserProfile { name, age, balance } = profile; (name, age, balance) } ``` You can also partially destructure with `..` to ignore fields: ```move public fun get_balance_from_profile(profile: UserProfile): u64 { let UserProfile { balance, .. } = profile; balance } ``` ## Storing Structs in Global Storage Structs with the `key` ability can be stored in global storage under an account address: ```move module my_addr::profiles { use std::signer; use std::string::String; struct UserProfile has key, store { name: String, age: u8, balance: u64, } /// Store a profile under the caller's account public entry fun register(account: &signer, name: String, age: u8) { let profile = UserProfile { name, age, balance: 0, }; move_to(account, profile); } /// Read the profile for a given address #[view] public fun get_profile_age(addr: address): u8 acquires UserProfile { UserProfile[addr].age } } ``` ## Nested Structs Structs can contain other structs as fields: ```move module my_addr::game { struct Position has copy, drop, store { x: u64, y: u64, } struct Player has key, store { position: Position, health: u64, score: u64, } } ``` ## Best Practices 1. **Use descriptive field names**: Make field names clear and self-documenting. 2. **Minimize abilities**: Only add the abilities your struct actually needs. 3. **Group related data**: If several values are always used together, put them in a struct. 4. **Document with `///`**: Add doc comments above your struct definitions explaining their purpose. 5. **Use `key` for top-level storage**: Only structs that need to be stored directly under an address need `key`. # Structs and How They Become Resources One of the most powerful concepts in Move is the idea of a **resource** -- a struct whose usage is tightly controlled by the type system to prevent duplication or accidental loss. Resources are the foundation of safe digital asset management on Aptos. ## What is a Resource? A resource is any struct that has the `key` ability but lacks the `copy` ability. Because a resource cannot be copied, it can only exist in one place at a time, which makes it ideal for representing assets like tokens, NFTs, or game items. ```move module my_addr::vault { /// A vault that holds a balance. Because it has `key` but not `copy`, /// it is a resource -- it cannot be duplicated. struct Vault has key { balance: u64, } } ``` ## Resources vs. Regular Structs | Property | Regular Struct (`copy + drop`) | Resource (`key`, no `copy`) | |---|---|---| | Can be copied | Yes | No | | Can be implicitly dropped | Yes (with `drop`) | No | | Stored in global storage | Only with `key` | Yes | | Ideal for | Temporary data, config | Assets, tokens, game items | ## Creating Resources Resources are created like any other struct, but they must be explicitly stored or returned -- they cannot simply go out of scope. ```move module my_addr::vault { use std::signer; struct Vault has key { balance: u64, } /// Create a vault and store it under the caller's account public entry fun create_vault(account: &signer, initial_balance: u64) { let vault = Vault { balance: initial_balance }; move_to(account, vault); } } ``` ## Global Storage Operations Move provides four built-in operations for working with resources in global storage: ### `move_to` -- Store a resource Stores a resource under a signer's address. Each account can hold at most one instance of a given resource type. ```move move_to(account, Vault { balance: 100 }); ``` ### `move_from` -- Remove a resource Removes and returns a resource from global storage. This transfers ownership back to the caller. ```move let vault = move_from(addr); ``` ### `borrow_global` -- Immutable reference Borrows an immutable reference to a resource in global storage. ```move let vault_ref = borrow_global(addr); let balance = vault_ref.balance; ``` ### `borrow_global_mut` -- Mutable reference Borrows a mutable reference to a resource in global storage. ```move let vault_mut = borrow_global_mut(addr); vault_mut.balance = vault_mut.balance + 100; ``` > **Note:** The newer indexing syntax `Vault[addr]` is equivalent to `borrow_global(addr)` and `&mut Vault[addr]` is equivalent to `borrow_global_mut(addr)`. ### `exists` -- Check existence Checks whether a resource exists at a given address. ```move if (exists(addr)) { // The vault exists }; ``` ## The `acquires` Annotation Any function that accesses global storage for a resource type must declare this with the `acquires` keyword: ```move #[view] public fun get_balance(addr: address): u64 acquires Vault { Vault[addr].balance } public entry fun deposit(account: &signer, amount: u64) acquires Vault { let addr = signer::address_of(account); let vault = &mut Vault[addr]; vault.balance = vault.balance + amount; } ``` ## Destroying Resources Because resources cannot be implicitly dropped, you must explicitly destroy them through destructuring: ```move public entry fun close_vault(account: &signer) acquires Vault { let addr = signer::address_of(account); let Vault { balance: _ } = move_from(addr); } ``` ## Why Resources Matter Resources solve a fundamental problem in blockchain programming: preventing the duplication or loss of digital assets. - **No duplication**: Because resources cannot be copied, a token cannot be "double-spent" at the language level. - **No accidental loss**: Because resources without `drop` cannot be silently discarded, assets cannot be accidentally lost. - **Explicit lifecycle**: Every resource must be explicitly created, stored, transferred, or destroyed. This is what makes Move uniquely safe for financial applications compared to languages like Solidity, where asset safety relies on developer discipline rather than compiler enforcement. # Structs and Abilities Abilities are the mechanism Move uses to control what operations can be performed on a type. Every struct must explicitly declare which abilities it has. This chapter explores each ability in detail. ## The Four Abilities | Ability | What it allows | |---|---| | `copy` | The value can be duplicated | | `drop` | The value can be implicitly destroyed when it goes out of scope | | `store` | The value can be stored inside another struct in global storage | | `key` | The value can be stored directly in global storage as a top-level resource | ## `copy` -- Duplicating Values The `copy` ability allows a value to be duplicated. Without it, assigning a value to a new variable moves it, making the original invalid. ```move struct Point has copy, drop { x: u64, y: u64, } fun copy_example() { let p1 = Point { x: 10, y: 20 }; let p2 = p1; // p1 is copied, both p1 and p2 are valid let sum = p1.x + p2.x; // Works because Point has copy } ``` > A struct can only have `copy` if all of its fields also have `copy`. ## `drop` -- Implicit Destruction The `drop` ability allows a value to be automatically destroyed when it goes out of scope. Without `drop`, every value must be explicitly consumed. ```move struct TempData has drop { value: u64, } fun drop_example() { let data = TempData { value: 42 }; // data is implicitly dropped at end of scope -- no error } ``` Without `drop`, the compiler would require you to destructure or move the value: ```move struct ImportantData { value: u64, } fun no_drop_example() { let data = ImportantData { value: 42 }; let ImportantData { value: _ } = data; // Must explicitly destroy } ``` ## `store` -- Nested Storage The `store` ability allows a value to be stored as a field inside another struct that is in global storage. Without `store`, a type cannot appear inside a resource. ```move struct Metadata has store, copy, drop { name: vector, version: u64, } struct Contract has key { metadata: Metadata, // Metadata needs `store` to be here owner: address, } ``` ## `key` -- Top-Level Storage The `key` ability allows a struct to be stored directly in global storage under an address. This is what makes a struct a "resource" in the traditional Move sense. ```move struct Balance has key { amount: u64, } fun store_balance(account: &signer, amount: u64) { move_to(account, Balance { amount }); } ``` > A struct with `key` implicitly requires all its fields to have `store`. ## Common Ability Combinations ### Pure Data (Value Type) ```move /// Freely copyable and droppable -- behaves like a primitive struct Config has copy, drop, store { max_supply: u64, is_active: bool, } ``` ### Storable Asset (Resource) ```move /// Stored in global storage, cannot be copied or implicitly dropped struct Token has key, store { id: u64, value: u64, } ``` ### Temporary Object ```move /// Can be dropped but not copied or stored struct Receipt has drop { amount: u64, timestamp: u64, } ``` ### Hot Potato (No Abilities) A struct with no abilities at all is called a "hot potato." It must be consumed by some specific function, which is useful for enforcing certain patterns. ```move /// Must be consumed -- cannot be copied, dropped, or stored struct FlashLoanReceipt { amount: u64, fee: u64, } ``` ## Ability Constraints on Fields A struct's abilities are constrained by its fields: - A struct can have `copy` only if **all** fields have `copy`. - A struct can have `drop` only if **all** fields have `drop`. - A struct can have `store` only if **all** fields have `store`. - A struct can have `key` only if **all** fields have `store`. ```move // This would NOT compile because String does not have copy: // struct BadExample has copy, drop { // name: String, // String does not have copy // } ``` ## Best Practices 1. **Start with minimal abilities**: Only add what you need. 2. **Use `key` for top-level resources**: Structs stored under an address need `key`. 3. **Omit `copy` for assets**: Digital assets should not be copyable. 4. **Omit `drop` for assets that must be tracked**: Prevent accidental loss by omitting `drop`. 5. **Use hot potato pattern for receipts**: When you need to ensure a value is consumed by a specific function. # Example Program Using Structs Let's build a practical example that uses structs to create a simple on-chain address book. This will demonstrate struct definition, creation, storage, reading, updating, and destruction. ## The Address Book Contract ```move /// A simple on-chain address book that stores contact information. module my_addr::address_book { use std::signer; use std::string::String; use std::vector; /// No address book found for this account const E_NO_ADDRESS_BOOK: u64 = 1; /// Contact not found in the address book const E_CONTACT_NOT_FOUND: u64 = 2; /// Address book already exists const E_ALREADY_EXISTS: u64 = 3; /// A single contact entry struct Contact has store, drop, copy { name: String, phone: String, } /// The address book resource stored under an account struct AddressBook has key { contacts: vector, owner: address, } /// Create a new address book for the caller public entry fun create_address_book(account: &signer) { let addr = signer::address_of(account); assert!(!exists(addr), E_ALREADY_EXISTS); move_to(account, AddressBook { contacts: vector[], owner: addr, }); } /// Add a contact to the address book public entry fun add_contact( account: &signer, name: String, phone: String, ) acquires AddressBook { let addr = signer::address_of(account); assert!(exists(addr), E_NO_ADDRESS_BOOK); let book = &mut AddressBook[addr]; book.contacts.push_back(Contact { name, phone }); } /// Remove a contact by index public entry fun remove_contact( account: &signer, index: u64, ) acquires AddressBook { let addr = signer::address_of(account); assert!(exists(addr), E_NO_ADDRESS_BOOK); let book = &mut AddressBook[addr]; assert!(index < book.contacts.length(), E_CONTACT_NOT_FOUND); book.contacts.remove(index); } /// View all contacts #[view] public fun get_contacts(addr: address): vector acquires AddressBook { assert!(exists(addr), E_NO_ADDRESS_BOOK); AddressBook[addr].contacts } /// View the number of contacts #[view] public fun contact_count(addr: address): u64 acquires AddressBook { assert!(exists(addr), E_NO_ADDRESS_BOOK); AddressBook[addr].contacts.length() } /// Remove the address book entirely public entry fun delete_address_book(account: &signer) acquires AddressBook { let addr = signer::address_of(account); assert!(exists(addr), E_NO_ADDRESS_BOOK); let AddressBook { contacts: _, owner: _ } = move_from(addr); } // ---- Tests ---- #[test(account = @0x1)] fun test_create_and_add_contact(account: &signer) acquires AddressBook { let addr = signer::address_of(account); create_address_book(account); assert!(exists(addr)); assert!(contact_count(addr) == 0); add_contact( account, std::string::utf8(b"Alice"), std::string::utf8(b"555-1234"), ); assert!(contact_count(addr) == 1); let contacts = get_contacts(addr); let first = contacts[0]; assert!(first.name == std::string::utf8(b"Alice")); } #[test(account = @0x1)] fun test_delete_address_book(account: &signer) acquires AddressBook { let addr = signer::address_of(account); create_address_book(account); assert!(exists(addr)); delete_address_book(account); assert!(!exists(addr)); } #[test(account = @0x1)] fun test_remove_contact(account: &signer) acquires AddressBook { let addr = signer::address_of(account); create_address_book(account); add_contact(account, std::string::utf8(b"Alice"), std::string::utf8(b"555-1234")); add_contact(account, std::string::utf8(b"Bob"), std::string::utf8(b"555-5678")); assert!(contact_count(addr) == 2); remove_contact(account, 0); assert!(contact_count(addr) == 1); } #[test(account = @0x1)] #[expected_failure(abort_code = E_ALREADY_EXISTS)] fun test_double_create_fails(account: &signer) { create_address_book(account); create_address_book(account); } #[test(account = @0x1)] #[expected_failure(abort_code = E_CONTACT_NOT_FOUND)] fun test_remove_invalid_index_fails(account: &signer) acquires AddressBook { create_address_book(account); remove_contact(account, 0); // No contacts -- should fail } } ``` ## Breakdown ### Struct Design - **`Contact`** has `store`, `drop`, and `copy` -- it is a lightweight data value that can be freely duplicated and is embeddable inside other stored types. - **`AddressBook`** has only `key` -- it is a resource that lives in global storage and cannot be copied or accidentally dropped. ### Entry Functions - `create_address_book` stores a new empty address book under the caller's account. - `add_contact` appends a contact to the caller's existing address book. - `remove_contact` removes a contact by index, using `E_CONTACT_NOT_FOUND` if the index is out of bounds. - `delete_address_book` removes and destructures the resource, freeing the storage slot. ### View Functions - `get_contacts` returns a copy of all contacts. - `contact_count` returns the number of contacts. ### Error Handling Each function validates preconditions with `assert!` and descriptive error codes. This is a best practice for all Move contracts. ### Tests The test functions verify the full lifecycle: creation, modification, reading, and deletion. The `#[expected_failure]` test ensures that duplicate creation is properly rejected. ## Key Takeaways 1. **Structs model your domain**: Use them to represent entities and their relationships. 2. **Abilities control behavior**: Choose abilities carefully to enforce safety properties. 3. **Resources live in global storage**: Use `move_to`, `move_from`, and borrowing operations. 4. **Test everything**: Move's built-in testing makes it easy to verify correctness. # Struct Method Syntax Move supports defining methods on structs using a receiver-style syntax. This allows you to call functions using dot notation on struct values, making code more readable and object-oriented in style. ## Defining Methods A method is a function whose first parameter is a reference to the struct type. The method can then be called using dot notation. ```move module my_addr::counter { struct Counter has key { value: u64, } /// Read the current value -- called as counter.value() public fun value(self: &Counter): u64 { self.value } /// Increment the counter -- called as counter.increment() public fun increment(self: &mut Counter) { self.value = self.value + 1; } } ``` ## Calling Methods Once defined, methods can be called with dot notation: ```move fun example() acquires Counter { let counter_ref = &Counter[@0x1]; let val = counter_ref.value(); // Method call let counter_mut = &mut Counter[@0x1]; counter_mut.increment(); // Mutable method call } ``` ## Receiver Types Methods can take three types of receivers: ### Immutable Reference (`&Self`) Used for read-only operations: ```move public fun is_empty(self: &Counter): bool { self.value == 0 } ``` ### Mutable Reference (`&mut Self`) Used for operations that modify the struct: ```move public fun reset(self: &mut Counter) { self.value = 0; } ``` ### By Value (`Self`) Used for operations that consume the struct: ```move public fun destroy(self: Counter): u64 { let Counter { value } = self; value } ``` ## Practical Example Here is a more complete example showing methods in a token module: ```move module my_addr::simple_token { use std::string::String; struct Token has store { name: String, balance: u64, } /// Create a new token public fun new(name: String, initial_balance: u64): Token { Token { name, balance: initial_balance } } /// Get the token name public fun name(self: &Token): &String { &self.name } /// Get the current balance public fun balance(self: &Token): u64 { self.balance } /// Check if the token has any balance public fun has_balance(self: &Token): bool { self.balance > 0 } /// Add to the balance public fun deposit(self: &mut Token, amount: u64) { self.balance = self.balance + amount; } /// Subtract from the balance public fun withdraw(self: &mut Token, amount: u64): u64 { assert!(self.balance >= amount, 1); self.balance = self.balance - amount; amount } /// Consume the token and return the balance public fun redeem(self: Token): u64 { let Token { name: _, balance } = self; balance } } ``` Usage with dot notation: ```move fun example() { let mut token = simple_token::new( std::string::utf8(b"MyToken"), 1000, ); let name = token.name(); let bal = token.balance(); assert!(token.has_balance()); token.deposit(500); let withdrawn = token.withdraw(200); let remaining = token.redeem(); } ``` ## Method Resolution When you write `x.method()`, the compiler looks for a function named `method` in the module where the type of `x` is defined. The first parameter of that function must be a reference to (or value of) the type of `x`. The compiler automatically inserts the appropriate borrow. For example: ```move let counter = Counter { value: 0 }; counter.value() // Compiler inserts &counter as the first argument ``` ## Best Practices 1. **Use methods for type-specific operations**: Any function that logically belongs to a struct should be defined as a method. 2. **Prefer `&self` over `&mut self`**: Use the least permissive receiver type. 3. **Name methods clearly**: Use verbs for actions (`deposit`, `withdraw`) and nouns for accessors (`balance`, `name`). 4. **Keep methods focused**: Each method should do one thing well. # Enums and Pattern Matching Enums (enumerations) allow you to define a type by listing its possible variants. While a struct groups related fields together, an enum says "this value is one of these possible things." Combined with pattern matching via `match`, enums enable expressive and safe handling of multiple states. ## Why Enums? In smart contract development, you frequently encounter situations where a value can be one of several distinct types: - A transaction result is either a success or a failure. - A game character's state is idle, moving, attacking, or defeated. - A proposal's status is pending, approved, or rejected. Enums make these states explicit and let the compiler verify that you handle every possible case. ## What You'll Learn 1. **[Defining Enums](defining.md)** - How to declare enums with and without data 2. **[The `match` Control Flow Construct](pattern_matching.md)** - How to branch on enum variants # Defining Enums Enums in Move allow you to define a type that can be one of several variants. Each variant can optionally carry data. ## Basic Enum The simplest enum is a set of named variants with no associated data: ```move module my_addr::status { enum Status has copy, drop { Active, Inactive, Suspended, } } ``` ## Enums with Data Variants can carry data, similar to structs: ```move module my_addr::shapes { enum Shape has copy, drop { Circle { radius: u64 }, Rectangle { width: u64, height: u64 }, Triangle { base: u64, height: u64 }, } } ``` Each variant can have different fields, making enums far more flexible than simple constants. ## Enums with Positional Fields Variants can also carry unnamed (positional) data: ```move module my_addr::result { use std::string::String; enum Result has copy, drop { Ok(u64), Err(String), } } ``` ## Abilities on Enums Like structs, enums can have abilities. The rules are the same: an enum can only have an ability if all data in all variants supports that ability. ```move /// Copyable, droppable enum enum Color has copy, drop, store { Red, Green, Blue, Custom { r: u8, g: u8, b: u8 }, } ``` ## Creating Enum Values You create an enum value by specifying the variant: ```move fun create_examples() { let active = Status::Active; let circle = Shape::Circle { radius: 10 }; let ok = Result::Ok(42); } ``` ## Enum Use Cases in Smart Contracts ### Proposal Status ```move module my_addr::governance { enum ProposalStatus has copy, drop, store { Pending, Active { votes_for: u64, votes_against: u64 }, Approved, Rejected, Executed, } struct Proposal has key { description: vector, status: ProposalStatus, } } ``` ### Order Types ```move module my_addr::exchange { enum OrderType has copy, drop, store { Market, Limit { price: u64 }, StopLoss { trigger_price: u64 }, } } ``` ## Best Practices 1. **Use enums for mutually exclusive states**: If a value can only be one of several options, use an enum. 2. **Prefer enums over integer constants**: Enums are type-safe and self-documenting. 3. **Attach data to variants**: If a variant needs additional context, embed it directly rather than using separate fields. 4. **Add abilities thoughtfully**: Only add abilities that all variants can support. # The `match` Control Flow Construct The `match` expression lets you compare a value against a series of patterns and execute code based on which pattern matches. It is the primary way to work with enums in Move. ## Basic Match ```move module my_addr::matcher { enum Coin has copy, drop { Penny, Nickel, Dime, Quarter, } fun value_in_cents(coin: Coin): u64 { match (coin) { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } } ``` ## Matching with Data When a variant carries data, you can bind the data to variables in the pattern: ```move enum Shape has copy, drop { Circle { radius: u64 }, Rectangle { width: u64, height: u64 }, } fun area(shape: Shape): u64 { match (shape) { Shape::Circle { radius } => { // Approximate: 3 * r^2 3 * radius * radius }, Shape::Rectangle { width, height } => { width * height }, } } ``` ## Matching Positional Variants ```move enum Result has copy, drop { Ok(u64), Err(u64), } fun unwrap_or_default(result: Result): u64 { match (result) { Result::Ok(value) => value, Result::Err(_) => 0, } } ``` ## Exhaustiveness The Move compiler requires that `match` expressions are **exhaustive** -- every possible variant must be handled. This prevents bugs where a case is accidentally forgotten. ```move // This would NOT compile -- missing Coin::Quarter: // fun broken(coin: Coin): u64 { // match (coin) { // Coin::Penny => 1, // Coin::Nickel => 5, // Coin::Dime => 10, // } // } ``` ## Multi-Line Match Arms Match arms can contain multiple statements within braces: ```move fun describe(shape: Shape): vector { match (shape) { Shape::Circle { radius } => { if (radius > 100) { b"large circle" } else { b"small circle" } }, Shape::Rectangle { width, height } => { if (width == height) { b"square" } else { b"rectangle" } }, } } ``` ## Match and References You can match on references to avoid consuming the value: ```move fun is_circle(shape: &Shape): bool { match (shape) { Shape::Circle { .. } => true, _ => false, } } ``` ## Practical Example: State Machine Enums and match are ideal for implementing state machines in smart contracts: ```move module my_addr::auction { use std::signer; enum AuctionState has copy, drop, store { Open { highest_bid: u64, highest_bidder: address }, Closed, Settled { winner: address, amount: u64 }, } struct Auction has key { state: AuctionState, seller: address, } public fun process_bid(auction: &mut Auction, bidder: address, amount: u64) { auction.state = match (auction.state) { AuctionState::Open { highest_bid, highest_bidder } => { if (amount > highest_bid) { AuctionState::Open { highest_bid: amount, highest_bidder: bidder, } } else { AuctionState::Open { highest_bid, highest_bidder } } }, AuctionState::Closed => abort 1, // Cannot bid on closed auction AuctionState::Settled { .. } => abort 2, // Already settled }; } } ``` ## Best Practices 1. **Always handle all variants**: The compiler enforces this, which prevents missing-case bugs. 2. **Use `_` for unused bindings**: If you don't need a value, use `_` to ignore it. 3. **Keep match arms simple**: If a match arm is complex, extract the logic into a separate function. 4. **Use enums for state machines**: The combination of enums and match naturally models state transitions. # Managing Modules and Packages Move code is organized into **modules** and **packages**. Understanding how to structure, import, and publish your code is essential for building maintainable smart contracts. - A **module** is the basic unit of code organization -- a named collection of types, functions, and constants published at a specific address. - A **package** is a collection of modules along with a manifest (`Move.toml`) that specifies metadata and dependencies. ## What You'll Learn 1. **[Module Basics](basics.md)** - Defining and structuring modules 2. **[Module Imports](imports.md)** - Using code from other modules 3. **[Function Scopes](scopes.md)** - Visibility levels for functions 4. **[Package Basics](packages.md)** - The `Move.toml` manifest and project layout 5. **[Package Dependencies](dependencies.md)** - Adding and managing external dependencies 6. **[Package Publishing](publishing.md)** - Deploying your code on-chain # Module Basics A module is the fundamental organizational unit in Move. It groups related types, functions, and constants together and is published at a specific blockchain address. ## Defining a Module ```move module my_addr::greeting { use std::string::String; /// A greeting resource struct Greeting has key { message: String, } /// Store a greeting public entry fun set_greeting(account: &signer, msg: String) { move_to(account, Greeting { message: msg }); } /// Read a greeting #[view] public fun get_greeting(addr: address): String acquires Greeting { Greeting[addr].message } } ``` The module declaration has two parts: - **`my_addr`** is a named address that is resolved at compile time. - **`greeting`** is the module name, which must be unique within the address. ## Module Contents A module can contain: ### Constants ```move module my_addr::config { /// Maximum allowed value const MAX_VALUE: u64 = 1000; /// Error: value exceeds maximum const E_VALUE_TOO_HIGH: u64 = 1; } ``` ### Structs and Enums ```move module my_addr::types { struct Point has copy, drop, store { x: u64, y: u64, } enum Direction has copy, drop { North, South, East, West, } } ``` ### Functions ```move module my_addr::math { public fun add(a: u64, b: u64): u64 { a + b } fun internal_helper(x: u64): u64 { x * 2 } } ``` ## Named Addresses Named addresses are placeholders that are resolved at compile or deploy time. They are defined in the `Move.toml` file: ```toml [addresses] my_addr = "_" # Will be filled at deploy time ``` Or set explicitly: ```toml [addresses] my_addr = "0x1234abcd" ``` When deploying with the Aptos CLI, you provide the address mapping: ```sh aptos move publish --named-addresses my_addr=default ``` ## Module Naming Conventions - Module names use **snake_case**: `my_module`, `token_factory`. - Struct and enum names use **PascalCase**: `UserProfile`, `OrderType`. - Function names use **snake_case**: `create_account`, `get_balance`. - Constants use **UPPER_SNAKE_CASE**: `MAX_SUPPLY`, `E_NOT_FOUND`. - Error constants begin with **`E_`** or **`E`**: `E_INSUFFICIENT_BALANCE`. ## One Module Per Purpose A best practice is to keep each module focused on a single responsibility: ``` sources/ ├── token.move # Token struct and logic ├── marketplace.move # Marketplace for trading tokens ├── governance.move # Voting and proposals └── config.move # Shared configuration ``` This makes your code easier to read, test, and upgrade. # Module Imports Move uses the `use` keyword to import modules, types, and functions from other modules. This lets you reference external code without writing out full paths every time. ## Basic Imports ### Importing a Module ```move module my_addr::example { use std::string; fun create_greeting(): string::String { string::utf8(b"Hello, Aptos!") } } ``` ### Importing a Specific Type ```move module my_addr::example { use std::string::String; fun create_greeting(): String { std::string::utf8(b"Hello, Aptos!") } } ``` ### Importing Both Module and Type Use `Self` to import the module itself alongside specific types: ```move module my_addr::example { use std::string::{Self, String}; fun create_greeting(): String { string::utf8(b"Hello, Aptos!") } } ``` This is the most common pattern -- it gives you access to both the `String` type directly and the module's functions via `string::`. ## Multiple Imports ### From the Same Module ```move use std::vector::{Self, empty, push_back, length}; ``` ### From Different Modules ```move use std::string::{Self, String}; use std::signer; use std::vector; use aptos_framework::event; use aptos_framework::account; ``` ## Standard Library Addresses Aptos has three standard library addresses: | Address | Name | Description | |---|---|---| | `std` (0x1) | Move Standard Library | Basic types, vectors, strings, options | | `aptos_std` (0x1) | Aptos Standard Library | Extended utilities, crypto, data structures | | `aptos_framework` (0x1) | Aptos Framework | Accounts, coins, objects, governance | Common imports: ```move // From Move Stdlib use std::string::{Self, String}; use std::vector; use std::option::{Self, Option}; use std::signer; use std::error; // From Aptos Stdlib use aptos_std::table::{Self, Table}; use aptos_std::smart_table::{Self, SmartTable}; // From Aptos Framework use aptos_framework::event; use aptos_framework::object::{Self, Object}; use aptos_framework::fungible_asset::{Self, FungibleAsset}; use aptos_framework::primary_fungible_store; ``` ## Aliasing Imports You can rename imports with `as`: ```move use std::string::String as Str; use aptos_framework::fungible_asset as fa; ``` ## Where to Place Imports Imports are placed at the top of the module body, before any definitions: ```move module my_addr::example { // Imports first use std::string::{Self, String}; use std::signer; use aptos_framework::event; // Then constants, structs, and functions const E_NOT_FOUND: u64 = 1; struct MyStruct has key { name: String, } public fun my_function() { } } ``` ## Test-Only Imports Use `#[test_only]` to import modules that are only needed in tests: ```move #[test_only] use std::debug::print; #[test_only] use aptos_framework::account::create_account_for_test; ``` ## Best Practices 1. **Import what you use**: Don't import modules you don't reference. 2. **Use `Self` pattern**: Import both the module and its key types with `use module::{Self, Type}`. 3. **Group imports logically**: Group std, aptos_std, aptos_framework, and your own modules. 4. **Prefer specific imports**: Import specific types rather than using full paths throughout. # Function Scopes Move provides several visibility levels for functions that control where they can be called from. Choosing the right scope is important for security and API design. ## Visibility Levels ### Private (default) Functions with no visibility modifier are private. They can only be called from within the same module. ```move module my_addr::example { /// Only callable within this module fun internal_calculation(x: u64): u64 { x * 2 } public fun public_function(): u64 { internal_calculation(21) // OK -- same module } } ``` ### Public `public` functions can be called from any module. ```move module my_addr::math { /// Callable from anywhere public fun add(a: u64, b: u64): u64 { a + b } } ``` ### Public Entry `entry` functions can be called as standalone transactions from outside the blockchain (e.g., from a wallet or SDK). They can be combined with `public` to also be callable from other modules. ```move module my_addr::token { /// Callable as a transaction AND from other modules public entry fun transfer(from: &signer, to: address, amount: u64) { // ... } /// Callable as a transaction ONLY (not from other modules) entry fun admin_action(admin: &signer) { // ... } } ``` ### Public(friend) `public(friend)` functions are callable only from modules that are declared as friends. ```move module my_addr::core { friend my_addr::helper; /// Only callable from my_addr::helper public(friend) fun internal_mint(amount: u64) { // ... } } module my_addr::helper { use my_addr::core; public fun do_mint(amount: u64) { core::internal_mint(amount); // OK -- declared as friend } } ``` ### Public(package) `public(package)` functions are callable from any module within the same package but not from external packages. ```move module my_addr::internal_api { /// Callable from any module in this package public(package) fun package_only_function(): u64 { 42 } } ``` ## View Functions The `#[view]` attribute marks a function as callable by external read queries without submitting a transaction. View functions should not modify state. ```move #[view] public fun get_balance(addr: address): u64 acquires Balance { Balance[addr].amount } ``` ## Summary Table | Modifier | Same Module | Same Package | Friend Module | Any Module | Transaction | |---|---|---|---|---|---| | *(private)* | Yes | No | No | No | No | | `public` | Yes | Yes | Yes | Yes | No | | `entry` | Yes | No | No | No | Yes | | `public entry` | Yes | Yes | Yes | Yes | Yes | | `public(friend)` | Yes | No | Yes | No | No | | `public(package)` | Yes | Yes | No | No | No | ## Best Practices 1. **Start private**: Make functions private by default and only increase visibility as needed. 2. **Use `entry` for user-facing actions**: Any function a user calls directly should be `entry`. 3. **Use `public` for reusable library functions**: Functions other modules need to call. 4. **Use `public(package)` for internal APIs**: Shared logic within your package that shouldn't be exposed externally. 5. **Use `#[view]` for read operations**: Mark all pure read functions with `#[view]`. 6. **Limit `friend` usage**: Use sparingly -- `public(package)` is usually a better choice. # Package Basics A Move package is a directory containing a `Move.toml` manifest and one or more Move source files. It is the unit of compilation and deployment on Aptos. ## Package Structure A typical package looks like this: ``` my_project/ ├── Move.toml # Package manifest ├── sources/ # Move source files │ ├── token.move │ └── marketplace.move └── tests/ # Test files (optional) └── token_tests.move ``` ## The `Move.toml` File The `Move.toml` manifest describes your package: ```toml [package] name = "MyProject" version = "1.0.0" authors = ["developer@example.com"] [addresses] my_addr = "_" [dependencies] AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", subdir = "aptos-move/framework/aptos-framework", rev = "main" } ``` ### Sections - **`[package]`** -- Metadata about your package (name, version, authors). - **`[addresses]`** -- Named address definitions. Use `"_"` for addresses that will be provided at deploy time. - **`[dependencies]`** -- External packages your code depends on. ## Creating a New Package The Aptos CLI can create a package from a template: ```sh mkdir my_project cd my_project aptos move init --name MyProject ``` This creates the basic directory structure and a `Move.toml` file. You can also use templates: ```sh aptos move init --name MyProject --template hello-blockchain ``` ## Building a Package To compile your package: ```sh aptos move compile --named-addresses my_addr=default ``` The `--named-addresses` flag maps named addresses to actual values. The special value `default` uses your CLI profile's address. ## Source Files Move source files have the `.move` extension and can contain one or more modules: ```move // sources/token.move module my_addr::token { struct Token has key { balance: u64, } } ``` Multiple modules can exist in a single file, but the convention is one module per file, with the filename matching the module name. ## Test Files Test files live in the `tests/` directory or alongside source files. Test functions are annotated with `#[test]`: ```move // tests/token_tests.move #[test_only] module my_addr::token_tests { use my_addr::token; #[test] fun test_creation() { // ... } } ``` ## Best Practices 1. **One module per file**: Keep source files focused and name them after the module they contain. 2. **Use named addresses**: Use named addresses with `"_"` rather than hardcoding addresses. 3. **Version your packages**: Use semantic versioning in `Move.toml`. 4. **Keep `Move.toml` clean**: Only include dependencies you actually use. # Package Dependencies Move packages can depend on other packages, including the Aptos framework, standard libraries, and third-party packages. ## Adding Dependencies Dependencies are declared in the `[dependencies]` section of `Move.toml`. ### Git Dependencies The most common type of dependency for Aptos projects: ```toml [dependencies] AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", subdir = "aptos-move/framework/aptos-framework", rev = "main" } ``` - **`git`** -- The repository URL. - **`subdir`** -- The subdirectory within the repo that contains the Move package. - **`rev`** -- The git revision (branch, tag, or commit hash). Using a specific commit hash is recommended for reproducible builds. ### Local Dependencies For packages on your local filesystem: ```toml [dependencies] MyLibrary = { local = "../my-library" } ``` ### Pinning a Specific Version For reproducible builds, pin to a specific commit hash: ```toml [dependencies] AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", subdir = "aptos-move/framework/aptos-framework", rev = "abc123def456" } ``` ## The Aptos Framework Dependencies Most Aptos projects need the Aptos Framework, which provides standard modules for accounts, coins, objects, and more. The framework includes three layers: ```toml [dependencies] # Includes MoveStdlib, AptosStdlib, and AptosFramework AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", subdir = "aptos-move/framework/aptos-framework", rev = "main" } ``` Importing `AptosFramework` automatically includes `MoveStdlib` and `AptosStdlib` as transitive dependencies. ## Address Mapping for Dependencies If a dependency uses named addresses, you may need to provide them. The Aptos Framework standard addresses are typically set automatically. ```toml [addresses] my_addr = "_" std = "0x1" aptos_std = "0x1" aptos_framework = "0x1" ``` ## Dependency Resolution When you run `aptos move compile`, the CLI: 1. Downloads all git dependencies to a local cache. 2. Resolves transitive dependencies. 3. Checks for version conflicts. 4. Compiles all dependencies before your package. ## Best Practices 1. **Pin dependency versions**: Use specific commit hashes rather than `main` for production code. 2. **Minimize dependencies**: Only include what you need. 3. **Use the framework**: The Aptos Framework provides battle-tested implementations of common patterns. 4. **Test with the same dependencies**: Ensure your tests use the same dependency versions as your production code. # Package Publishing Publishing a package deploys your compiled Move modules to the Aptos blockchain. Once published, your code is live and callable by anyone. ## Publishing with the Aptos CLI The most common way to publish is with the `aptos move publish` command (or `aptos move deploy`): ```sh aptos move publish --named-addresses my_addr=default ``` This will: 1. Compile your package. 2. Show you a summary of the modules being deployed and estimated gas cost. 3. Prompt for confirmation. 4. Submit the publish transaction. ### Specifying a Profile If you have multiple CLI profiles, specify which to use: ```sh aptos move publish --profile mainnet --named-addresses my_addr=mainnet ``` ### Skipping Confirmation For scripts or CI/CD: ```sh aptos move publish --named-addresses my_addr=default --assume-yes ``` ## Upgrade Policies When you publish code for the first time, you can choose an upgrade policy: - **`compatible`** (default): You can upgrade the package, but changes must be backward-compatible (no removing public functions, no changing function signatures). - **`immutable`**: The code can never be changed after publication. For most projects, `compatible` is the right choice. It allows you to fix bugs and add new functionality while preserving existing interfaces. ## Upgrading a Package To upgrade an already-published package, simply run the publish command again: ```sh aptos move publish --named-addresses my_addr=default ``` The CLI will detect the existing package and perform an upgrade. The upgrade will fail if the changes are not backward-compatible (e.g., removing a public function). ### What Can Be Changed - Adding new modules - Adding new functions (public, entry, etc.) - Adding new structs - Changing private function implementations - Adding new fields to structs (in some cases) ### What Cannot Be Changed - Removing or renaming public functions - Changing the signature of existing public functions - Removing or renaming existing structs - Removing or renaming existing modules ## Publishing Workflow A typical workflow for deploying to mainnet: 1. **Develop and test locally**: ```sh aptos move test --dev ``` 2. **Deploy to devnet**: ```sh aptos init --profile devnet --network devnet aptos move publish --profile devnet --named-addresses my_addr=devnet ``` 3. **Test on devnet**: Interact with your contract and verify behavior. 4. **Deploy to testnet**: ```sh aptos init --profile testnet --network testnet aptos move publish --profile testnet --named-addresses my_addr=testnet ``` 5. **Deploy to mainnet**: ```sh aptos init --profile mainnet --network mainnet aptos move publish --profile mainnet --named-addresses my_addr=mainnet ``` ## Verifying Published Code After publishing, you can verify your code on the [Aptos Explorer](https://explorer.aptoslabs.com). The source code is uploaded by default during publication, making it easy for others to verify what your contract does. ## Best Practices 1. **Always test before publishing**: Run `aptos move test` before every deployment. 2. **Deploy to devnet first**: Catch issues before they reach mainnet. 3. **Use compatible upgrades**: Allow yourself to fix bugs after deployment. 4. **Document your API**: Once published, your public functions are your contract's interface. 5. **Keep private functions private**: Only expose what external callers need. # Collections There are two major categories of collections on Aptos: - Key Value Collections -> Maps and Tables - Ordered Collections -> Vectors We'll go through each of these in the following pages. # Vectors Vectors are ordered sequences of elements in Move. They are one of the most fundamental collection types and are used extensively throughout Move programs. ## Available Vector Types ### Standard Vector (`std::vector`) The basic vector type from the Move standard library. ```move use std::vector; // Create an empty vector let empty_vec: vector = vector::empty(); // Create a vector with initial elements let numbers: vector = vector[1, 2, 3, 4, 5]; // Create a vector of strings let strings: vector = vector[ string::utf8(b"hello"), string::utf8(b"world") ]; ``` ### BigVector (`aptos_std::big_vector`) A specialized vector implementation for large datasets that uses table storage internally. ```move use aptos_std::big_vector::{Self, BigVector}; // Create a BigVector let big_vec = big_vector::new(); // Add elements big_vector::push_back(&mut big_vec, 1); big_vector::push_back(&mut big_vec, 2); ``` ## How to Use Vectors ### Basic Operations ```move module my_module::vector_example { use std::vector; use std::string::{Self, String}; public fun create_and_manipulate_vector(): vector { // Create an empty vector let v = vector::empty(); // Add elements vector::push_back(&mut v, 1); vector::push_back(&mut v, 2); vector::push_back(&mut v, 3); // Get length let len = vector::length(&v); // Access element by index let first = *vector::borrow(&v, 0); let last = *vector::borrow(&v, len - 1); // Remove and return last element let popped = vector::pop_back(&mut v); v } public fun vector_operations(): vector { let v = vector::empty(); // Add strings vector::push_back(&mut v, string::utf8(b"hello")); vector::push_back(&mut v, string::utf8(b"world")); // Check if empty let is_empty = vector::is_empty(&v); // Get capacity (if available) let capacity = vector::length(&v); v } } ``` ### Iterating Over Vectors ```move public fun sum_vector(v: &vector): u64 { let sum = 0u64; let i = 0; let len = vector::length(v); while (i < len) { sum = sum + *vector::borrow(v, i); i = i + 1; }; sum } // Using for loop (Move 2024+) public fun sum_vector_for(v: &vector): u64 { let sum = 0u64; for (element in v) { sum = sum + *element; }; sum } ``` ### Vector Manipulation ```move public fun vector_manipulation(): vector { let v = vector[1, 2, 3, 4, 5]; // Insert at specific index vector::insert(&mut v, 2, 10); // Result: [1, 2, 10, 3, 4, 5] // Remove element at index let removed = vector::remove(&mut v, 1); // Result: [1, 10, 3, 4, 5], removed = 2 // Swap elements vector::swap(&mut v, 0, 2); // Result: [3, 10, 1, 4, 5] // Reverse the vector vector::reverse(&mut v); // Result: [5, 4, 1, 10, 3] v } ``` ## Vector Abilities and Constraints Vectors have specific abilities that determine how they can be used: ```move // Vector has copy, drop, and store abilities struct VectorHolder has key, store { data: vector } // Vectors can be stored in global storage public fun store_vector(account: &signer) { let v = vector[1, 2, 3]; move_to(account, VectorHolder { data: v }); } // Vectors can be copied and dropped public fun vector_abilities() { let v1 = vector[1, 2, 3]; let v2 = v1; // Copy // v1 is still available due to copy ability // Both v1 and v2 are automatically dropped at end of function } ``` ## Tradeoffs ### Advantages of Standard Vectors - **Simple and familiar** - Easy to understand and use - **Efficient for small datasets** - Good performance for collections with < 1000 elements - **Flexible** - Supports all basic operations (push, pop, insert, remove, etc.) - **Memory efficient** - Compact storage for small collections - **BCS serializable** - Can be easily serialized and deserialized ### Disadvantages of Standard Vectors - **Limited scalability** - Performance degrades with large datasets - **Single storage slot** - All elements stored in one storage slot - **No parallelization** - Operations cannot be parallelized - **Memory constraints** - Limited by single storage slot size - **Expensive operations** - Insert/remove operations are O(n) ### When to Use BigVector - **Large datasets** - When you need to store thousands of elements - **Frequent additions** - When you frequently add elements - **Table-based storage** - When you want table storage benefits - **Scalability requirements** - When you need to scale to large collections ### When to Use Standard Vector - **Small datasets** - Collections with < 1000 elements - **Simple use cases** - When you need basic sequential storage - **Memory efficiency** - When storage space is a concern - **BCS serialization** - When you need to serialize the data ## Performance Characteristics | Operation | Standard Vector | BigVector | |-----------|----------------|-----------| | Push back | O(1) amortized | O(1) | | Pop back | O(1) | O(1) | | Access by index | O(1) | O(1) | | Insert at index | O(n) | O(n) | | Remove at index | O(n) | O(n) | | Storage cost | Single slot | Multiple slots | | Parallelization | No | Yes (for table operations) | ## Best Practices 1. **Choose the right type**: Use standard vectors for small collections, BigVector for large ones 2. **Prefer push_back/pop_back**: These are the most efficient operations 3. **Avoid frequent insert/remove**: These operations are expensive 4. **Use references when possible**: Borrow elements instead of copying when you only need to read 5. **Consider storage costs**: Vectors stored in global storage consume storage slots 6. **Plan for growth**: If your collection might grow large, consider BigVector from the start # Maps Maps are key-value storage structures that allow efficient lookup and storage of data by unique keys. Aptos provides several map implementations with different characteristics and use cases. ## Available Map Types ### SimpleMap (`std::simple_map`) A basic hash map implementation from the Move standard library. ```move use std::simple_map::{Self, SimpleMap}; // Create an empty map let map: SimpleMap = simple_map::create(); // Add key-value pairs simple_map::add(&mut map, @0x1, 100); simple_map::add(&mut map, @0x2, 200); ``` ### OrderedMap (`std::ordered_map`) A map implementation that maintains insertion order of keys. ```move use std::ordered_map::{Self, OrderedMap}; // Create an ordered map let ordered_map: OrderedMap = ordered_map::new(); // Add elements (maintains order) ordered_map::add(&mut ordered_map, 1, string::utf8(b"first")); ordered_map::add(&mut ordered_map, 2, string::utf8(b"second")); ``` ### BigOrderedMap (`aptos_std::big_ordered_map`) A scalable ordered map implementation for large datasets using table storage. ```move use aptos_std::big_ordered_map::{Self, BigOrderedMap}; // Create a big ordered map let big_map = big_ordered_map::new(); ``` ## How to Use Maps ### Basic Operations with SimpleMap ```move module my_module::map_example { use std::simple_map::{Self, SimpleMap}; use std::string::{Self, String}; public fun create_and_use_map(): SimpleMap { // Create an empty map let map = simple_map::create(); // Add key-value pairs simple_map::add(&mut map, @0x1, 100); simple_map::add(&mut map, @0x2, 200); simple_map::add(&mut map, @0x3, 300); // Check if key exists let has_key = simple_map::contains_key(&map, &@0x1); // Get value by key let value = *simple_map::borrow(&map, &@0x2); // Update existing value simple_map::set(&mut map, @0x1, 150); // Remove key-value pair let removed_value = simple_map::remove(&mut map, &@0x3); map } public fun map_with_strings(): SimpleMap { let map = simple_map::create(); simple_map::add(&mut map, string::utf8(b"alice"), 1000); simple_map::add(&mut map, string::utf8(b"bob"), 2000); simple_map::add(&mut map, string::utf8(b"charlie"), 3000); map } } ``` ### Working with OrderedMap ```move public fun ordered_map_operations(): OrderedMap { let map = ordered_map::new(); // Add elements (order is preserved) ordered_map::add(&mut map, 3, string::utf8(b"third")); ordered_map::add(&mut map, 1, string::utf8(b"first")); ordered_map::add(&mut map, 2, string::utf8(b"second")); // Get length let len = ordered_map::length(&map); // Check if empty let is_empty = ordered_map::is_empty(&map); // Get value by key let value = *ordered_map::borrow(&map, &1); // Update value ordered_map::set(&mut map, 1, string::utf8(b"updated_first")); // Remove key-value pair let removed = ordered_map::remove(&mut map, &3); map } ``` ### Iterating Over Maps ```move public fun iterate_simple_map(map: &SimpleMap): u64 { let total = 0u64; let keys = simple_map::keys(map); let i = 0; let len = vector::length(&keys); while (i < len) { let key = *vector::borrow(&keys, i); let value = *simple_map::borrow(map, &key); total = total + value; i = i + 1; }; total } public fun iterate_ordered_map(map: &OrderedMap): vector { let values = vector::empty(); let keys = ordered_map::keys(map); let i = 0; let len = vector::length(&keys); while (i < len) { let key = *vector::borrow(&keys, i); let value = *ordered_map::borrow(map, &key); vector::push_back(&mut values, value); i = i + 1; }; values } ``` ### Map Abilities and Storage ```move // Maps can be stored in global storage struct MapHolder has key, store { user_balances: SimpleMap, user_names: OrderedMap } public fun store_maps(account: &signer) { let balances = simple_map::create(); let names = ordered_map::new(); // Add some data simple_map::add(&mut balances, @0x1, 1000); ordered_map::add(&mut names, @0x1, string::utf8(b"Alice")); move_to(account, MapHolder { user_balances: balances, user_names: names }); } ``` ## Tradeoffs ### SimpleMap Advantages - **Simple and efficient** - Basic hash map implementation - **Fast lookups** - O(1) average case for key lookups - **Memory efficient** - Compact storage for small to medium datasets - **Standard library** - Part of Move standard library - **BCS serializable** - Can be easily serialized ### SimpleMap Disadvantages - **No ordering** - Keys are not stored in any particular order - **Limited scalability** - Performance degrades with very large datasets - **Single storage slot** - All data stored in one storage slot - **No parallelization** - Operations cannot be parallelized ### OrderedMap Advantages - **Maintains order** - Keys are stored in insertion order - **Predictable iteration** - Consistent order when iterating - **Useful for UI** - Good for displaying data in a consistent order - **Standard library** - Part of Move standard library ### OrderedMap Disadvantages - **Higher overhead** - Additional storage for maintaining order - **Slower operations** - Slightly slower than SimpleMap due to ordering overhead - **Limited scalability** - Similar limitations to SimpleMap ### BigOrderedMap Advantages - **Scalable** - Designed for large datasets - **Table-based storage** - Uses efficient table storage - **Parallelizable** - Can benefit from table parallelization - **Maintains order** - Preserves insertion order like OrderedMap ### BigOrderedMap Disadvantages - **Higher storage cost** - Uses multiple storage slots - **More complex** - More complex implementation - **Not in standard library** - Part of Aptos standard library ## Performance Characteristics | Operation | SimpleMap | OrderedMap | BigOrderedMap | |-----------|-----------|------------|---------------| | Insert | O(1) avg | O(1) avg | O(1) avg | | Lookup | O(1) avg | O(1) avg | O(1) avg | | Delete | O(1) avg | O(1) avg | O(1) avg | | Iteration | Unordered | Ordered | Ordered | | Storage | Single slot | Single slot | Multiple slots | | Parallelization | No | No | Yes | ## When to Use Each Type ### Use SimpleMap when: - You need basic key-value storage - Order doesn't matter - Dataset is small to medium size (< 10,000 entries) - You want maximum performance - You're working with standard library only ### Use OrderedMap when: - You need to maintain insertion order - You frequently iterate over the map - Dataset is small to medium size - Order is important for your use case - You want predictable iteration order ### Use BigOrderedMap when: - You have large datasets (> 10,000 entries) - You need both ordering and scalability - You want table storage benefits - You need parallelizable operations - You're building a high-scale application ## Best Practices 1. **Choose the right map type**: Consider your data size and ordering requirements 2. **Use appropriate key types**: Prefer simple types like `address` or `u64` for keys 3. **Handle missing keys**: Always check if a key exists before accessing it 4. **Consider storage costs**: Maps stored in global storage consume storage slots 5. **Plan for growth**: If your map might grow large, consider BigOrderedMap from the start 6. **Use references**: Borrow values instead of copying when you only need to read 7. **Batch operations**: When possible, batch multiple operations together # Tables Tables are hash-addressable storage structures designed for large-scale data storage on Aptos. They provide efficient storage and retrieval of data using unique keys, with each table entry stored in its own storage slot for better performance and parallelization. ## Available Table Types ### Table (`aptos_std::table`) The basic table implementation that provides hash-addressable storage. ```move use aptos_std::table::{Self, Table}; // Create a new table let table: Table = table::new(); // Add key-value pairs table::add(&mut table, @0x1, 100); table::add(&mut table, @0x2, 200); ``` ### TableWithLength (`aptos_std::table_with_length`) A table implementation that tracks the number of entries and allows full deletion. ```move use aptos_std::table_with_length::{Self, TableWithLength}; // Create a table with length tracking let table: TableWithLength = table_with_length::new(); // Add elements (length is automatically tracked) table_with_length::add(&mut table, @0x1, 100); ``` ### SmartTable (`aptos_std::smart_table`) A bucketed table implementation that groups entries into vectors for more efficient storage. ```move use aptos_std::smart_table::{Self, SmartTable}; // Create a smart table let smart_table: SmartTable = smart_table::new(); // Add elements (automatically bucketed) smart_table::add(&mut smart_table, @0x1, 100); ``` ## How to Use Tables ### Basic Operations with Table ```move module my_module::table_example { use aptos_std::table::{Self, Table}; use std::string::{Self, String}; public fun create_and_use_table(): Table { // Create a new table let table = table::new(); // Add key-value pairs table::add(&mut table, @0x1, 100); table::add(&mut table, @0x2, 200); table::add(&mut table, @0x3, 300); // Check if key exists let has_key = table::contains(&table, @0x1); // Get value by key let value = *table::borrow(&table, @0x2); // Update existing value table::set(&mut table, @0x1, 150); // Remove key-value pair let removed_value = table::remove(&mut table, @0x3); table } public fun table_with_strings(): Table { let table = table::new(); table::add(&mut table, string::utf8(b"alice"), 1000); table::add(&mut table, string::utf8(b"bob"), 2000); table::add(&mut table, string::utf8(b"charlie"), 3000); table } } ``` ### Working with TableWithLength ```move public fun table_with_length_operations(): TableWithLength { let table = table_with_length::new(); // Add elements (length is tracked automatically) table_with_length::add(&mut table, 1, string::utf8(b"first")); table_with_length::add(&mut table, 2, string::utf8(b"second")); table_with_length::add(&mut table, 3, string::utf8(b"third")); // Get length let len = table_with_length::length(&table); // Check if empty let is_empty = table_with_length::is_empty(&table); // Get value by key let value = *table_with_length::borrow(&table, 1); // Update value table_with_length::set(&mut table, 1, string::utf8(b"updated_first")); // Remove key-value pair let removed = table_with_length::remove(&mut table, 3); // Length is automatically updated let new_len = table_with_length::length(&table); table } ``` ### Using SmartTable ```move public fun smart_table_operations(): SmartTable { let table = smart_table::new(); // Add elements (automatically bucketed) smart_table::add(&mut table, @0x1, 100); smart_table::add(&mut table, @0x2, 200); smart_table::add(&mut table, @0x3, 300); // Get length let len = smart_table::length(&table); // Check if empty let is_empty = smart_table::is_empty(&table); // Get value by key let value = *smart_table::borrow(&table, @0x2); // Update value smart_table::set(&mut table, @0x1, 150); // Remove key-value pair let removed = smart_table::remove(&mut table, @0x3); table } ``` ### Table Abilities and Storage ```move // Tables can be stored in global storage struct TableHolder has key, store { user_balances: Table, user_profiles: TableWithLength, user_scores: SmartTable } public fun store_tables(account: &signer) { let balances = table::new(); let profiles = table_with_length::new(); let scores = smart_table::new(); // Add some data table::add(&mut balances, @0x1, 1000); table_with_length::add(&mut profiles, @0x1, string::utf8(b"Alice")); smart_table::add(&mut scores, @0x1, 95); move_to(account, TableHolder { user_balances: balances, user_profiles: profiles, user_scores: scores }); } ``` ### Iterating Over Tables ```move public fun iterate_table(table: &Table): u64 { let total = 0u64; let keys = table::keys(table); let i = 0; let len = vector::length(&keys); while (i < len) { let key = *vector::borrow(&keys, i); let value = *table::borrow(table, key); total = total + value; i = i + 1; }; total } // SmartTable supports direct iteration public fun iterate_smart_table(table: &SmartTable): u64 { let total = 0u64; let keys = smart_table::keys(table); let i = 0; let len = vector::length(&keys); while (i < len) { let key = *vector::borrow(&keys, i); let value = *smart_table::borrow(table, key); total = total + value; i = i + 1; }; total } ``` ## Tradeoffs ### Table Advantages - **Hash-addressable storage** - Each entry stored in its own storage slot - **Parallelizable operations** - Different entries can be processed in parallel - **Scalable** - Designed for large datasets - **Efficient lookups** - O(1) average case for key lookups - **Individual storage slots** - Each entry has its own storage slot ### Table Disadvantages - **Higher storage cost** - Each entry uses a separate storage slot - **Table handle cost** - The table handle itself uses a storage slot - **No length tracking** - Basic table doesn't track number of entries - **Handle cannot be deleted** - Table handle persists even when empty ### TableWithLength Advantages - **Length tracking** - Automatically tracks number of entries - **Full deletion** - Can delete the entire table including handle - **All table benefits** - Inherits all advantages of basic table - **Useful for queries** - Easy to check if table is empty or get size ### TableWithLength Disadvantages - **Not parallelizable** - Length updates prevent parallelization - **Higher overhead** - Additional storage for length tracking - **Sequential operations** - Length updates must be sequential ### SmartTable Advantages - **Bucketed storage** - Groups entries into vectors for efficiency - **Length tracking** - Automatically tracks number of entries - **Iterable** - Supports iteration over all entries - **Storage efficient** - Reduces number of storage slots used - **Parallelizable** - Can be parallelized for certain operations ### SmartTable Disadvantages - **DDoS vulnerability** - Malicious keys could create large buckets - **Complex implementation** - More complex than basic table - **Variable performance** - Performance depends on key distribution ## Performance Characteristics | Operation | Table | TableWithLength | SmartTable | |-----------|-------|-----------------|------------| | Insert | O(1) avg | O(1) avg | O(1) avg | | Lookup | O(1) avg | O(1) avg | O(1) avg | | Delete | O(1) avg | O(1) avg | O(1) avg | | Length | N/A | O(1) | O(1) | | Iteration | Yes | Yes | Yes | | Storage slots | N+1 | N+2 | Variable | | Parallelization | Yes | No | Yes | ## When to Use Each Type ### Use Table when: - You need basic hash-addressable storage - You have large datasets - You want parallelizable operations - You don't need length tracking - You want maximum flexibility ### Use TableWithLength when: - You need to track the number of entries - You want to be able to delete the entire table - You need to check if the table is empty - You don't need parallelization - You want all table benefits plus length tracking ### Use SmartTable when: - You want more storage-efficient bucketing - You need both length tracking and iteration - You want to reduce storage slot usage - You can control key distribution - You need scalable storage with iteration ## Best Practices 1. **Choose the right table type**: Consider your specific needs for length tracking and iteration 2. **Use appropriate key types**: Prefer simple types like `address` or `u64` for keys 3. **Handle missing keys**: Always check if a key exists before accessing it 4. **Consider storage costs**: Each table entry uses a storage slot 5. **Plan for parallelization**: Use basic Table when you need parallel operations 6. **Monitor key distribution**: For SmartTable, ensure keys are well-distributed 7. **Use references**: Borrow values instead of copying when you only need to read 8. **Batch operations**: When possible, batch multiple operations together 9. **Consider deletion**: Use TableWithLength if you need to delete the entire table 10. **Monitor storage usage**: Tables can be expensive for very large datasets # Collection Types Comparison This document provides a comprehensive comparison of all collection types available in Move on Aptos, helping you choose the right collection for your specific use case. ## Quick Reference Table | Collection Type | Library | Ordering | Length Tracking | Scalability | Parallelization | Storage Cost | Best For | |-----------------|---------|----------|-----------------|-------------|-----------------|--------------|----------| | Vector | std | Yes | Yes | Low | No | Single slot | Small sequences | | BigVector | aptos_std | Yes | Yes | High | Yes | Multiple slots | Large sequences | | SimpleMap | std | No | No | Low | No | Single slot | Small key-value | | OrderedMap | aptos_std | Yes | Yes | Low | No | Single slot | Ordered key-value | | BigOrderedMap | aptos_std | Yes | Yes | High | Yes | Multiple slots | Large ordered key-value | | Table | aptos_std | No | No | High | Yes | N+1 slots | Large conflicting key-value data sets | | TableWithLength | aptos_std | No | Yes | High | No | N+2 slots | Large data sets with lots of reads and low writes | | SmartTable | aptos_std | No | Yes | High | Yes | Variable | Large key-value (not-recommended) | ## Detailed Comparison ### Vectors **Use Cases:** - Storing ordered sequences of data - Building lists, arrays, or queues - When you need to maintain insertion order - Small to medium datasets (< 1,000 elements) **When to Choose:** - **Standard Vector**: Small datasets, simple use cases, memory efficiency - **BigVector**: Large datasets, frequent additions, scalability requirements **Example:** ```move // Standard Vector for small dataset let small_list: vector = vector[1, 2, 3, 4, 5]; // BigVector for large dataset let large_list = big_vector::new(); big_vector::push_back(&mut large_list, 1); ``` ### Maps **Use Cases:** - Key-value storage and lookup - Associative data structures - When you need fast access by unique keys - Small to medium datasets (< 10,000 entries) **When to Choose:** - **SimpleMap**: Small key-value storage, maximum space efficiency, order doesn't matter - **OrderedMap**: Need order of items, and provides `O(log(n))` lookup **Example:** ```move // SimpleMap for basic storage let balances = simple_map::create(); simple_map::add(&mut balances, @0x1, 1000); // OrderedMap for ordered storage let profiles = ordered_map::new(); ordered_map::add(&mut profiles, @0x1, string::utf8(b"Alice")); ``` ### Tables **Use Cases:** - Large-scale key-value storage - When you need parallelizable operations - Hash-addressable storage requirements - Large datasets (> 10,000 entries) **When to Choose:** - **Table**: Basic hash-addressable storage, parallelization needed - **TableWithLength**: Need length tracking, want to delete entire table, not a lot of writes (breaks parallelism) - **BigOrderedMap**: Large datasets, need both ordering and scalability - **SmartTable**: Storage efficiency, iteration, length tracking (not recommended, choose BigOrderedMap) **Example:** ```move // Table for basic hash storage let balances = table::new(); table::add(&mut balances, @0x1, 1000); // SmartTable for efficient storage let scores = smart_table::new(); smart_table::add(&mut scores, @0x1, 95); ``` ## Performance Characteristics ### Time Complexity #### Worst case | Operation | Vector | BigVector | SimpleMap | OrderedMap | BigOrderedMap | Table | TableWithLength | SmartTable | |-----------|--------|-----------|-----------|------------|---------------|-------|-----------------|------------| | Append | O(1) | O(1) | N/A | N/A | N/A | N/A | N/A | N/A | | Insert (index) | O(n) | O(n) | N/A | N/A | N/A | N/A | N/A | N/A | | Insert | N/A | N/A | O(1) | O(log(n)) | O(log(n)) | O(1) | O(1) | O(n) | | Access | O(1) | O(1) | O(n) | O(log(n)) | O(log(n)) | O(1) | O(1) | O(n) | | Delete (end) | O(1) | O(1) | O(1) | O(1) | O(1) | O(1) | O(1) | O(1) | | Delete (index) | O(n) | O(n) | O(1) | O(log(n)) | O(log(n)) | O(1) | O(1) | O(1) | | Iteration | O(n) | O(n) | O(n) | O(n) | O(n) | N/A | N/A | O(n) | ### Storage Characteristics | Characteristic | Vector | BigVector | SimpleMap | OrderedMap | BigOrderedMap | Table | TableWithLength | SmartTable | |----------------|--------|-----------|-----------|------------|---------------|-------|-----------------|------------| | Storage Slots | O(1) | O(n) | O(1) | O(1) | O(n) | O(n) | O(n) | O(n) | | Parallelizable | No | *Sometimes | No | No | **Sometimes | Yes | *Sometimes | *Sometimes | | BCS serializable | Yes | No | Yes | Yes | No | No | No | No | - `*Sometimes` Table with length based types are parallelizable when there are no new storage slots created. - `**Sometimes` BigOrderedMap is parallelizable when there are no conflicts on insertion to the same storage slot. ## Decision Matrix ### For Small Datasets (< 1,000 elements) - **Sequential data**: Use **Vector** - **Key-value data**: Use **OrderedMap** over **SimpleMap** unless the data set is really small ### For Medium Datasets (1,000 - 10,000 elements) - **Sequential data**: Use **BigVector** (if scalable) - **Key-value data**: Use **Table** or **BigOrderedMap** (if scalable) ### For Large Datasets (> 10,000 elements) - **Sequential data**: Use **BigVector** - **Key-value data**: Use **Table** (basic), **TableWithLength** (need length), or **BigOrderedMap** (storage efficient) ### Special Considerations #### When You Need Parallelization - Use **BigVector**, **BigOrderedMap**, **Table** #### When You Need Length Tracking - Use **Vector**, **BigVector**, **OrderedMap**, **BigOrderedMap**, **TableWithLength** #### When You Need Ordering - Use **Vector**, **BigVector**, **OrderedMap**, or **BigOrderedMap** ## Best Practices Summary 1. **Start Simple**: Begin with standard library collections (Vector, OrderedMap) 2. **Plan for Growth**: If your dataset might grow large, consider scalable alternatives early 3. **Consider Storage Costs**: Each storage slot has a cost, so choose efficiently 4. **Match Use Case**: Choose collections that match your specific requirements 5. **Test Performance**: Benchmark with realistic data sizes before committing 6. **Monitor Gas Costs**: Different collections have different gas costs for operations 7. **Consider Parallelization**: Use parallelizable collections when possible for better performance ## Migration Paths ### Growing from Small to Large - **Vector** → **BigVector**: When you exceed ~1,000 elements - **OrderedMap** → **BigOrderedMap**: When you exceed ~1,000 entries ### Optimizing for Storage - **Table** → **SmartTable**: When you want to reduce storage slot usage - **Table** → **TableWithLength**: When you need length tracking ### Optimizing for Performance - **TableWithLength** → **Table**: When you need parallelization - **OrderedMap** → **SimpleMap**: When you don't need ordering ### Types not recommended for most use cases - **SimpleMap** → O(n) lookups mean a lot of reads. Suggested to use **OrderedMap** instead. - **SmartTable** → There are some ways that the table can become unbalanced, suggested to use **BigOrderedMap** instead. This comparison should help you make informed decisions about which collection type to use for your specific use case on Aptos. # Error Handling Errors on Aptos are fairly simple. Whenever the code aborts, an error message will be returned to the user. The error message is statically defined by the doc comment above it. All error codes are u64. By convention, errors start with E. For example: ```move /// Uh oh const E_UH_OH_BAD: u64 = 1; fun do_something() { abort E_UH_OH_BAD } ``` In this case, the error message when calling `do_something()` wil be `Uh oh`. More appropriately, errors are usually thrown by asserts. An example here below shows if you want to ensure a value is less than 10. ```move /// It's too high! const E_TOO_HIGH: u64 = 128; fun check(val: u64) { assert!(val < 10, E_TOO_HIGH) } ``` Additionally, in tests the error code can be omitted for the assertion. # Generic Types ## Overview Generics are similar to other languages, which allow for a function to specify implementation for multiple input types, output types or struct inner types. Here is a simple example of a parameterized input type: ```move /// Take a generic type `T` and use it as an input type fun print(input: &T) { std::debug::print(input) } ``` Similarly can be used to define outputs: ```move fun from_string(input: &String): T { // Implementation } ``` Structs and enums can also have generics in them. Here is an example making a wrapper: ```move struct Box { inner: T } ``` We can see in this example, the `Box` wraps the inner value. This can be used similarly in Enums or in combination with other values. > Note that if a generic is provided in a struct, it will always need to be provided. ## Type Constraints When considering generics, it's important to remember that, it can be any type, and that some consideration has to be made for the differences. For example, in the previous example, there were no abilities on the types. If you try to drop those types in the function, it will fail to compile, as the input can't be dropped. Here's an example: ```move fun print(input: T) { // ... This will fail with a compilation error } ``` But, if we add the drop constraint, only inputs of type T that can be dropped will be allowed. Which provides type safety like so: ```move fun print(input: T) { // ... This will succeed } ``` > Note that, multiple properties can be added with `+` like: ```move fun print(input: T) { // ... This will succeed } ``` ## Phantom Generics The `phantom` keyword works just like in Rust. It is only used for type checking, but not used for an underlying type. This is useful if you want types that don't conflict, but are essentially the same. Here's an example using `Coin`. ```move struct Coin { amount: u64 } ``` The `phantom` keyword is used, because the `T` value is used for domain separation, that is that each type of coin is different. But, it is not used directly in the struct's inner values. ## Tradeoffs and considerations ### Generics as Inputs Since all types that the constraints apply to, this can be tricky to use when providing into entry functions. For example if I have a function that takes in a generic and converts it to a single type, we will need to manage all possible types, including primitives to be passed in. ```move struct Storage { bytes: vector } entry fun store_type_as_bytes(caller: &signer) { // Convert to bytes let bytes: vector = //... move_to(caller, Storage { bytes }) } ``` Callers will also have to know which input types are supported, and it would be best to have abort messages explaining that it doesn't support the type. #### Example A perfect example of this is the original Coin standard on Aptos. By using a generic, the caller needs to pass in the coin types. However, the contract will not know all possible types, and cannot store them in the contract. ### Generics as Outputs Using a generic as an output has the same considerations as an input. It can be tricky to properly support different types together in a clean way. # How to Write Tests Move has a built-in unit testing framework that lets you write tests alongside your smart contract code. Tests are compiled and run by the Move compiler, giving you fast feedback without deploying to a blockchain. ## Why Test? Smart contracts manage real assets and run in an immutable environment. Bugs can be costly and difficult to fix after deployment. Testing helps you: - **Catch bugs early** before deployment - **Verify correctness** of your business logic - **Document expected behavior** through executable examples - **Enable safe upgrades** by ensuring backward compatibility ## What You'll Learn 1. **[Writing Unit Tests](writing_unit_tests.md)** - How to define test functions and use assertions 2. **[Running Unit Tests](running_unit_tests.md)** - How to execute tests with the Aptos CLI 3. **[Coverage](coverage.md)** - How to measure test coverage 4. **[Formal Verification](formal_verification.md)** - Using the Move Prover for mathematical guarantees ## Quick Start Here's a minimal test to give you a sense of the framework: ```move module my_addr::math { public fun add(a: u64, b: u64): u64 { a + b } #[test] fun test_add() { assert!(add(2, 3) == 5); assert!(add(0, 0) == 0); assert!(add(100, 200) == 300); } } ``` Run the test with: ```sh aptos move test --dev ``` # Writing Unit Tests Unit tests in Move are functions annotated with `#[test]`. They live inside your modules and have access to all private functions, making it easy to test internal logic. ## Basic Test ```move module my_addr::math { fun double(x: u64): u64 { x * 2 } #[test] fun test_double() { assert!(double(0) == 0); assert!(double(5) == 10); assert!(double(100) == 200); } } ``` ## Test Annotations ### `#[test]` Marks a function as a test. The function will be executed when you run `aptos move test`. ```move #[test] fun test_basic() { assert!(1 + 1 == 2); } ``` ### `#[test]` with Signers You can create test signers by specifying them in the annotation: ```move #[test(alice = @0x1, bob = @0x2)] fun test_with_accounts(alice: &signer, bob: &signer) { let alice_addr = std::signer::address_of(alice); let bob_addr = std::signer::address_of(bob); assert!(alice_addr != bob_addr); } ``` ### `#[expected_failure]` Tests that should abort can be marked with `#[expected_failure]`: ```move /// Division by zero should abort const E_DIVIDE_BY_ZERO: u64 = 1; fun safe_divide(a: u64, b: u64): u64 { assert!(b != 0, E_DIVIDE_BY_ZERO); a / b } #[test] #[expected_failure(abort_code = E_DIVIDE_BY_ZERO)] fun test_divide_by_zero() { safe_divide(10, 0); } ``` You can also specify the location of the abort: ```move #[test] #[expected_failure(abort_code = E_DIVIDE_BY_ZERO, location = my_addr::math)] fun test_divide_by_zero_with_location() { safe_divide(10, 0); } ``` ### `#[test_only]` Marks a function, import, or constant as available only during testing. Test-only code is excluded from production compilation. ```move #[test_only] use aptos_framework::account::create_account_for_test; #[test_only] fun setup_test_env(account: &signer) { create_account_for_test(std::signer::address_of(account)); } ``` ## Testing Resources and Global State When testing functions that interact with global storage, you typically need to set up accounts: ```move module my_addr::counter { use std::signer; struct Counter has key { value: u64, } public entry fun create(account: &signer) { move_to(account, Counter { value: 0 }); } public entry fun increment(account: &signer) acquires Counter { let addr = signer::address_of(account); let counter = &mut Counter[addr]; counter.value = counter.value + 1; } #[view] public fun get_value(addr: address): u64 acquires Counter { Counter[addr].value } #[test(account = @0x1)] fun test_counter(account: &signer) acquires Counter { let addr = signer::address_of(account); create(account); assert!(get_value(addr) == 0); increment(account); assert!(get_value(addr) == 1); increment(account); assert!(get_value(addr) == 2); } #[test(account = @0x1)] #[expected_failure] fun test_increment_without_create(account: &signer) acquires Counter { increment(account); // Should fail: no Counter exists } } ``` ## Assertions Move provides the `assert!` macro for testing conditions: ```move #[test] fun test_assertions() { // Basic equality assert!(1 + 1 == 2); // With error code (useful for debugging) assert!(1 + 1 == 2, 0); // Boolean conditions assert!(true); assert!(!false); // Comparison assert!(10 > 5); assert!(5 < 10); } ``` ## Testing Best Practices 1. **Test the happy path**: Verify that correct inputs produce correct outputs. 2. **Test error cases**: Use `#[expected_failure]` to verify that invalid inputs are properly rejected. 3. **Test edge cases**: Check boundary conditions (zero values, maximum values, empty collections). 4. **Use descriptive test names**: Name tests after what they verify (e.g., `test_transfer_insufficient_balance`). 5. **Set up and tear down**: Use `#[test_only]` helper functions for common setup. 6. **Keep tests focused**: Each test should verify one specific behavior. # Running Unit Tests The Aptos CLI provides commands for running tests, filtering which tests to run, and controlling output verbosity. ## Basic Test Execution Run all tests in your package: ```sh aptos move test ``` If your package uses named addresses with `"_"`, use the `--dev` flag to fill them automatically: ```sh aptos move test --dev ``` Or provide them explicitly: ```sh aptos move test --named-addresses my_addr=0x1 ``` ## Filtering Tests ### Run Tests Matching a Name ```sh aptos move test --filter test_transfer ``` This runs all tests whose names contain `test_transfer`. ### Run Tests in a Specific Module ```sh aptos move test --filter my_module ``` ## Test Output ### Successful Output ``` Running Move unit tests [ PASS ] 0x1::counter::test_counter [ PASS ] 0x1::counter::test_increment Test result: OK. Total tests: 2; passed: 2; failed: 0 ``` ### Failed Output ``` Running Move unit tests [ FAIL ] 0x1::counter::test_bad_assertion Error: assertion failed Test result: FAILED. Total tests: 1; passed: 0; failed: 1 ``` ## Verbose Output For more detailed output including gas usage: ```sh aptos move test --dev -v ``` ## Debugging with Print You can print values during tests using `std::debug::print`: ```move #[test_only] use std::debug::print; #[test] fun test_with_debug() { let value = 42; print(&value); // Prints during test execution assert!(value == 42); } ``` Run with verbose output to see the printed values: ```sh aptos move test --dev -v ``` ## Test Gas Limits By default, tests have a gas budget. If a test exceeds this limit, it will fail. You can increase the limit: ```sh aptos move test --dev --gas-limit 100000 ``` ## Common Issues ### "Unresolved named address" Provide the missing address with `--named-addresses` or use `--dev`. ### "Test exceeded gas limit" Your test is doing too much computation. Either optimize the test or increase the gas limit. ### Flaky tests Move tests are deterministic -- if a test passes once, it should always pass. If you see inconsistent results, check for uninitialized state or timing-dependent logic. # Coverage Test coverage measures how much of your code is exercised by tests. The Aptos CLI supports generating coverage reports for your Move packages. ## Generating Coverage Run tests with coverage enabled: ```sh aptos move test --dev --coverage ``` This runs your tests and generates a coverage report. ## Viewing Coverage After running tests with coverage, view the summary: ```sh aptos move coverage summary --dev ``` This shows the percentage of code covered by tests for each module. For a detailed source-level report: ```sh aptos move coverage source --module counter --dev ``` This shows which lines of code were covered (marked) and which were not. ## Interpreting Coverage - **High coverage** (>80%) indicates that most code paths are tested. - **Low coverage** means there are untested code paths that could contain bugs. - **100% coverage** does not guarantee correctness -- it only means every line was executed. Focus on testing: 1. All public entry points 2. All error conditions 3. Edge cases and boundary values 4. State transitions ## Best Practices 1. **Aim for high coverage**: Target at least 80% coverage for production code. 2. **Cover error paths**: Ensure all `assert!` and `abort` conditions are tested. 3. **Don't chase 100%**: Some code is trivial and doesn't need dedicated tests. 4. **Use coverage to find gaps**: Review uncovered lines to identify missing tests. 5. **Run coverage in CI**: Make coverage checks part of your continuous integration pipeline. # Formal Verification Beyond unit testing, Move supports **formal verification** through the Move Prover. The prover mathematically proves that your code satisfies specified properties, providing stronger guarantees than testing alone. ## What is Formal Verification? Unit tests check specific inputs, but formal verification checks **all possible inputs**. If the Move Prover verifies a property, it holds for every possible execution path. ## Specification Language Move uses a specification language to express properties that should hold. Specifications are written in `spec` blocks: ```move module my_addr::math { public fun add(a: u64, b: u64): u64 { a + b } spec add { // The result is the sum of the inputs ensures result == a + b; // The result is at least as large as either input ensures result >= a; ensures result >= b; // Aborts if the addition would overflow aborts_if a + b > MAX_U64; } } ``` ## Common Specification Constructs ### `ensures` Describes postconditions -- what must be true after the function executes: ```move spec get_balance { ensures result >= 0; } ``` ### `requires` Describes preconditions -- what must be true when the function is called: ```move spec transfer { requires amount > 0; requires from_addr != to_addr; } ``` ### `aborts_if` Describes under what conditions the function may abort: ```move spec withdraw { aborts_if !exists(addr); aborts_if Account[addr].balance < amount; } ``` ### `invariant` Describes properties that must always hold for a struct: ```move spec module { invariant forall addr: address where exists(addr): Counter[addr].value <= MAX_COUNT; } ``` ## Running the Prover ```sh aptos move prove --dev ``` The prover will either confirm that all specifications are satisfied or report a counterexample showing a violation. ## When to Use Formal Verification - **Financial logic**: Verify that token transfers preserve total supply. - **Access control**: Prove that only authorized accounts can perform privileged operations. - **Invariants**: Ensure that data structure invariants are maintained across all operations. ## Limitations - The prover may time out on complex functions. - Not all Move features are fully supported by the specification language. - Writing good specifications requires practice and careful thought. ## Best Practices 1. **Start with critical functions**: Focus verification on security-sensitive code. 2. **Write specifications incrementally**: Add specifications as you develop. 3. **Combine with testing**: Use unit tests for basic correctness and formal verification for stronger guarantees. 4. **Keep functions simple**: Simpler functions are easier to specify and verify. # Closures, Lambdas, and Function Values Aptos has extended the Move language with support for closures, lambdas, and function values. These features allow you to pass functions as arguments to other functions, enabling higher-order programming patterns. ## Function Types Move supports function types that describe the signature of a callable: ```move module my_addr::functional { /// Apply a function to each element of a vector public fun map(v: &vector, f: |u64| -> u64): vector { let result = vector[]; let i = 0; let len = v.length(); while (i < len) { result.push_back(f(v[i])); i = i + 1; }; result } } ``` ## Lambda Expressions Lambdas are anonymous functions defined inline: ```move fun example() { let numbers = vector[1, 2, 3, 4, 5]; let doubled = map(&numbers, |x| x * 2); // doubled = [2, 4, 6, 8, 10] } ``` ## Using with Standard Library The standard library provides functions that accept closures: ```move fun example() { let numbers = vector[1, 2, 3, 4, 5]; // for_each applies a function to each element numbers.for_each(|n| { std::debug::print(&n); }); // filter keeps elements matching a predicate let evens = numbers.filter(|n| *n % 2 == 0); } ``` ## Inline Functions with Closures Inline functions that accept closures allow the compiler to optimize away the function call overhead: ```move public inline fun do_if(condition: bool, f: || -> T, default: T): T { if (condition) { f() } else { default } } ``` ## Limitations Closures in Move have some restrictions compared to other languages: - Closures cannot capture mutable references from the enclosing scope in all cases. - Function values cannot be stored in structs or global storage. - Recursive closures are not supported. These restrictions exist to maintain Move's safety guarantees while providing the expressiveness of higher-order functions. ## When to Use Closures - **Collection processing**: Mapping, filtering, and folding over vectors. - **Custom iteration**: Providing callback functions for iteration patterns. - **Configuration**: Passing behavior as a parameter to generic functions. # Events Events are the way that allows apps and users to easily see what's going on in transactions. For all transactions, a `WriteSet` is created, which keeps track of which changes to the blockchain storage occurred in the transaction. Events can also be emitted, which are custom to the developer's choices. Let's dive into some of the details. # Defining and Emitting Events ## Choosing your events There are two types of events: handle events and module events. > Note that, handle events are mostly deprecated except in some situations, and are not parallelizable. For this reason, I will only go over module events in this book. ## Module Events To define a module event in Move, you simply need to add a `#[event]` annotation above the struct you want to be an event. Then call `emit` for each one you want to emit. Here's an example: ```move module my_addr::my_module { use std::signer; use std::string::String; #[event] struct Message { caller: address, inner: String } entry fun emit_message(caller: &signer, message: String) { aptos_framework::event::emit(Message { caller: signer::address_of(caller), inner: message }) } } ``` This will emit a message, which will show up in the writeset for later indexing. > Note that the events do not have a sequence number. # Indexing Events Once events are emitted on-chain, they need to be consumed by off-chain services. This is done through **indexing** -- the process of reading events from the blockchain and storing them in a queryable database. ## How Events Are Stored When a transaction emits events, they are included in the transaction output and stored on the blockchain. Each event contains: - The event type (the fully qualified struct name) - The event data (the serialized struct fields) - The sequence number (order within the transaction) ## Querying Events ### Using the Aptos REST API You can query events through the Aptos Node API: ``` GET /v1/accounts/{address}/events/{event_handle}/{field_name} ``` For module events (the modern `#[event]` style), you can query by event type: ``` GET /v1/events?event_type={module_address}::{module_name}::{EventStructName} ``` ### Using the Aptos Indexer The Aptos Indexer provides a GraphQL API for more complex event queries: ```graphql query GetTransferEvents { events( where: { type: { _eq: "0x1::coin::WithdrawEvent" } } order_by: { transaction_version: desc } limit: 10 ) { type data transaction_version event_index } } ``` ### Using the TypeScript SDK The Aptos TypeScript SDK provides methods for querying events: ```typescript import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; const config = new AptosConfig({ network: Network.MAINNET }); const aptos = new Aptos(config); const events = await aptos.getModuleEventsByEventType({ eventType: "0x1::coin::WithdrawEvent", }); ``` ## Building an Indexer For production applications, you typically run a custom indexer that: 1. **Streams** transactions from an Aptos fullnode. 2. **Filters** for events relevant to your application. 3. **Processes** the event data and stores it in a database. 4. **Serves** the indexed data through an API. The Aptos Indexer framework provides tools for building custom processors: - **Transaction Stream Service**: Streams raw transaction data. - **Custom Processors**: Parse and index specific event types. - **PostgreSQL Storage**: Store indexed data for querying. ## Best Practices 1. **Design events for indexing**: Include all data that off-chain services will need. 2. **Use consistent event naming**: Make event types easy to filter and query. 3. **Index critical events**: Ensure all important state changes are indexed. 4. **Handle reorgs**: Your indexer should handle chain reorganizations gracefully. 5. **Monitor indexer health**: Set up alerts for indexing lag or failures. # Parallelization The Aptos blockchain was built originally to provide Facebook level TPS for the future of the internet. This means to achieve this scale, parallelization of execution takes a big play. We'll go over how parallelization works on Aptos, and how to code to take advantage of this. # Parallelization Basics BlockSTM is the parallel execution engine that drives Aptos. It uses what we call dynamic (or optimistic) parallelism. What this means is that all transactions are executed in parallel, and the write sets between each are compared. If they write (and then read or write) to the same storage slot, it's a conflict, and therefore those transactions must be serialized. ## How BlockSTM Works 1. **Optimistic Execution**: All transactions in a block are executed in parallel, assuming no conflicts. When all transactions complete without conflicts, the entire block can be committed without any re-execution -- this is the ideal case that BlockSTM is optimized for. 2. **Validation**: After execution, the read/write sets of each transaction are compared. 3. **Conflict Detection**: If transaction B reads a storage slot that transaction A wrote to, and A comes before B in the block ordering, this is a conflict. 4. **Re-execution**: Conflicting transactions are re-executed with the updated state. 5. **Commitment**: Once all transactions are validated, the block is committed. This approach achieves near-linear scaling with the number of CPU cores when transactions access independent state, which is the common case for most blockchain workloads. # Parallelization Considerations When considering code on Aptos for parallelization, think of these three things: 1. Where will the most conflicts occur on my codebase? 2. Can I use different accounts or addresses to ensure that there are reduced conflicts? 3. Can I use separate storage slots (such as a table or objects) to ensure that there are reduced conflicts? 4. Can I use an aggregator instead for my conflicting read / writes to a single number? # Design Patterns Design patterns are reusable solutions to common problems in Move smart contract development. This chapter covers patterns that have emerged from the Aptos ecosystem and are considered best practices. ## Patterns Covered 1. **[Account Authorization by Signer](account_authorization_by_signer.md)** - Using signers to control access to resources 2. **[Event Emission](event_emission.md)** - Emitting events for indexing and observability ## Why Patterns Matter Smart contracts are immutable once deployed and manage real assets. Using well-tested patterns helps you: - **Avoid common security pitfalls**: Patterns encode lessons learned from real-world exploits. - **Write idiomatic code**: Other developers can understand your code more easily. - **Build reliable contracts**: Patterns have been battle-tested across many projects. # Account Authorization by Signer The signer is the primary mechanism for authorization in Move. A `signer` value can only be created by the Aptos VM as part of a transaction -- it cannot be forged. This makes signers the foundation for access control in smart contracts. ## The Signer Type A `signer` represents the identity of an account that signed a transaction. It is used to: 1. **Authorize resource creation**: `move_to` requires a signer to store resources under an address. 2. **Prove identity**: Functions can verify the caller's address. 3. **Control access**: Only the signer can modify their own resources (unless explicitly delegated). ## Basic Pattern ```move module my_addr::profile { use std::signer; use std::string::String; /// Only the account owner can modify their profile struct Profile has key { name: String, bio: String, } /// Create a profile -- requires the signer's authorization public entry fun create_profile( account: &signer, name: String, bio: String, ) { move_to(account, Profile { name, bio }); } /// Update a profile -- only the owner can call this public entry fun update_bio( account: &signer, new_bio: String, ) acquires Profile { let addr = signer::address_of(account); let profile = &mut Profile[addr]; profile.bio = new_bio; } } ``` The signer ensures that only the account owner can create or modify their own profile. ## Authorization Checks ### Verifying the Caller ```move const E_NOT_ADMIN: u64 = 1; const ADMIN_ADDRESS: address = @0x1; public entry fun admin_only_action(account: &signer) { let addr = signer::address_of(account); assert!(addr == ADMIN_ADDRESS, E_NOT_ADMIN); // Perform admin action } ``` ### Multi-Signer Authorization Entry functions can require multiple signers, which enables multi-party authorization: ```move public entry fun joint_action( party_a: &signer, party_b: &signer, ) { let addr_a = signer::address_of(party_a); let addr_b = signer::address_of(party_b); // Both parties have authorized this action } ``` ## Signer vs. Address A common question is when to use `&signer` versus `address` as a function parameter: - Use **`&signer`** when the function modifies the caller's state or needs authorization. - Use **`address`** when the function only reads state. ```move /// Requires authorization -- uses signer public entry fun deposit(account: &signer, amount: u64) { ... } /// Read-only -- uses address #[view] public fun get_balance(addr: address): u64 { ... } ``` ## Capability Pattern For delegated authorization, you can use a capability pattern where a `signer` creates a capability object that can be used later: ```move module my_addr::managed_token { use std::signer; struct MintCapability has key, store { max_amount: u64, } /// Only the admin can grant mint capabilities public entry fun grant_mint_capability( admin: &signer, recipient: &signer, max_amount: u64, ) { assert!(signer::address_of(admin) == @my_addr, 1); move_to(recipient, MintCapability { max_amount }); } /// Anyone with a MintCapability can mint public entry fun mint(account: &signer, amount: u64) acquires MintCapability { let addr = signer::address_of(account); let cap = &MintCapability[addr]; assert!(amount <= cap.max_amount, 2); // Perform minting } } ``` ## Best Practices 1. **Always use signers for state changes**: Never accept a plain `address` for operations that modify state. 2. **Check addresses explicitly**: When you need a specific caller (like an admin), verify the address with `assert!`. 3. **Minimize signer scope**: Pass `&signer` (reference) rather than `signer` (by value) to prevent unintended transfers. 4. **Use capabilities for delegation**: When you need to delegate authority, create capability resources rather than passing signers around. # Event Emission Events are the primary mechanism for making contract state changes observable to off-chain services like indexers, wallets, and dApps. Well-designed events make your contract easy to integrate with. ## The Event Pattern ### Define the Event Events are structs with the `#[event]` attribute and the `drop` and `store` abilities: ```move module my_addr::marketplace { use aptos_framework::event; #[event] struct ItemListed has drop, store { seller: address, item_id: u64, price: u64, } #[event] struct ItemSold has drop, store { seller: address, buyer: address, item_id: u64, price: u64, } } ``` ### Emit the Event Use `event::emit` to emit events during function execution: ```move public entry fun list_item( seller: &signer, item_id: u64, price: u64, ) { let seller_addr = signer::address_of(seller); // Business logic... event::emit(ItemListed { seller: seller_addr, item_id, price, }); } public entry fun buy_item( buyer: &signer, seller_addr: address, item_id: u64, ) { let buyer_addr = signer::address_of(buyer); let price = get_item_price(seller_addr, item_id); // Transfer logic... event::emit(ItemSold { seller: seller_addr, buyer: buyer_addr, item_id, price, }); } ``` ## What to Include in Events Good events contain enough information for off-chain consumers to understand what happened: - **Who**: The addresses of the parties involved. - **What**: The specific action that occurred. - **Details**: Relevant data like amounts, IDs, and timestamps. ```move #[event] struct Transfer has drop, store { from: address, to: address, amount: u64, } ``` ## Event Design Guidelines 1. **One event per significant action**: Emit an event for every state change that external observers might care about. 2. **Include all relevant data**: Don't force indexers to make additional API calls to reconstruct what happened. 3. **Use descriptive names**: Event struct names should clearly describe the action (e.g., `TokenMinted`, `OrderFilled`). 4. **Keep events small**: Don't include large data structures -- include IDs and references instead. 5. **Be consistent**: Use consistent naming and field ordering across all events in your module. ## Testing Events You can verify event emission in tests: ```move #[test(seller = @0x1)] fun test_list_emits_event(seller: &signer) { list_item(seller, 1, 100); // The event is emitted during execution // Indexers will pick it up from the transaction output } ``` ## Best Practices 1. **Always emit events for entry functions**: Any user-facing action should emit an event. 2. **Don't emit events for view functions**: View functions don't change state. 3. **Use module events (not handle events)**: The `#[event]` attribute with `event::emit` is the modern approach. 4. **Document your events**: Include doc comments on event structs so consumers know what each field means. # Data Models One of the important concepts of Aptos is flexibility. Aptos provides multiple data models to allow users to pick and choose the model that is best for their use case, or even mix and match them accordingly. Move on Aptos is built around two base data models. To read more about them, see each page: * [Account Model](account_model.md) - Similar to the account model of Ethereum * [Object Model](object_model.md) - Similar to the UTXO model of Bitcoin # The Account Model The Account model of Aptos behaves where each user has their data stored in global storage. Think of this as a giant mapping between the set of `Address` and `Resource Name` to a single storage slot. TODO: Diagram There are two types of accounts today: - [User Accounts](#user-accounts) - [Resource Accounts](#resource-accounts) ## User Accounts User accounts are the standard accounts that users create to interact with the Aptos blockchain. They are denoted by the resource `0x1::account::Account` and are used to hold assets, execute transactions, and interact with smart contracts. They are generated from a signer and are associated with a public key. The public key is then hashed to create the account address. ## Resource Accounts Resource accounts are accounts that are separate of a user account. The accounts are derived from an existing account, and can have a `SignerCapability` stored in order to sign as the account. Alternatively, the signer can be rotated to `0x0` preventing anyone from authenticating as an account. # The Object Model Objects similar to resource accounts, but rather than using a `SignerCapability` instead a `ExtendRef` can be used to authenticate for the account. These have owners, and always have the resource `0x1::object::Object` stored at its address. TODO: Diagram ```mermaid graph subgraph Account 0x1::account::Account end subgraph Object subgraph 0x1::object::ObjectCore Owner end A[0x1::object::ObjectCore] B[0x42::example::Resource] C[0x1234::example2::Resource2] end Owner --> 0x1::account::Account ``` # Data Model Tradeoffs You probably are asking, when would I use one over the other? Here are some details of advantages and disadvantages of each. ## Account Model The account model is very simple where each address has direct ownership over its resources. The resources at that address can only be added with that signer. Keep in mind, we'll mention both user and resource accounts below. ### Account Model Advantages - User accounts are simple and directly tied to a key. - Resource accounts are similar to contract addresses in Eth, and can have programmatic access. - Only the signer associated with the key can write data to the account. - All resources are indexed by account, and type. Easily accessed automatically in transactions by the signer. - Creator control over how resources in an account are accessed. - Ownership based indexing is simple, the account containing the resources is the owner. ### Account Model Disadvantages - Parallelism is not as easy, requires to ensure that multiple accounts don't access a shared resource. - No programmatic access except for resource accounts. - No way to get rid of resources in an account, except through the original contract. ## Object Model The object model also has each address has resources owned by the owner of the object. This helps provide more complex ownership models, as well as some tricks for providing composability, and soul-bound resources. ### Object Model Advantages - Parallelism across objects are easy, just create separate objects for parallel tasks. - Built in ownership. - Resources are collected easily in a resource group. - With the resource group, all resources in the group get written to the write set. - Multiple resources in the resource group only cause a single storage read (less gas). - Addresses can be randomly generated or derived from the initial owner for instant access. - Programmatic signer access. - Composability is easy, NFTs own other NFTs etc. - Creator control over ownership, transfers, and other pieces. - Owner can choose to hide the object, allowing wallets or other items to hide it. ### Object Model Disadvantages - For full parallelism, addresses need to be stored off-chain and passed into functions, - Keeping track of objects can be complex. - More complex access, does require handling ownership or other access actions. - Soul-bound objects cannot be removed entirely, indexers need to ignore the resources to make them disappear. - More complex indexing needed to keep track of object owners and properties (especially with ownership chains). # Advanced Topics This section delves into specialized concepts and powerful features for experienced Aptos developers. Here, you'll explore: - **Binary Canonical Serialization (BCS):** Understand the core serialization format used by Aptos. - **Storage:** Learn about on-chain and off-chain data management strategies. - **Gas:** Optimize your contracts by understanding the gas model for execution, I/O, and storage. Mastering these topics will help you build more efficient, robust, and cost-effective applications on the Aptos blockchain. # Binary Canonical Serialization (BCS) Binary Canonical Serialization (BCS) is a method that allows for compact and efficient storage. It was invented at Diem for the purposes of a consistent signing mechanism. ## Properties ### Binary The serialization method is directly in bytes and is not human-readable. For example, for a String `hello`, it would be represented by the length of the string as a binary encoded uleb-128, followed by the UTF-8 encoded bytes of hello. e.g. `"hello" = 0x0548656C6C6F` This is different from, say, a human-readable format such as JSON which would give `"hello"` ### Canonical There is only one canonical way to represent the bytes. This ensures that signing and representation are consistent. Example: Let's consider this struct in Move: ```move module 0x42::example { struct FunStruct { a: u8, b: u8 } } ``` In JSON, the struct `{"a":1, "b": 2}` can also be represented as `{"b":2, "a":1}`. Both are interchangeable so they are not canonical. In BCS, it would be a pre-defined order, so only one would be the valid representation. However, in BCS, there is only one valid representation of that, which would be the bytes `0x0102`. `0x0201` is not canonical, and it would instead be interpreted as `{"a":2, "b":1}`. ### Non-self describing The format is not self-describing. This means that deserialization requires knowledge of the shape and how to interpret the bytes. This is in opposition to a type like JSON, which is self-describing. Example: Let's consider this struct in Move again: ```move module 0x42::example { struct FunStruct { a: u8, b: u8 } } ``` The first byte will always be interpreted as `a` then `b`. So, `0x0A00` would be `{"a":10, "b":0}` and `0x0A01` would be `{"a":10, "b":1}`. If we flip it to `0x000A` it would be `{"a":0, "b":10}`. Note that this means if I do not know what the shape of the struct is, then I do not know if this is a single `u16`, the above struct, or something else. # Details about different types - [Primitives](primitives.md) - [Sequences](sequences.md) - [Structs](structs.md) - [Enums](enums.md) - [Strings](strings.md) # BCS Primitives Here is a list of primitives, and more descriptions below. Keep in mind all numbers are stored in little-endian byte order. | Type | Number of bytes | Description | |---------------------|-----------------|-----------------------------------------| | [bool](#bool) | 1 | Boolean value (true or false) | | [u8](#u8) | 1 | Unsigned 8-bit integer | | [u16](#u16) | 2 | Unsigned 16-bit integer | | [u32](#u32) | 4 | Unsigned 32-bit integer | | [u64](#u64) | 8 | Unsigned 64-bit integer | | [u128](#u128) | 16 | Unsigned 128-bit integer | | [u256](#u256) | 32 | Unsigned 256-bit integer | | [address](#address) | 32 | Aptos Address (32-byte integer) | | [uleb128](#uleb128) | 1-32 | Unsigned little-endian base-128 integer | ## Bool A boolean is a single byte. `0x00` represents `false`, `0x01` represents `true`. All other values are defined as invalid. Examples: | Value | BCS Serialized Value | |-------|----------------------| | false | 0x00 | | true | 0x01 | ## U8 A U8 is an unsigned 8-bit integer (1 byte). Examples: | Value | BCS Serialized Value | |-------|----------------------| | 0 | 0x00 | | 1 | 0x01 | | 16 | 0x0F | | 255 | 0xFF | ## U16 A U16 is an unsigned 16-bit integer (2 bytes). Examples: | Value | BCS Serialized Value | |-------|----------------------| | 0 | 0x0000 | | 1 | 0x0001 | | 16 | 0x000F | | 255 | 0x00FF | | 256 | 0x0100 | | 65535 | 0xFFFF | ## U32 A U32 is an unsigned 32-bit integer (4 bytes). Examples: | Value | BCS Serialized Value | |------------|----------------------| | 0 | 0x00000000 | | 1 | 0x00000001 | | 16 | 0x0000000F | | 255 | 0x000000FF | | 65535 | 0x0000FFFF | | 4294967295 | 0xFFFFFFFF | ## U64 A U64 is an unsigned 64-bit integer (8 bytes). Examples: | Value | BCS Serialized Value | |----------------------|----------------------| | 0 | 0x0000000000000000 | | 1 | 0x0000000000000001 | | 16 | 0x000000000000000F | | 255 | 0x00000000000000FF | | 65535 | 0x000000000000FFFF | | 4294967295 | 0x00000000FFFFFFFF | | 18446744073709551615 | 0xFFFFFFFFFFFFFFFF | ## U128 A U128 is an unsigned 128-bit integer (16 bytes). Examples: | Value | BCS Serialized Value | |-----------------------------------------|------------------------------------| | 0 | 0x00000000000000000000000000000000 | | 1 | 0x00000000000000000000000000000001 | | 16 | 0x0000000000000000000000000000000F | | 255 | 0x000000000000000000000000000000FF | | 65535 | 0x0000000000000000000000000000FFFF | | 4294967295 | 0x000000000000000000000000FFFFFFFF | | 18446744073709551615 | 0x0000000000000000FFFFFFFFFFFFFFFF | | 340282366920938463463374607431768211455 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | ## U256 A U256 is an unsigned 256-bit integer (32 bytes). Examples: | Value | BCS Serialized Value | |--------------------------------------------------------------------------------|--------------------------------------------------------------------| | 0 | 0x0000000000000000000000000000000000000000000000000000000000000000 | | 1 | 0x0000000000000000000000000000000000000000000000000000000000000001 | | 16 | 0x000000000000000000000000000000000000000000000000000000000000000F | | 255 | 0x00000000000000000000000000000000000000000000000000000000000000FF | | 65535 | 0x000000000000000000000000000000000000000000000000000000000000FFFF | | 4294967295 | 0x00000000000000000000000000000000000000000000000000000000FFFFFFFF | | 18446744073709551615 | 0x000000000000000000000000000000000000000000000000FFFFFFFFFFFFFFFF | | 340282366920938463463374607431768211455 | 0x00000000000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | | 115792089237316195423570985008687907853269984665640564039457584007913129639935 | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | ## Address An address is the 32-byte representation of a storage slot on Aptos. It can be used for accounts, objects, and other addressable storage. Addresses, have special addresses `0x0` -> `0xA`, and then full length addresses. Note, for legacy purposes, addresses missing `0`'s in front, are extended by filling the missing bytes with `0`s. Examples: | Value | BCS Serialized Value | |--------------------------------------------------------------------|--------------------------------------------------------------------| | 0x0 | 0x0000000000000000000000000000000000000000000000000000000000000000 | | 0x1 | 0x0000000000000000000000000000000000000000000000000000000000000001 | | 0xA | 0x000000000000000000000000000000000000000000000000000000000000000A | | 0xABCDEF (Legacy shortened address) | 0x0000000000000000000000000000000000000000000000000000000000ABCDEF | | 0x0000000000000000000000000000000000000000000000000000000000ABCDEF | 0x0000000000000000000000000000000000000000000000000000000000ABCDEF | | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | ## Uleb128 A uleb128 is a variable-length integer used mainly for representing sequence lengths. It is very efficient at storing small numbers, and takes up more space as the number grows. Note that the Aptos specific implementation only supports representing a `u32` in the uleb128. Any value more than the max `u32` is considered invalid. Examples: | Value | BCS Serialized Value | |------------|----------------------| | 0 | 0x00 | | 1 | 0x01 | | 127 | 0x7F | | 128 | 0x8001 | | 240 | 0xF001 | | 255 | 0xFF01 | | 65535 | 0xFFFF03 | | 16777215 | 0xFFFFFF07 | | 4294967295 | 0xFFFFFFFF0F | # Sequences Sequences are represented as an initial uleb128 followed by a sequence of encoded types. This can include sequences nested inside each other. You can compose them together to make more complex nested vectors Detailed Example: The most trivial example is an empty sequence, which is always represented as the zero length byte `0x00`. This is for any sequence no matter the type. A more complex example is the `vector` `[0, 1, 2]`. We first encode the length, as a uleb128, which is the byte `0x03`. Then, it is followed up by the three individual u8 bytes `0x00`, `0x01`, `0x02`. This gives us an entire byte array of `0x03000102`. Examples: | Type | Value | Encoded Value | |------------------------------|-------------------------------|------------------------------| | vector\ | `[]` | 0x00 | | vector\ | `[2]` | 0x0102 | | vector\ | `[2,3,4,5]` | 0x0402030405 | | vector\ | `[true, false]` | 0x020100 | | vector\ | `[65535, 1]` | 0x02FFFF0001 | | vector> | `[[], [1], [2,3]]` | 0x03000101020203 | | vector>> | `[[[],[1]],[],[[2,3],[4,5]]]` | 0x03020001010002020203020405 | Longer examples (multi-byte uleb128 length): | Type | Value | Encoded Value | |--------------|------------------------------------------------|--------------------------------------------------| | vector\ | `[0,1,2,3,...,126,127]` | 0x8001000102...FDFEFF | | vector\ | `[0,1,2,...,4294967293,4294967294,4294967295]` | 0xFFFFFFFF0F0000000000000001...FFFFFFFEFFFFFFFFF | # Structs Structs are represented as an *ordered* list of bytes. They must always be in this same order. This makes it very simple to always interpret the bytes in a struct. Detailed example: Consider the following struct: ```move module 0x42::example { struct ExampleStruct { number: u8, vec: vector, uint16: u16 } } ``` We see here that we have mixed types. These types will *always* be interpreted in that order, and must be canonical. Here is an example of the struct: ``` ExampleStruct { number: 255, vec: vector[true, false, true], uint16: 65535 } ``` This would be encoded as each of the individual encodings in order. - `255 = 0xFF` - `[true, false, true] = 0x03010001` - `65535 = 0xFFFF` So combined they would be: `0xFF03010001FFFF` # Enums Enums allow for upgradable and different types in a compact representation. They are headed first by a type (in a uleb128), followed by the expected type values. Example: Here is an enum in Move, you can see the first value is a struct, the second is a simple value, and the third is a tuple. ```move module 0x42::example { enum ExampleStruct { T1 { number: u8, vec: vector, uint16: u16 }, T2, T3(u8, bool) } } ``` ## Struct Enums Let's start with the first type: ``` ExampleStruct::T1 { number: 1, vec: [true, false, true] uint16: 65535 } ``` This would first start with the initial uleb128 representing the type, then followed by the bytes. In this case, it is the first in the enum, so it will be represented as enum `0`. All together it is represented by: `0x000103010001FFFF`. To use the struct enum in a match statement, you can do the following: ```move module 0x42::example { enum ExampleStruct has drop { T1 { number: u8, vec: vector, uint16: u16 }, T2, T3(u8, bool) } public fun handle_example(example: ExampleStruct): u8 { match (example) { ExampleStruct::T1 { number, vec: _, uint16 } => { // Do something with the struct fields return number + uint16 as u8; // Just an example operation } _ => { abort 1 // Handle other cases } } } } ``` ## Simple Value Enums For the second type, it's simply just represented as the uleb128 representing the type for value `1`. This is useful for traditional enums that do not have any additional data. In this case, the enum is `T2`, which has no fields, and can be represented as: ``` ExampleStruct::T2 {} = 0x01 ``` To use the simple value enum in a match statement, you can do the following: ```move module 0x42::example { enum ExampleStruct has drop { T1 { number: u8, vec: vector, uint16: u16 }, T2, T3(u8, bool) } public fun handle_example(example: ExampleStruct): u8 { match (example) { ExampleStruct::T2 => { return 42; // some arbitrary value for T2 } _ => { abort 1 } } } } ``` ## Tuple Enums For the third type, it's represented as the uleb128 representing the type for value `2` followed by the tuple. The tuple can contain any types, and they will be encoded in the same way as structs, just without named fields. In this case, the tuple is `(3, true)`, and can be represented as: ``` ExampleStruct::T2(3,true) = 0x020301 ``` To use the tuple enum in a match statement, you can do the following: ```move module 0x42::example { enum ExampleStruct has drop { T1 { number: u8, vec: vector, uint16: u16 }, T2, T3(u8, bool) } public fun handle_example(example: ExampleStruct): (u8, bool) { let (start, end) = match (example) { ExampleStruct::T3(x, y) => { (x, y) } _ => { abort 1 } }; return (start, end) } } ``` # Strings Strings are a special case. They are represented as a `vector`, but only `UTF-8` valid characters are allowed. This means, it would look exactly the same as a vector. Note, that the sequence length is the number of _bytes_, not the number of _characters_. For example the string: `❤️12345` = `0x0BE29DA4EFB88F3132333435` Examples: | Value | Encoded Value | |---------|--------------------| | A | 0x0141 | | hello | 0x0568656C6C6f | | goodbye | 0x07676F6F64627965 | | ❤️ | 0x06e29da4efb88f | | 💻 | 0x03E29C8D | # Options Options are a special case of [enums](enums.md). It is simply either a `0` for a `None` option and a `1` plus the value for a `Some` option. The purpose of the option is to represent a value that may or may not be present, similar to other programming languages' `Option` or `Maybe` types. Examples: | Type | Value | Encoded Value | |----------------------|-----------------|----------------| | Option | None | 0x00 | | Option | Some(false) | 0x0100 | | Option | Some(true) | 0x0101 | | Option> | None | 0x00 | | Option> | Some([1,65535)) | 0x01020001FFFF | # Storage Aptos currently uses RocksDB for on-chain storage. The data is stored in mainly addressable storage on-chain, but the [Digital Assets Standard](../standard_libraries/non_fungible_tokens/digital_assets.md) uses off-chain storage for image and other assets. ## Types of storage - [On-chain](onchain_storage.md) - [Off-chain](offchain_storage.md) # On-chain Storage On-chain Rocks DB uses a concept of storage slots. The storage slots are accessible storage, and each slot has an associated cost with it. The slots are accessible by a hash of given inputs. There are a few types: - Resources - Resource Groups - Tables - Vectors - BigVectors - SimpleMap - OrderedMap - BigOrderedMap ## Resources Resource storage a single slot is the combination of _address_ and _resource name_. This combination gives a single slot, which storing a lot of data in that resource, can be efficient. TODO: Diagram Keep in mind that the storage deposit associated with a resource is a single slot per resource. ## Resource Groups As specified in AIP-9, resource groups are a collection of resources in a single storage slot. These are stored as a b-tree, in the storage slot, which allows for more efficient packing of data (fewer slots). This is a slight tradeoff where, if accessing multiple resources together often, will be much more efficient, but all writes send all resources in the group to the output writeset. There is also slight cost to navigate the b-tree. TODO: Diagram The most common usage for resource groups is for the `0x1::object::ObjectGroup` group for objects. ## Tables Tables are hash addressable storage based on a handle stored in a resource. Each item in a table is a single storage slot, but with the advantage that has less execution cost associated. Additionally, each storage slot can be parallelized separately. Note that by far tables are the most expensive, as you need to store both the slot for the handle and the slot for each individual table item. The basic table handle cannot be deleted, but the table items can be. The cost of the table handle's slot cannot be recovered via storage refund. TODO: Diagram Note that there is no indexed tracking of which table items are filled or not and how many there are, this must be done off-chain or with a different table variant. There are other variants of the table with different tradeoffs: - Table with length - Smart Table ### Table With Length Table with length is exactly the same as a table, but with a length value. Keep in mind that table with length can be deleted fully including the table handle. However, table with length is not parallelizable on creation or deletion of table items, because every transaction increments or decrements the length. TODO: Diagram ### Smart Table Smart table uses buckets to lower the number of storage slots used. It keeps track of the length, and it buckets items into vectors. It is additionally iterable over the course of the whole table. TODO: Diagram Note: there is a possible DDoS vector if people can create keys that end up in a single storage item. ### Vector Vectors are sequences of # Off-Chain Storage All URLs stored on Aptos can be of the following: - An HTTP or HTTPS URL e.g. https://mydomain.com/image.png - An IPFS URL e.g. ipfs://hash TODO: Followup with more info / examples ## Indexing Off-chain indexing is also very common, see https://aptos.dev for more info. TODO: Link tutorials and other information. # Gas Gas is used to measure the amount of execution, storage, and IO used for every transaction. This is to provide fairness and ensure the network runs smoothly and with high performance. Gas cost for a transaction has three parts: - The gas used - which is the amount of units used to execute the transaction (execution, storage, and IO). - The gas unit price (also sometimes called price per unit gas) - which is the amount the fee payer chose to pay to prioritize the transaction. - The storage refund - which is based on the number of storage slots deleted. The total gas cost (fee or refund) is calculated as: Keep in mind, the storage refund can be greater than the other side, so you can actually gain gas in a transaction by freeing storage slots. ## How is gas used calculated? Gas used is calculated by three parts: 1. The number of [execution units](execution.md), which vary based on the operations taken. 2. The number of [IO units](io.md), which vary based on which storage slots are read or written. 3. The [storage deposit](storage.md), which is the cost for each new storage slot created. Storage deposit is returned to users. Each one of these has an individual upper bound, so keep that in mind if you have a task that uses any one of these heavily. # Execution Execution gas is measured by the amount of work that an operation does on-chain. Keep in mind that this includes things like: - Iterating over items in a table - Performing cryptographic operations - Unpacking input arguments - Mathematical operations - Control flow operations # IO IO operations are the amount of reads and writes done to existing storage slots (or storage slots after they are created). This includes things like: - Writing to existing storage - Adding values to a vector and saving it in storage - Deleting values from a vector and saving it in storage - Reading existing storage slots # Storage Deposit / Refund Storage deposit (and subsequent refund) are based on the number of storage slots _created_ or _deleted_. Each storage slot has a cost associated with it, which is deposited to encourage freeing of storage slots later. The storage slots that are freed will then refund the fee payer of the transaction, possibly even making their transaction be free or even pay them for the transaction. TODO: More details # Configuring Gas There are only 2 knobs you can turn today on Aptos to configure gas cost for a transaction. - Max gas amount - The number of gas units you are willing to spend on a transaction. - Gas unit price - The number of octas (APT*10^-8) from a minimum of 100. ## Max gas amount If a max gas amount is too low for a transaction to complete, `OUT_OF_GAS` will be returned, and the transaction will abort. This means that nothing will happen in the transaction, but you will still pay the gas. Setting this to a reasonable bound prevents you from spending too much on a single transaction. Range: 2 - ??? (TODO: Put number or how to get it from gas config) If you do not have enough APT to fulfill the gas deposit (max gas amount * gas unit price), you will get an error of `INSUFFICIENT_BALANCE_FOR_TRANSACTION_FEE`. This means you will need more APT in your account, or to adjust one of these two values. ## Gas unit price Gas unit price is the amount you're willing to pay per gas unit on a transaction. Higher values are prioritized over lower values. When choosing a gas unit price, keep in mind that your account needs enough APT to pay for the entire max gas amount times the gas unit price. Range: 100 - ??? (TODO: put number or how to get it from gas config) ## Gas Config TODO: Explain how to get it directly from on-chain. # Standard Libraries Aptos provides multiple standard libraries at the given addresses: - 0x1 - [MoveStdLib](move_stdlib.md) - [AptosStdLib](aptos_stdlib.md) - [AptosFramework](aptos_framework.md) - 0x3 - AptosToken - Legacy NFT standard (not suggested for any future usage) - 0x4 - AptosTokenObjects - Digital Assets -> New NFT and semi-fungible token standard ## Standards These standards are built into the 0x1 and 0x4 addresses: - Fungible Tokens - [Coins (Legacy)](fungible_tokens/coin.md) - [Fungible Assets](fungible_tokens/fungible_assets.md) - Non-fungible and Semi-fungible Tokens - [Digital Assets](non_fungible_tokens/digital_assets.md) # Additional Libraries The Aptos ecosystem also has third-party libraries and tools: - [Aptos Token Objects](https://github.com/aptos-labs/aptos-framework/tree/main/aptos-move/framework/aptos-token-objects) -- The modern NFT and semi-fungible token framework - [Pyth Network](https://pyth.network/) -- Price oracle data feeds - [Switchboard](https://switchboard.xyz/) -- Decentralized oracle network - [Econia](https://github.com/econia-labs/econia) -- On-chain order book DEX protocol # Coin (Legacy) Coin is the original standard for fungible tokens on Aptos. It is being replaced with the [fungible asset standard](fungible_assets.md), which allows for much more flexibility and more fine-grained control over the fungible tokens. Note that APT is a Coin being [migrated to the fungible asset](#migrating-coins-to-fungible-asset). TODO: Simple example ## Migrating coins to Fungible asset TODO # Fungible Assets The fungible asset standard is a fungible token standard built for flexibility and control over supplies and their properties. Note that this is built upon the [object model](../../data_models/object_model.md). ## Dispatchable Fungible Assets TODO: More details # Digital Assets The digital asset standard is a token standard for both non-fungible and semi-fungible tokens. It's important to note that combining the fungible asset standard and the digital asset standard create semi-fungible tokens. Note that this is built upon the [object model](../../data_models/object_model.md). # Move Standard Library (MoveStdlib) The Move Standard Library (`std`) provides the foundational types and utilities used across all Move programs. It is deployed at address `0x1`. Source: [aptos-framework/move-stdlib](https://github.com/aptos-labs/aptos-framework/tree/main/aptos-move/framework/move-stdlib) ## Key Modules ### `std::string` UTF-8 encoded strings. ```move use std::string::{Self, String}; let greeting: String = string::utf8(b"Hello, Aptos!"); let length = string::length(&greeting); let is_empty = string::is_empty(&greeting); ``` ### `std::vector` Dynamic arrays (see [Collections > Vectors](../collections/vectors.md) for details). ```move use std::vector; let v = vector[1, 2, 3]; let len = v.length(); ``` ### `std::option` Optional values -- a value that may or may not be present. ```move use std::option::{Self, Option}; let some_val: Option = option::some(42); let none_val: Option = option::none(); let val = option::extract(&mut some_val); // 42 ``` ### `std::signer` Functions for working with transaction signers. ```move use std::signer; fun get_addr(account: &signer): address { signer::address_of(account) } ``` ### `std::error` Standard error categories for structured error reporting. ```move use std::error; const E_NOT_FOUND: u64 = 1; fun example() { abort error::not_found(E_NOT_FOUND) } ``` Error categories include: `not_found`, `already_exists`, `permission_denied`, `invalid_argument`, `invalid_state`, `out_of_range`, `resource_exhausted`, and `internal`. ### `std::bcs` Binary Canonical Serialization (see [BCS chapter](../bcs/intro.md)). ```move use std::bcs; let bytes = bcs::to_bytes(&42u64); ``` ### `std::hash` Cryptographic hash functions. ```move use std::hash; let data = b"hello"; let hash = hash::sha3_256(data); ``` ### `std::debug` Debugging utilities (test-only). ```move #[test_only] use std::debug; #[test] fun test_debug() { debug::print(&42); } ``` ### `std::fixed_point32` / `std::fixed_point64` Fixed-point arithmetic for precise financial calculations. ```move use std::fixed_point32::{Self, FixedPoint32}; let half = fixed_point32::create_from_rational(1, 2); let result = fixed_point32::multiply_u64(100, half); // 50 ``` ## Summary | Module | Purpose | |---|---| | `std::string` | UTF-8 strings | | `std::vector` | Dynamic arrays | | `std::option` | Optional values | | `std::signer` | Transaction signer utilities | | `std::error` | Structured error codes | | `std::bcs` | Serialization | | `std::hash` | Cryptographic hashing | | `std::debug` | Test-time debugging | | `std::fixed_point32/64` | Fixed-point math | # Aptos Standard Library (AptosStdlib) The Aptos Standard Library (`aptos_std`) extends the Move Standard Library with data structures and utilities specific to the Aptos blockchain. It is deployed at address `0x1`. Source: [aptos-framework/aptos-stdlib](https://github.com/aptos-labs/aptos-framework/tree/main/aptos-move/framework/aptos-stdlib) ## Key Modules ### Data Structures #### `aptos_std::smart_table` A scalable hash table designed for large datasets. Entries are stored in separate storage slots for parallelism. ```move use aptos_std::smart_table::{Self, SmartTable}; let table = smart_table::new(); smart_table::add(&mut table, @0x1, 100); let value = smart_table::borrow(&table, @0x1); ``` #### `aptos_std::smart_vector` A vector that automatically splits into a table when it grows beyond a threshold. ```move use aptos_std::smart_vector::{Self, SmartVector}; let sv = smart_vector::new(); smart_vector::push_back(&mut sv, 42); ``` #### `aptos_std::simple_map` A simple key-value map backed by a vector. Best for small collections. ```move use aptos_std::simple_map::{Self, SimpleMap}; let map = simple_map::new(); simple_map::add(&mut map, key, value); ``` #### `aptos_std::table` A hash-based key-value store where each entry occupies its own storage slot. ```move use aptos_std::table::{Self, Table}; let t = table::new(); table::add(&mut t, @0x1, 100); ``` ### Cryptography #### `aptos_std::ed25519` Ed25519 signature verification. #### `aptos_std::multi_ed25519` Multi-signature Ed25519 verification. #### `aptos_std::secp256k1` Secp256k1 elliptic curve operations (compatible with Ethereum keys). ### Utilities #### `aptos_std::type_info` Get runtime type information for generic types. ```move use aptos_std::type_info; fun get_type_name(): String { type_info::type_name() } ``` #### `aptos_std::math64` / `aptos_std::math128` Common math operations like `min`, `max`, `pow`, and `sqrt`. ```move use aptos_std::math64; let max_val = math64::max(10, 20); // 20 let min_val = math64::min(10, 20); // 10 ``` #### `aptos_std::comparator` Generic comparison utilities for ordering values. #### `aptos_std::string_utils` Extended string formatting and manipulation. ```move use aptos_std::string_utils; let formatted = string_utils::to_string(&42u64); ``` ## Summary | Module | Purpose | |---|---| | `smart_table` | Scalable hash table | | `smart_vector` | Auto-scaling vector | | `simple_map` | Small key-value map | | `table` | Large key-value store | | `ed25519` | Ed25519 signatures | | `type_info` | Runtime type info | | `math64/128` | Math utilities | | `string_utils` | String formatting | | `comparator` | Value comparison | # Aptos Framework The Aptos Framework (`aptos_framework`) provides the core functionality of the Aptos blockchain, including accounts, coins, objects, governance, and staking. It is deployed at address `0x1` and depends on both MoveStdlib and AptosStdlib. Source: [aptos-framework/aptos-framework](https://github.com/aptos-labs/aptos-framework/tree/main/aptos-move/framework/aptos-framework) ```mermaid graph A[MoveStdLib] --> B[AptosStdLib] B --> C[AptosFramework] C --> D[AptosToken] C --> E[AptosTokenObjects] ``` ## Key Modules ### Account Management #### `aptos_framework::account` Manages account creation, authentication key rotation, and signer capabilities. ```move use aptos_framework::account; // In tests #[test_only] fun setup(account: &signer) { account::create_account_for_test(signer::address_of(account)); } ``` ### Token Standards #### `aptos_framework::coin` The legacy fungible token standard (see [Coin (Legacy)](fungible_tokens/coin.md)). #### `aptos_framework::fungible_asset` The modern fungible token standard (see [Fungible Asset](fungible_tokens/fungible_assets.md)). #### `aptos_framework::primary_fungible_store` Primary stores for fungible assets, automatically created per account. ### Object Framework #### `aptos_framework::object` The object model for creating and managing on-chain objects (see [Object Model](../data_models/object_model.md)). ```move use aptos_framework::object::{Self, Object, ConstructorRef}; fun create_my_object(creator: &signer): ConstructorRef { object::create_object(signer::address_of(creator)) } ``` ### Events #### `aptos_framework::event` Event emission for indexing and observability. ```move use aptos_framework::event; #[event] struct MyEvent has drop, store { value: u64, } fun emit_event() { event::emit(MyEvent { value: 42 }); } ``` ### Randomness #### `aptos_framework::randomness` On-chain randomness for games, lotteries, and fair selection. ```move use aptos_framework::randomness; #[randomness] entry fun random_action(account: &signer) { let random_number = randomness::u64_integer(); } ``` ### Governance and Staking #### `aptos_framework::aptos_governance` On-chain governance for protocol upgrades and parameter changes. #### `aptos_framework::staking` Validator staking and delegation. ### Utilities #### `aptos_framework::timestamp` Access the current blockchain timestamp. ```move use aptos_framework::timestamp; fun get_current_time(): u64 { timestamp::now_seconds() } ``` #### `aptos_framework::aptos_coin` The native APT token module. ## Summary | Module | Purpose | |---|---| | `account` | Account management | | `coin` | Legacy fungible tokens | | `fungible_asset` | Modern fungible tokens | | `object` | Object model | | `event` | Event emission | | `randomness` | On-chain randomness | | `timestamp` | Blockchain time | | `aptos_coin` | Native APT token | | `aptos_governance` | On-chain governance | | `staking` | Validator staking | # Building on Aptos with LLMs Large Language Models (LLMs) are transforming how developers build, audit, and interact with blockchain applications. This chapter covers how to effectively use LLMs as development tools when building on Aptos, and how to build AI-powered applications that integrate with the Aptos blockchain. ## Why LLMs and Aptos? The Aptos ecosystem is uniquely well-suited for LLM-assisted development: 1. **Move is a well-structured language**: Move's strict type system, explicit resource management, and formal specification support make it easier for LLMs to generate correct code. 2. **Safety properties are compiler-enforced**: Even if an LLM generates imperfect code, the Move compiler catches many classes of bugs. 3. **The llms.txt standard**: This book and the Aptos developer documentation are published in the [llms.txt](https://llmstxt.org/) format, making them directly consumable by LLMs as context. 4. **Growing AI agent ecosystem**: Aptos supports on-chain AI agents that can autonomously manage assets, execute trades, and interact with DeFi protocols. ## What You'll Learn 1. **[Using LLMs to Write Move Code](writing_move_with_llms.md)** - Prompt engineering and best practices for generating smart contracts 2. **[Using LLMs to Audit Smart Contracts](auditing_with_llms.md)** - Leveraging AI for security review 3. **[Building AI Agents on Aptos](ai_agents_on_aptos.md)** - Creating autonomous on-chain agents 4. **[The llms.txt Standard and Aptos](llms_txt_and_aptos.md)** - How Aptos documentation is optimized for LLM consumption ## The AI-Blockchain Convergence The intersection of AI and blockchain creates powerful new possibilities: - **AI agents** that manage on-chain portfolios and execute DeFi strategies - **LLM-powered developer tools** that generate, test, and audit Move code - **Natural language interfaces** to blockchain operations - **Automated smart contract verification** using AI-assisted formal methods Aptos is at the forefront of this convergence, with active development in AI agent frameworks, LLM-optimized documentation, and on-chain infrastructure for autonomous agents. # Using LLMs to Write Move Code LLMs can significantly accelerate Move development when used effectively. This section covers best practices for prompting LLMs to generate correct, secure, and idiomatic Move code. ## Providing Context The most important factor for high-quality Move code generation is giving the LLM sufficient context about the Aptos ecosystem and Move language. ### Using llms.txt The Aptos Book and Aptos developer documentation are published in the [llms.txt](https://llmstxt.org/) format. You can provide this content directly to an LLM: ``` # In your prompt or system message, include: # - The relevant chapter of the Aptos Book (e.g., the structs or events chapter) # - The Aptos Framework documentation for modules you plan to use # - Your existing code for context ``` ### Structuring Your Prompts A well-structured prompt for Move code generation includes: 1. **Goal**: What the contract should do 2. **Context**: Relevant Aptos/Move concepts 3. **Constraints**: Security requirements, gas considerations 4. **Examples**: Similar code patterns from the Aptos ecosystem #### Example Prompt ``` Write a Move module for a simple escrow contract on Aptos. Requirements: - Seller deposits an item (represented as a u64 token ID) - Buyer deposits APT as payment - Either party can cancel before both sides deposit - When both sides deposit, the swap executes automatically - Use the Aptos object model for the escrow - Emit events for all state changes - Include comprehensive error codes - Follow the naming conventions from the Aptos Book Use these Aptos framework modules: - aptos_framework::object for the escrow object - aptos_framework::event for events - aptos_framework::fungible_asset for APT transfers ``` ## Effective Prompt Patterns ### Pattern 1: Incremental Development Build contracts step by step: ``` Step 1: "Define the struct and error codes for a voting contract" Step 2: "Add the create_proposal entry function" Step 3: "Add the vote entry function with duplicate vote prevention" Step 4: "Add view functions for proposal status" Step 5: "Add unit tests for the happy path" Step 6: "Add unit tests for error cases" ``` ### Pattern 2: Code Review Ask the LLM to review generated code: ``` Review this Move module for: 1. Security vulnerabilities 2. Missing error handling 3. Gas optimization opportunities 4. Incorrect use of abilities 5. Missing events [paste your code] ``` ### Pattern 3: Test Generation LLMs excel at generating comprehensive tests: ``` Write unit tests for this Move module. Include: - Happy path tests for each entry function - Error case tests with #[expected_failure] - Edge case tests (zero values, max values, empty collections) - Multi-user interaction tests [paste your module] ``` ### Pattern 4: Explain and Refactor Use LLMs to understand existing code: ``` Explain what this Move module does, including: - The purpose of each struct and its abilities - What each function does and when it should be called - The security model (who can call what) - Any potential issues [paste existing code] ``` ## Common Pitfalls ### 1. Outdated Syntax LLMs may generate Move code using outdated syntax. Key things to watch for: - **Resource indexing**: Modern Move uses `Resource[addr]` instead of `borrow_global(addr)` - **Vector methods**: Modern Move supports `v.push_back(x)` instead of `vector::push_back(&mut v, x)` - **Event emission**: Use `#[event]` with `event::emit()` instead of legacy event handles ### 2. Missing Abilities LLMs sometimes forget or misapply abilities. Always check: - Does the struct need `key` for global storage? - Does the struct need `store` to be embedded in other stored types? - Should the struct lack `copy` to prevent duplication (for assets)? - Should the struct lack `drop` to prevent accidental loss (for receipts)? ### 3. Incorrect Error Handling LLMs may use generic error codes or forget error conditions. Ensure: - Every abort has a descriptive error constant - Error constants follow the `E_` naming convention - Doc comments describe the error condition - All preconditions are checked ### 4. Security Assumptions LLMs may make incorrect security assumptions. Always verify: - Signer authorization is required for state changes - View functions don't modify state - Entry function parameters are validated - Access control is enforced ## Workflow: LLM-Assisted Development A recommended workflow for using LLMs in Move development: 1. **Spec**: Describe what you want to build in natural language 2. **Generate**: Use an LLM to generate the initial code 3. **Review**: Manually review the generated code for correctness 4. **Compile**: Run `aptos move compile` to check for compilation errors 5. **Test**: Generate and run tests with `aptos move test` 6. **Audit**: Use the LLM to review the code for security issues 7. **Iterate**: Refine based on test results and review feedback ## LLM Tool Integration Several development tools integrate LLMs with Move development: - **Cursor / VS Code with AI**: Use the Aptos Move Analyzer extension alongside an AI assistant - **GitHub Copilot**: Provides inline Move code suggestions (ensure you have Aptos context loaded) - **Claude / GPT with documentation**: Feed llms.txt content for context-aware code generation ## Best Practices Summary 1. **Always provide Aptos-specific context**: Don't assume the LLM knows the latest Move syntax. 2. **Review all generated code**: LLMs are assistants, not replacements for developer judgment. 3. **Compile and test**: Always verify generated code compiles and passes tests. 4. **Use incremental prompts**: Build complex contracts step by step. 5. **Cross-reference with documentation**: Verify LLM suggestions against official Aptos docs. 6. **Keep the LLM updated**: Provide the latest llms.txt content for current API information. # Using LLMs to Audit Smart Contracts LLMs can serve as a valuable first line of defense in smart contract security review. While they don't replace professional auditors, they can catch common vulnerabilities and improve code quality before a formal audit. ## The AI Audit Workflow ### Step 1: Initial Scan Feed your entire contract to the LLM with a structured audit prompt: ``` Perform a security audit of this Move smart contract. Check for: 1. Access control issues (missing signer checks, unauthorized state changes) 2. Resource safety (potential for resource duplication or loss) 3. Integer overflow/underflow 4. Reentrancy or cross-module interaction issues 5. Missing input validation 6. Incorrect use of abilities 7. Missing or incorrect error codes 8. Event emission completeness 9. Gas optimization opportunities 10. Upgrade safety (backward compatibility) For each issue found, provide: - Severity (Critical / High / Medium / Low / Informational) - Location (function name and line) - Description of the issue - Recommended fix [paste your contract] ``` ### Step 2: Focused Analysis Drill into specific areas: ``` Analyze the access control model of this contract: - Who can call each entry function? - What state can each caller modify? - Are there any privilege escalation paths? - Is the admin role properly protected? [paste your contract] ``` ### Step 3: Test Gap Analysis Identify missing test coverage: ``` Given this contract and its test suite, identify: - Which functions lack test coverage? - Which error paths are not tested? - Which edge cases are missing? - What interaction patterns between functions should be tested? Contract: [paste contract] Tests: [paste tests] ``` ## Common Vulnerabilities LLMs Can Detect ### Missing Signer Authorization ```move // VULNERABLE: No signer check -- anyone can call this public entry fun set_admin(new_admin: address) acquires Config { let config = &mut Config[@my_addr]; config.admin = new_admin; } // FIXED: Requires signer authorization public entry fun set_admin(current_admin: &signer, new_admin: address) acquires Config { let config = &mut Config[@my_addr]; assert!(signer::address_of(current_admin) == config.admin, E_NOT_ADMIN); config.admin = new_admin; } ``` ### Unchecked Arithmetic ```move // VULNERABLE: Could overflow public fun add_balance(account: &mut Account, amount: u64) { account.balance = account.balance + amount; } // FIXED: Check for overflow public fun add_balance(account: &mut Account, amount: u64) { let new_balance = account.balance + amount; assert!(new_balance >= account.balance, E_OVERFLOW); account.balance = new_balance; } ``` ### Missing Existence Checks ```move // VULNERABLE: Will abort with an unhelpful error if resource doesn't exist public fun get_balance(addr: address): u64 acquires Balance { Balance[addr].amount } // FIXED: Explicit existence check with descriptive error public fun get_balance(addr: address): u64 acquires Balance { assert!(exists(addr), E_NO_BALANCE); Balance[addr].amount } ``` ## Limitations of LLM Auditing LLMs have important limitations as auditors: 1. **No execution**: LLMs reason about code statically -- they cannot run it. 2. **Context window**: Very large contracts may exceed the LLM's context window. 3. **Novel vulnerabilities**: LLMs may miss novel attack vectors not present in training data. 4. **False positives**: LLMs may flag correct code as vulnerable. 5. **No formal proofs**: LLMs provide heuristic analysis, not mathematical proofs. ## Combining AI and Human Auditing The most effective approach combines AI and human review: | Stage | Tool | Purpose | |---|---|---| | Development | LLM-assisted coding | Generate initial code with best practices | | Self-review | LLM audit | Catch obvious issues before submission | | Compiler | `aptos move compile` | Type checking and ability verification | | Testing | `aptos move test` | Runtime correctness verification | | Formal verification | `aptos move prove` | Mathematical guarantees for critical properties | | Professional audit | Human auditors | Deep analysis of business logic and novel attacks | ## Best Practices 1. **Use LLMs as a first pass**: Run AI audits before spending on professional auditors. 2. **Don't rely solely on AI**: Always have human review for production contracts. 3. **Provide full context**: Give the LLM all related modules, not just the one being audited. 4. **Iterate on findings**: Fix issues the LLM identifies, then re-audit. 5. **Document assumptions**: Ask the LLM to list all assumptions it made during the audit. 6. **Check against known patterns**: Ask the LLM to verify your contract follows established design patterns. # Building AI Agents on Aptos AI agents are autonomous programs that can interact with the blockchain on behalf of users. Aptos provides infrastructure for building AI agents that can manage digital assets, execute DeFi strategies, and participate in governance. ## What is an On-Chain AI Agent? An on-chain AI agent typically consists of: 1. **An off-chain LLM** that makes decisions based on on-chain data 2. **A wallet/account** that the agent controls for executing transactions 3. **Smart contracts** that define what the agent can do and enforce guardrails 4. **An execution loop** that reads state, decides on actions, and submits transactions ``` ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ │ │ │ │ │ │ LLM Engine │────▶│ Agent Backend │────▶│ Aptos Chain │ │ (Decision) │ │ (Execution) │ │ (Settlement) │ │ │◀────│ │◀────│ │ └─────────────────┘ └──────────────────┘ └─────────────────┘ ``` ## Agent Architecture ### 1. Read On-Chain State Use the Aptos SDK to read blockchain state: ```typescript import { Aptos, AptosConfig, Network } from "@aptos-labs/ts-sdk"; const aptos = new Aptos(new AptosConfig({ network: Network.MAINNET })); // Read account balance const balance = await aptos.getAccountAPTAmount({ accountAddress: agentAddress, }); // Read contract state via view function const state = await aptos.view({ payload: { function: "0x1::coin::balance", typeArguments: ["0x1::aptos_coin::AptosCoin"], functionArguments: [agentAddress], }, }); ``` ### 2. LLM Decision Making Feed the on-chain state to an LLM for decision-making: ```typescript const prompt = ` You are an AI agent managing a DeFi portfolio on Aptos. Current state: - APT balance: ${balance} - Current positions: ${JSON.stringify(positions)} - Market conditions: ${JSON.stringify(marketData)} Available actions: 1. swap(token_in, token_out, amount) - Swap tokens on a DEX 2. provide_liquidity(pool, amount) - Add liquidity to a pool 3. stake(amount) - Stake APT with a validator 4. hold() - Take no action Decide the best action based on the current state. Return your decision as JSON: { "action": "...", "params": {...}, "reasoning": "..." } `; const decision = await llm.complete(prompt); ``` ### 3. Execute Transactions Submit the decided transaction to the blockchain: ```typescript import { Account, Ed25519PrivateKey } from "@aptos-labs/ts-sdk"; // Agent's account (secured in practice) const agentAccount = Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(agentPrivateKey), }); // Build and submit the transaction const transaction = await aptos.transaction.build.simple({ sender: agentAccount.accountAddress, data: { function: "0x1::coin::transfer", typeArguments: ["0x1::aptos_coin::AptosCoin"], functionArguments: [recipientAddress, amount], }, }); const committedTxn = await aptos.signAndSubmitTransaction({ signer: agentAccount, transaction, }); await aptos.waitForTransaction({ transactionHash: committedTxn.hash, }); ``` ## Smart Contract Guardrails For safety, define on-chain guardrails that limit what an agent can do: ```move module my_addr::agent_vault { use std::signer; use aptos_framework::timestamp; /// Agent is not active const E_AGENT_INACTIVE: u64 = 1; /// Transfer exceeds per-transaction limit const E_TRANSFER_TOO_LARGE: u64 = 2; /// Transfer would exceed daily spending limit const E_DAILY_LIMIT_EXCEEDED: u64 = 3; /// Seconds in a day, used for daily spend reset const SECONDS_PER_DAY: u64 = 86400; struct AgentConfig has key { owner: address, daily_spent: u64, last_reset_timestamp: u64, max_transfer: u64, max_daily_spend: u64, is_active: bool, } /// Only the agent owner can configure the agent public entry fun configure_agent( owner: &signer, max_transfer: u64, max_daily_spend: u64, ) { let owner_addr = signer::address_of(owner); move_to(owner, AgentConfig { owner: owner_addr, daily_spent: 0, last_reset_timestamp: timestamp::now_seconds(), max_transfer, max_daily_spend, is_active: true, }); } /// Reset daily spending if a new day has started fun maybe_reset_daily_spent(config: &mut AgentConfig) { let now = timestamp::now_seconds(); if (now - config.last_reset_timestamp >= SECONDS_PER_DAY) { config.daily_spent = 0; config.last_reset_timestamp = now; }; } /// The agent calls this -- guardrails enforce limits public entry fun agent_transfer( agent: &signer, to: address, amount: u64, ) acquires AgentConfig { let agent_addr = signer::address_of(agent); let config = &mut AgentConfig[agent_addr]; assert!(config.is_active, E_AGENT_INACTIVE); assert!(amount <= config.max_transfer, E_TRANSFER_TOO_LARGE); // Reset daily spending if a new day has started maybe_reset_daily_spent(config); assert!(config.daily_spent + amount <= config.max_daily_spend, E_DAILY_LIMIT_EXCEEDED); config.daily_spent = config.daily_spent + amount; // Execute transfer... } /// Owner can pause the agent at any time public entry fun pause_agent(owner: &signer) acquires AgentConfig { let config = &mut AgentConfig[signer::address_of(owner)]; config.is_active = false; } } ``` ## Agent Frameworks on Aptos Several frameworks simplify building AI agents on Aptos: ### Move Agent Kit The Move Agent Kit provides tools for building AI agents that interact with the Aptos blockchain. It integrates with popular LLM frameworks and provides pre-built tools for common on-chain actions. ### Key features: - Pre-built Aptos transaction tools for LLM agents - Integration with LangChain, Eliza, and other agent frameworks - Token transfer, NFT minting, and DeFi interaction tools - Wallet management and transaction signing ### Eliza Framework The Eliza Framework is a multi-agent simulation framework that supports Aptos blockchain interactions. Agents can: - Manage Aptos wallets - Execute token transfers - Interact with DeFi protocols - Monitor on-chain events ## Use Cases ### Portfolio Manager An AI agent that: - Monitors token prices and portfolio performance - Rebalances holdings based on predefined strategies - Executes swaps on DEXes for optimal pricing - Reports performance to the owner ### NFT Trader An AI agent that: - Monitors NFT marketplace listings - Evaluates NFT value using image analysis and market data - Places bids or buys undervalued NFTs - Lists overvalued NFTs for sale ### Governance Participant An AI agent that: - Reads governance proposals - Analyzes proposal impact using LLM reasoning - Votes according to predefined principles - Reports voting activity and reasoning ## Security Considerations 1. **Key management**: Agent private keys must be securely stored (use HSMs or secure enclaves). 2. **Spending limits**: Always enforce on-chain guardrails for maximum transaction amounts. 3. **Kill switch**: Implement an owner-controlled pause mechanism. 4. **Monitoring**: Log all agent actions and set up alerts for unusual behavior. 5. **Testing**: Thoroughly test agent behavior on devnet before mainnet deployment. 6. **Prompt injection**: Protect against prompt injection attacks if the agent processes external data. ## Best Practices 1. **Start small**: Begin with simple, low-risk actions and gradually increase agent capabilities. 2. **Use guardrails**: Always implement on-chain spending limits and pause mechanisms. 3. **Monitor continuously**: Watch agent behavior and be ready to intervene. 4. **Test extensively**: Run agents on devnet with simulated market conditions. 5. **Separate concerns**: Keep the LLM decision-making separate from transaction execution. 6. **Audit agent contracts**: The smart contracts that enforce guardrails are security-critical. # The llms.txt Standard and Aptos The [llms.txt](https://llmstxt.org/) standard provides a way for websites and documentation to be directly consumable by Large Language Models. The Aptos ecosystem embraces this standard to make blockchain development more accessible to AI-assisted workflows. ## What is llms.txt? The llms.txt standard defines a simple, structured text format that: - Is easy for LLMs to parse and understand - Contains all essential information without HTML, JavaScript, or CSS noise - Follows a consistent hierarchy that maps well to LLM context windows - Can be directly included in prompts or system messages ## How the Aptos Book Uses llms.txt This book is built with `mdbook-llms-txt-tools`, which automatically generates llms.txt output alongside the HTML version. The build process creates two LLM-optimized outputs: ### `llms.txt` - Summary Format A concise overview of the book's structure and key concepts. Useful for giving an LLM a high-level understanding of the Aptos ecosystem. ### `llms-full.txt` - Complete Content The entire book's content in a flat, LLM-friendly format. Useful for giving an LLM deep knowledge about specific topics. ## Using llms.txt in Your Workflow ### Providing Context to LLMs When working with an LLM on Aptos development, include relevant sections of the llms.txt output: ``` System prompt: You are an expert Aptos/Move developer. Use the following reference material: [paste relevant sections from llms-full.txt] User prompt: Write a Move module for a token vesting contract... ``` ### Building Custom AI Tools You can fetch the llms.txt file programmatically to build AI-powered tools: ```typescript // Fetch the Aptos Book content for LLM context // This book is hosted at aptos-book.com; the llms-full.txt endpoint // serves the entire book in a single LLM-friendly text file. const response = await fetch("https://aptos-book.com/llms-full.txt"); const aptosBookContent = await response.text(); // Include in your LLM prompt const prompt = ` Reference: ${aptosBookContent} Task: Generate a Move module for... `; ``` ### IDE Integration Configure your AI-powered IDE to include Aptos documentation: 1. Add the llms.txt URL to your editor's AI context sources 2. Reference specific chapters when asking for code assistance 3. Use the structured format to provide targeted context ## Making Your Own Documentation LLM-Friendly If you're building on Aptos and want your documentation to be LLM-consumable: ### 1. Use mdBook with llms-txt-tools ```toml # book.toml [output.llms-txt] [output.llms-txt-full] ``` ### 2. Structure Content Hierarchically ```markdown # Main Topic ## Subtopic ### Concept Explanation with code examples. ```move // Code example ``` ``` ### 3. Write for Both Humans and LLMs Good documentation for LLMs: - **Uses clear headings** that describe the content below - **Includes complete code examples** that can be used as-is - **Explains concepts before using them** (define before reference) - **Uses consistent terminology** throughout - **Provides context** for code examples (what problem they solve) - **Lists prerequisites** when building on earlier concepts ### 4. Include Structured Data Tables, lists, and structured examples help LLMs extract information: ```markdown | Feature | Description | Example | |---|---|---| | `key` | Stored in global storage | `struct Token has key { ... }` | | `store` | Can be nested | `struct Metadata has store { ... }` | ``` ## The Broader Aptos AI Ecosystem The Aptos ecosystem is actively building infrastructure for AI-blockchain integration: - **llms.txt support**: Documentation optimized for LLM consumption - **AI agent frameworks**: Tools for building autonomous on-chain agents - **Smart contract templates**: LLM-friendly templates for common patterns - **API documentation**: REST and GraphQL APIs documented in LLM-friendly formats ## Benefits for Developers By using llms.txt-optimized documentation: 1. **Faster onboarding**: LLMs can provide accurate Aptos-specific guidance 2. **Better code generation**: LLMs produce more correct Move code with proper context 3. **Reduced errors**: AI assistants catch common mistakes when given comprehensive documentation 4. **Always up-to-date**: llms.txt files are generated from the latest documentation 5. **Language-agnostic**: LLMs can explain concepts in any natural language, making Aptos accessible globally # Learning Resources ## Official Documentation - [Aptos Developer Docs](https://aptos.dev) - The official developer documentation - [Aptos Learn](https://learn.aptoslabs.com) - Interactive tutorials and courses - [Move Language Reference](https://aptos.dev/en/build/smart-contracts/book) - The Move language specification ## Tools - [Aptos CLI](https://aptos.dev/en/build/cli) - Command-line tool for building, testing, and deploying - [Aptos Explorer](https://explorer.aptoslabs.com) - Block explorer for viewing transactions and accounts - [Aptos TypeScript SDK](https://github.com/aptos-labs/aptos-ts-sdk) - TypeScript/JavaScript SDK for building dApps ## Community - [Aptos Discord](https://discord.gg/aptosnetwork) - Community chat and developer support - [Aptos GitHub](https://github.com/aptos-labs) - Source code and open-source projects - [Aptos Forum](https://forum.aptoslabs.com) - Governance and technical discussions ## AI and LLM Resources - [llms.txt Standard](https://llmstxt.org/) - The standard for LLM-optimized documentation - [Move Agent Kit](https://github.com/aptosbot/move-agent-kit) - AI agent toolkit for Aptos - This book's llms.txt output - Available at the book's URL with `/llms.txt` and `/llms-full.txt` paths ## Example Projects - [Aptos Framework](https://github.com/aptos-labs/aptos-framework) - The core blockchain framework - [Move Spiders](https://movespiders.com) - Move code examples and tutorials - [Aptos Templates](https://github.com/aptos-labs/aptos-core/tree/main/aptos-move/move-examples) - Example Move projects # Appendix This section contains supplementary material to support the main content of the book. Here you'll find helpful resources like detailed installation guides, a glossary of terms, and other useful references. # CLI Installation Methods This appendix provides detailed instructions for installing and upgrading the Aptos CLI on various operating systems. For a quick start, see the [Installation](../getting_started/installation.md) page. [![Packaging status](https://repology.org/badge/vertical-allrepos/aptos.svg)](https://repology.org/project/aptos/versions) --- ## macOS ### Homebrew (Recommended) **To install:** ```bash brew install aptos ``` **To upgrade:** ```bash brew upgrade aptos ``` ### Script Installation Run the following command for both initial installation and upgrading: ```bash curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh ``` --- ## Linux ### Script Installation **To install:** ```bash curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh ``` > **Note on CPU Compatibility** > If you encounter an `Illegal instruction` error, your CPU may not support certain SIMD instructions. This can happen on older processors or when running in specific virtualized environments (e.g., Ubuntu x86_64 on an ARM Mac). > > Use the following command instead for a generic build: > > ```bash > curl -fsSL "https://aptos.dev/scripts/install_cli.sh" | sh -s -- --generic-linux > ``` **To upgrade:** You can either use the built-in update command: ```bash aptos update aptos ``` Or, re-run the installation script: ```bash curl -sSfL https://aptos.dev/scripts/install_cli.sh | sh ``` --- ## Windows ### Winget (Recommended) **To install:** ```powershell winget install aptos.aptos-cli ``` **To upgrade:** ```powershell winget upgrade aptos.aptos-cli ``` ### Chocolatey **To install** (in an administrative shell): ```powershell choco install aptos-cli ``` **To upgrade** (in an administrative shell): ```powershell choco upgrade aptos-cli ``` ### Script Installation **To install:** Run the following command in PowerShell. This will set the execution policy for the current user and then run the installer. ```powershell Set-ExecutionPolicy RemoteSigned -Scope CurrentUser; iwr https://aptos.dev/scripts/install_cli.ps1 | iex ``` **To upgrade:** You can either use the built-in update command: ```powershell aptos update aptos ``` Or, re-run the installation script: ```powershell iwr https://aptos.dev/scripts/install_cli.ps1 | iex ``` # Glossary This glossary defines key terms used throughout the Aptos documentation. --- ## Abort Code A numeric identifier indicating why a program terminated abnormally. In Move, it signals specific error conditions. ## Aptos CLI The command-line interface for the Aptos blockchain. It is used for compiling, testing, deploying, and interacting with Move packages. ## Attributes Special annotations in Move code, like `#[test]` or `#[view]`, that provide metadata to the compiler or runtime, modifying the behavior of the annotated code. ## BCS (Binary Canonical Serialization) The deterministic serialization format used in Aptos. BCS ensures that data structures always serialize to the same byte representation, which is crucial for blockchain consensus. ## Custom Error Type A user-defined struct that provides detailed context for an error, offering a more informative alternative to simple abort codes. ## Entry Function A public function in a Move module, marked with the `entry` keyword, that can be directly invoked in a transaction. ## Enum A type that can hold one of several defined variants. Enums are useful for representing states or options in a type-safe way. ## Error Propagation The process where an error (abort) in one function causes its calling functions to also abort, passing the error up the call stack. ## Error Range A designated range of abort codes for a specific module to prevent conflicts and organize error handling. ## Error Testing The practice of writing tests to verify that functions fail as expected under error conditions, often using the `#[expected_failure]` attribute. ## Event A mechanism for Move contracts to emit data that can be indexed and queried by off-chain services, signaling that something of interest has occurred. ## Expected Failure A test attribute (`#[expected_failure]`) that asserts a test case should fail with a specific abort code, enabling robust error-handling tests. ## Legacy Shortened Address A condensed form of an Aptos address where leading zeros are omitted. These are automatically padded with zeros for compatibility. ## Little-endian A byte-ordering scheme where the least significant byte is stored first. Aptos uses little-endian byte order for BCS serialization. ## Module A single file containing Move code (`.move`), which acts as a fundamental unit of code organization and encapsulation within a package. ## Named Address An alias for a specific account address used in Move source code. The actual address is substituted during compilation, making code more portable. ## Package A deployable unit of Move code that can contain one or more modules. It is published to the Aptos blockchain as a single entity. ## Publish The act of deploying a Move package to the Aptos blockchain, making its code and resources available on-chain. ## Resource A special struct with the `key` ability that represents data stored in an account's global storage. Resources have ownership semantics and cannot be copied or discarded. ## Storage Slot A location in the blockchain's global state, identified by a 32-byte address, where data (like accounts or objects) can be stored. ## Struct A composite data type that groups related values into a single, named structure. Its behavior is defined by its abilities (`copy`, `drop`, `store`, `key`). ## Tuple A fixed-size, ordered collection of values that can have different types. ## ULEB128 (Unsigned Little-Endian Base-128) A variable-length encoding for unsigned integers used in BCS, primarily for representing the length of sequences efficiently. ## View Function A read-only function marked with the `#[view]` attribute. It can be called to query blockchain state without submitting a transaction and incurring gas fees for state changes. ### Move Module ```move /// Test module /// module module_addr::test_module { use std::option::{Self, Option}; use std::signer; use aptos_std::smart_table::{Self, SmartTable}; use aptos_framework::dispatchable_fungible_asset; use aptos_framework::fungible_asset::{Self, Metadata, FungibleStore}; use aptos_framework::object::{Self, Object, ExtendRef, DeleteRef}; use aptos_framework::primary_fungible_store; use aptos_framework::timestamp; /// The lookup to object for escrow in an easily addressable spot /// /// The main purpose here is to provide fully removable types to allow for full recovery of storage refunds, and not /// have a duplicate object. struct LockupRef has key { lockup_address: address, } #[resource_group_member(group = aptos_framework::object::ObjectGroup)] /// A single lockup, which has the same lockup period for all of them /// /// These are stored on objects, which map to the appropriate escrows enum Lockup has key { /// SmartTable implementation, which can be replaced with a newer version later ST { // Creator of the lockup creator: address, /// Used to control funds in the escrows extend_ref: ExtendRef, /// Used to cleanup the Lockup object delete_ref: DeleteRef, /// Normally with coin, we could escrow in the table, but we have to escrow in owned objects for the purposes of FA escrows: SmartTable } } /// A key used for keeping track of all escrows in an easy to find place enum EscrowKey has store, copy, drop { FAPerUser { /// Marker for which FA is stored fa_metadata: Object, /// The user in which it's being stored for user: address, } } #[resource_group_member(group = aptos_framework::object::ObjectGroup)] /// An escrow object for a single user and a single FA enum Escrow has key { Simple { /// The original owner original_owner: address, /// Used for cleaning up the escrow delete_ref: DeleteRef, }, TimeUnlock { /// The original owner original_owner: address, /// Time that the funds can be unlocked unlock_secs: u64, /// Used for cleaning up the escrow delete_ref: DeleteRef, } } // -- Errors -- /// Lockup already exists at this address const E_LOCKUP_ALREADY_EXISTS: u64 = 1; /// Lockup not found at address const E_LOCKUP_NOT_FOUND: u64 = 2; /// No lockup was found for this user and this FA const E_NO_USER_LOCKUP: u64 = 3; /// Unlock time has not yet passed const E_UNLOCK_TIME_NOT_YET: u64 = 4; /// Not original owner or lockup owner const E_NOT_ORIGINAL_OR_LOCKUP_OWNER: u64 = 5; /// Not a time lockup const E_NOT_TIME_LOCKUP: u64 = 6; /// Not a simple lockup const E_NOT_SIMPLE_LOCKUP: u64 = 7; /// Can't shorten lockup time const E_CANNOT_SHORTEN_LOCKUP_TIME: u64 = 8; /// Initializes a lockup at an address public entry fun initialize_lockup( caller: &signer, ) { init_lockup(caller); } inline fun init_lockup(caller: &signer): Object { let caller_address = signer::address_of(caller); // Create the object only if it doesn't exist, otherwise quit out assert!(!exists(caller_address), E_LOCKUP_ALREADY_EXISTS); // Create the object let constructor_ref = object::create_object(@0x0); let lockup_address = object::address_from_constructor_ref(&constructor_ref); let extend_ref = object::generate_extend_ref(&constructor_ref); let delete_ref = object::generate_delete_ref(&constructor_ref); let obj_signer = object::generate_signer(&constructor_ref); move_to(&obj_signer, Lockup::ST { creator: caller_address, escrows: smart_table::new(), extend_ref, delete_ref }); // This is specifically to ensure that we don't create two lockup objects, we put a marker in the account move_to(caller, LockupRef { lockup_address }); object::object_from_constructor_ref(&constructor_ref) } /// Escrows funds with a user defined lockup time public entry fun escrow_funds_with_no_lockup( caller: &signer, lockup_obj: Object, fa_metadata: Object, amount: u64, ) acquires Lockup, Escrow { let caller_address = signer::address_of(caller); let lockup_address = object::object_address(&lockup_obj); let lockup = &mut Lockup[lockup_address]; let lockup_key = EscrowKey::FAPerUser { fa_metadata, user: caller_address }; let escrow_address = lockup.escrows.borrow_mut_with_default(lockup_key, @0x0); // If we haven't found it, create a new escrow object if (escrow_address == &@0x0) { let constructor_ref = object::create_object(lockup_address); let object_signer = object::generate_signer(&constructor_ref); let object_delete_ref = object::generate_delete_ref(&constructor_ref); // Make it a store to keep the escrow funds fungible_asset::create_store(&constructor_ref, fa_metadata); // Store the appropriate info for the funds move_to(&object_signer, Escrow::Simple { original_owner: caller_address, delete_ref: object_delete_ref }); // Save it to the table *escrow_address = object::address_from_constructor_ref(&constructor_ref); } else { // Otherwise, we'll reset the unlock time to the new time let escrow = &Escrow[*escrow_address]; match (escrow) { Simple { .. } => { // Do nothing } TimeUnlock { .. } => { abort E_NOT_SIMPLE_LOCKUP; } }; }; // Now transfer funds into the escrow escrow_funds(caller, fa_metadata, *escrow_address, caller_address, amount); } /// Escrows funds with a user defined lockup time public entry fun escrow_funds_with_time( caller: &signer, lockup_obj: Object, fa_metadata: Object, amount: u64, lockup_time_secs: u64, ) acquires Lockup, Escrow { let caller_address = signer::address_of(caller); let lockup_address = object::object_address(&lockup_obj); let lockup = &mut Lockup[lockup_address]; let lockup_key = EscrowKey::FAPerUser { fa_metadata, user: caller_address }; let escrow_address = lockup.escrows.borrow_mut_with_default(lockup_key, @0x0); // TODO: Do we make this specified by the contract rather than user? let new_unlock_secs = timestamp::now_seconds() + lockup_time_secs; // If we haven't found it, create a new escrow object if (escrow_address == &@0x0) { // We specifically make this object on @0x0, so that the creator doesn't have the ability to pull the funds // out without the contract let constructor_ref = object::create_object(lockup_address); let object_signer = object::generate_signer(&constructor_ref); let object_delete_ref = object::generate_delete_ref(&constructor_ref); // Make it a store to keep the escrow funds fungible_asset::create_store(&constructor_ref, fa_metadata); // Store the appropriate info for the funds move_to(&object_signer, Escrow::TimeUnlock { original_owner: caller_address, unlock_secs: new_unlock_secs, delete_ref: object_delete_ref }); // Save it to the table *escrow_address = object::address_from_constructor_ref(&constructor_ref); } else { // Otherwise, we'll reset the unlock time to the new time let escrow = &mut Escrow[*escrow_address]; match (escrow) { Simple { .. } => { abort E_NOT_TIME_LOCKUP; } TimeUnlock { unlock_secs, .. } => { // We however, cannot shorten the unlock time if (*unlock_secs > new_unlock_secs) { abort E_CANNOT_SHORTEN_LOCKUP_TIME; } else { *unlock_secs = new_unlock_secs } } }; }; // Now transfer funds into the escrow escrow_funds(caller, fa_metadata, *escrow_address, caller_address, amount); } /// Claims an escrow by the owner of the escrow public entry fun claim_escrow( caller: &signer, lockup_obj: Object, fa_metadata: Object, user: address, ) acquires Lockup, Escrow { let caller_address = signer::address_of(caller); let lockup = get_lockup_mut(&lockup_obj); assert!(caller_address == lockup.creator, E_NOT_ORIGINAL_OR_LOCKUP_OWNER); let (lockup_key, escrow_address) = lockup.get_escrow( fa_metadata, user ); // Take funds from lockup lockup.take_funds(fa_metadata, escrow_address); // Clean up the object lockup.delete_escrow(lockup_key); } /// Returns funds for the user /// /// TODO: add additional entry function for using LockupRef public entry fun return_user_funds( caller: &signer, lockup_obj: Object, fa_metadata: Object, user: address, ) acquires Lockup, Escrow { let caller_address = signer::address_of(caller); let lockup = get_lockup_mut(&lockup_obj); assert!(caller_address == lockup.creator, E_NOT_ORIGINAL_OR_LOCKUP_OWNER); let (lockup_key, escrow_address) = lockup.get_escrow( fa_metadata, user ); // Determine original owner, and any conditions on returning let original_owner = match (&Escrow[escrow_address]) { Escrow::Simple { original_owner, .. } => { *original_owner } Escrow::TimeUnlock { original_owner, .. } => { // Note, the lockup owner can reject the unlock faster than the unlock time *original_owner } }; lockup.return_funds(fa_metadata, escrow_address, original_owner); // Clean up the object lockup.delete_escrow(lockup_key); } /// Returns funds for the caller /// /// TODO: add additional entry function for using LockupRef public entry fun return_my_funds( caller: &signer, lockup_obj: Object, fa_metadata: Object, ) acquires Lockup, Escrow { let caller_address = signer::address_of(caller); let lockup = get_lockup_mut(&lockup_obj); let (lockup_key, escrow_address) = lockup.get_escrow( fa_metadata, caller_address ); // Determine original owner, and any conditions on returning let original_owner = match (&Escrow[escrow_address]) { Escrow::Simple { original_owner, .. } => { *original_owner } Escrow::TimeUnlock { original_owner, unlock_secs, .. } => { assert!(timestamp::now_seconds() >= *unlock_secs, E_UNLOCK_TIME_NOT_YET); *original_owner } }; // To prevent others from being annoying, only the original owner can return funds assert!(original_owner == caller_address, E_NOT_ORIGINAL_OR_LOCKUP_OWNER); lockup.return_funds(fa_metadata, escrow_address, original_owner); // Clean up the object lockup.delete_escrow(lockup_key); } /// Retrieves the lockup object for mutation inline fun get_lockup_mut( lockup_obj: &Object, ): &mut Lockup { let lockup_address = object::object_address(lockup_obj); &mut Lockup[lockup_address] } /// Retrieves the lockup object for reading inline fun get_lockup( lockup_obj: &Object, ): &Lockup { let lockup_address = object::object_address(lockup_obj); &Lockup[lockup_address] } /// Retrieves the lockup object for removal inline fun get_escrow( self: &mut Lockup, fa_metadata: Object, user: address ): (EscrowKey, address) { let lockup_key = EscrowKey::FAPerUser { fa_metadata, user, }; assert!(self.escrows.contains(lockup_key), E_NO_USER_LOCKUP); (lockup_key, *self.escrows.borrow(lockup_key)) } /// Escrows an amount of funds to the escrow object inline fun escrow_funds( caller: &signer, fa_metadata: Object, escrow_address: address, caller_address: address, amount: u64 ) { let store_obj = object::address_to_object(escrow_address); let caller_primary_store = primary_fungible_store::primary_store_inlined(caller_address, fa_metadata); dispatchable_fungible_asset::transfer(caller, caller_primary_store, store_obj, amount); } /// Returns all outstanding funds inline fun take_funds( self: &Lockup, fa_metadata: Object, escrow_address: address, ) { // Transfer funds back to the original owner let escrow_object = object::address_to_object(escrow_address); let balance = fungible_asset::balance(escrow_object); let primary_store = primary_fungible_store::ensure_primary_store_exists(self.creator, fa_metadata); // Use dispatchable because we don't know if it uses it let lockup_signer = object::generate_signer_for_extending(&self.extend_ref); dispatchable_fungible_asset::transfer(&lockup_signer, escrow_object, primary_store, balance); } /// Returns all outstanding funds inline fun return_funds( self: &Lockup, fa_metadata: Object, escrow_address: address, original_owner: address ) { // Transfer funds back to the original owner let escrow_object = object::address_to_object(escrow_address); let balance = fungible_asset::balance(escrow_object); let original_owner_primary_store = primary_fungible_store::primary_store_inlined( original_owner, fa_metadata ); // Use dispatchable because we don't know if it uses it let lockup_signer = object::generate_signer_for_extending(&self.extend_ref); dispatchable_fungible_asset::transfer(&lockup_signer, escrow_object, original_owner_primary_store, balance); } /// Deletes an escrow object inline fun delete_escrow(self: &mut Lockup, lockup_key: EscrowKey) { let escrow_addr = self.escrows.remove(lockup_key); // The following lines will return the storage deposit let delete_ref = match (move_from(escrow_addr)) { Escrow::Simple { delete_ref, .. } => { delete_ref } Escrow::TimeUnlock { delete_ref, .. } => { delete_ref } }; fungible_asset::remove_store(&delete_ref); object::delete(delete_ref); } #[view] /// Tells the lockup address for the user who created the original lockup public fun lockup_address(escrow_account: address): address acquires LockupRef { LockupRef[escrow_account].lockup_address } #[view] /// Tells the amount of escrowed funds currently available public fun escrowed_funds( lockup_obj: Object, fa_metadata: Object, user: address ): Option acquires Lockup { let lockup = get_lockup(&lockup_obj); let escrow_key = EscrowKey::FAPerUser { fa_metadata, user }; if (lockup.escrows.contains(escrow_key)) { let escrow_address = lockup.escrows.borrow(escrow_key); let escrow_obj = object::address_to_object(*escrow_address); option::some(fungible_asset::balance(escrow_obj)) } else { option::none() } } #[view] /// Tells the amount of escrowed funds currently available public fun remaining_escrow_time( lockup_obj: Object, fa_metadata: Object, user: address ): Option acquires Lockup, Escrow { let lockup = get_lockup(&lockup_obj); let escrow_key = EscrowKey::FAPerUser { fa_metadata, user }; if (lockup.escrows.contains(escrow_key)) { let escrow_address = lockup.escrows.borrow(escrow_key); let remaining_secs = match (&Escrow[*escrow_address]) { Simple { .. } => { 0 } TimeUnlock { unlock_secs, .. } => { let now = timestamp::now_seconds(); if (now >= *unlock_secs) { 0 } else { *unlock_secs - now } } }; option::some(remaining_secs) } else { option::none() } } #[test_only] const TWO_HOURS_SECS: u64 = 2 * 60 * 60; #[test_only] fun setup_for_test( framework: &signer, asset: &signer, creator: &signer, user: &signer ): (address, address, Object, Object) { timestamp::set_time_has_started_for_testing(framework); let (creator_ref, metadata) = fungible_asset::create_test_token(asset); let (mint_ref, _transfer_ref, _burn_ref) = primary_fungible_store::init_test_metadata_with_primary_store_enabled( &creator_ref ); let creator_address = signer::address_of(creator); let user_address = signer::address_of(user); primary_fungible_store::mint(&mint_ref, user_address, 100); let fa_metadata: Object = object::convert(metadata); let lockup_obj = init_lockup(creator); (creator_address, user_address, fa_metadata, lockup_obj) } #[test(framework = @0x1, asset = @0xAAAAA, creator = @0x10C0, user = @0xCAFE)] fun test_out_flow(framework: &signer, asset: &signer, creator: &signer, user: &signer) acquires Lockup, Escrow { let (creator_address, user_address, fa_metadata, lockup_obj) = setup_for_test(framework, asset, creator, user); escrow_funds_with_no_lockup(user, lockup_obj, fa_metadata, 5); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95); // Check view functions assert!(remaining_escrow_time(lockup_obj, fa_metadata, user_address) == option::some(0)); assert!(escrowed_funds(lockup_obj, fa_metadata, user_address) == option::some(5)); assert!(remaining_escrow_time(lockup_obj, fa_metadata, @0x1234567) == option::none()); assert!(escrowed_funds(lockup_obj, fa_metadata, @0x1234567) == option::none()); // Should be able to return funds immediately return_user_funds(creator, lockup_obj, fa_metadata, user_address); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 100); // Same with the user escrow_funds_with_no_lockup(user, lockup_obj, fa_metadata, 5); return_my_funds(user, lockup_obj, fa_metadata); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 100); // Claim an escrow escrow_funds_with_no_lockup(user, lockup_obj, fa_metadata, 5); claim_escrow(creator, lockup_obj, fa_metadata, user_address); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95); assert!(primary_fungible_store::balance(creator_address, fa_metadata) == 5); // -- Now test with time lockup -- escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 90); // Check view functions assert!(remaining_escrow_time(lockup_obj, fa_metadata, user_address) == option::some(TWO_HOURS_SECS)); assert!(escrowed_funds(lockup_obj, fa_metadata, user_address) == option::some(5)); // Should be able to return funds immediately return_user_funds(creator, lockup_obj, fa_metadata, user_address); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95); escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS); // User can't unescrow without time passing, let's go forward 2 hours timestamp::fast_forward_seconds(TWO_HOURS_SECS); return_my_funds(user, lockup_obj, fa_metadata); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 95); // Claim an escrow, can be immediate escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS); claim_escrow(creator, lockup_obj, fa_metadata, user_address); assert!(primary_fungible_store::balance(user_address, fa_metadata) == 90); assert!(primary_fungible_store::balance(creator_address, fa_metadata) == 10); } #[test(framework = @0x1, asset = @0xAAAAA, creator = @0x10C0, user = @0xCAFE)] #[expected_failure(abort_code = E_UNLOCK_TIME_NOT_YET, location = lockup_deployer::fa_lockup)] fun test_too_short_lockup( framework: &signer, asset: &signer, creator: &signer, user: &signer ) acquires Lockup, Escrow { let (_creator_address, _user_address, fa_metadata, lockup_obj) = setup_for_test( framework, asset, creator, user ); escrow_funds_with_time(user, lockup_obj, fa_metadata, 5, TWO_HOURS_SECS); // User can't return funds without waiting for lockup return_my_funds(user, lockup_obj, fa_metadata); } #[test_only] public fun test_only_addr() { @module_addr } } ``` ### Comments ```move /** Block doc comments */ module 0x1234::test_module_no_addr { const HEX: vector = x"0123456789ABCDEFabcdef"; const BYTE: vector = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+=<>,./?':;\"`~!@#$%^&*() "; /* Block comments */ // One line comment /// Doc comment fun do_nothing(): bool { true } } ``` ### Script ```move script { use aptos_framework::object::Object; use aptos_framework::option::Option; use aptos_framework::aptos_account; use aptos_std::signer; use aptos_framework::object; /// Comment describing function fun some_Name( account1: &signer, account2: &signer, i1: u8, i2: u16, i3: u32, i4: u64, i5: u128, i6: u256, i7: address, i8: bool, i9: vector, i10: Object, i11: Option, _I_12: U ) { // TODO: add more info here let total = (i1 as u256) + (i2 as u256) + (i3 as u256) + (i4 as u256) + (i5 as u256) + i6; if (i8) total *= 2 else total /= 2; if (i9.length() >= 1) { total += (i9[0] as u256); } else if (i11.is_some()) { total -= i11.destroy_some(); } else { total %= 2 }; let receiver = signer::address_of(account2); let balance: u64 = (total / 3) as u64; aptos_account::transfer(account1, receiver, balance); aptos_account::transfer(account1, i7, balance); let addr = object::object_address(&i10); aptos_account::transfer(account1, addr, balance); } } ``` ## Separate address ```move // Legacy syntax address 0x1234 { /** Block doc comments */ module test_module_no_addr { const HEX: vector = x"0123456789ABCDEFabcdef"; const BYTE: vector = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_+=<>,./?':;\"`~!@#$%^&*() "; /* Block comments */ // One line comment /// Doc comment fun do_nothing(): bool { true } } } ```