Set up the Move toolchain

Before writing smart contracts, you need a working development environment. The Move language is primarily used by two major ecosystems: Sui and Aptos. Both provide their own CLI tools that bundle the Move compiler and blockchain interaction utilities.

This guide covers the setup for both ecosystems. Choose the one that matches your target blockchain.

1
Install the Sui CLI

Sui provides a unified CLI for managing keys, deploying contracts, and interacting with the Sui network.

Install the latest version using the official installer script:

Shell
Shell
curl https://release.sui.io/sui/install | bash -s -- -v latest

Verify the installation by checking the version:

Shell
Shell
sui --version

For detailed installation options, including manual binary downloads, see the Sui documentation.

2
Install the Aptos CLI

Aptos uses a similar CLI structure for managing accounts and submitting transactions.

Use the official installation script:

Shell
Shell
curl https://releases.aptoslabs.com/aptos-cli/aptos-cli-installer.sh | bash

Confirm the installation:

Shell
Shell
aptos --version

Refer to the Aptos documentation for advanced setup configurations.

3
Verify your environment

Both CLIs require a working Rust toolchain to compile Move projects locally. If you haven't installed Rust yet, run:

Shell
Shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After installation, ensure your PATH is updated by running source $HOME/.cargo/env (or restarting your terminal).

Test your Move compiler by creating a new project:

Shell
Shell
sui move new my_move_project
# or
aptos init

If the command runs without errors, your Move toolchain is ready for development.

Create a new Move project

Start by initializing a Move package using the Move package manager. This command scaffolds the directory structure and creates the configuration files needed to build smart contracts for Sui or Aptos.

Run the following command in your terminal. Replace my_move_project with your desired package name.

Shell
move init my_move_project

Project Structure

The move init command generates a specific directory layout. Understanding this structure is essential before writing your first module.

Directory/FilePurpose
Move.tomlPackage configuration, dependencies, and compiler settings.
sources/Contains your .move source files.
tests/Holds integration tests for your modules.
scripts/Optional folder for Move script files.

The Move.toml File

The Move.toml file is the core configuration for your project. It defines the package name, version, and dependencies. For Sui, you typically use the sui framework; for Aptos, you use the aptos framework.

Here is an example of a basic Move.toml for a Sui project:

TOML
[package]
name = "my_move_project"
version = "0.0.1"

[dependencies]
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "main" }

[addresses]
mymove = "0x0"

Next Steps

Once the project is initialized, you can start writing your first module in the sources/ directory. The Move compiler will automatically detect your files when you run move build.

For more details on project configuration, refer to the Move Package Manager documentation.

Write a basic Move module

Move treats digital assets as resources, which are unique objects that cannot be copied or dropped accidentally. This design prevents common smart contract bugs like double-spending or accidental value loss. In this section, you will build a simple module that defines a resource, stores it in an account, and transfers it to another address.

We will use a generic structure compatible with both Sui and Aptos, focusing on the core Move concepts: struct, key, and transfer. The code below demonstrates how to create a custom asset and move it between accounts.

Step 1: Define the Resource Structure

In Move, any type that holds value must be marked with the key or store ability. The key ability allows the struct to be stored in an account's object table. We define a simple Token struct that holds an amount.

MOVE
module my_package::token {
    use std::signer;
    use std::string;

    // Define a resource struct
    struct Token has key, store {
        id: UID,
        amount: u64
    }

    // Helper to create a new token
    fun mint(signer: &signer, amount: u64) {
        let token = Token {
            id: object::new(&signer),
            amount
        };
        // In a real scenario, you might store this or transfer it immediately
        transfer::public_transfer(token, signer::address_of(&signer));
    }
}

Step 2: Implement Transfer Logic

Move enforces strict ownership rules. To move a resource, you must explicitly transfer it. The transfer::public_transfer function moves a resource to another account's address. This is safer than sending coins because the receiver must have a way to handle the specific resource type.

MOVE
    // Transfer token to another address
    fun transfer_token(
        signer: &signer,
        recipient: address,
        token: Token
    ) {
        // Ensure the signer owns the token before transferring
        assert!(signer::address_of(&signer) == owner_of(&token), EUnauthorized);
        transfer::public_transfer(token, recipient);
    }

    // Helper to check ownership (internal use)
    fun owner_of(token: &Token): address {
        object::address_of(&token.id)
    }

Step 3: Test the Module

After writing the module, you must compile it using the Move CLI. This checks for type safety and resource handling errors. If the compilation succeeds, you can deploy it to a testnet.

  1. Compile: Run move build in your project directory. This generates bytecode that the Move VM can execute.
  2. Deploy: Use move publish to deploy the module to a local node or testnet.
  3. Interact: Use a client script or Move console to call mint and transfer_token.

This simple example illustrates how Move’s resource model prevents accidental duplication. By marking Token with key, you ensure it exists in only one place at a time. This is the foundation of secure asset management in Move-based blockchains like Sui and Aptos.

Note: Always verify the key and store abilities in your structs. Missing store prevents the type from being sent across modules, while missing key prevents it from being stored in accounts.

For more detailed documentation on Move’s type system, refer to the official Move documentation. This resource covers advanced topics like generics and abilities in depth.

Compile and test the contract

Before deploying a Move-based smart contract to any network, you must verify that the code compiles without errors and passes all unit tests. This process acts as a safety net, catching syntax mistakes and logical flaws before they reach the mainnet. In ecosystems like Sui and Aptos, the compiler is strict about resource ownership and access control, so catching these issues locally saves significant time and gas fees later.

Verify compilation

Start by compiling the contract using the official Move compiler (move). This step checks for syntax errors, type mismatches, and structural issues specific to the Move language. For Sui projects, you can use the sui move build command, which compiles the module and generates the bytecode. For Aptos, use aptos move compile. If the compilation fails, the compiler will provide detailed error messages pointing to the exact line and nature of the issue. Address these errors iteratively until the build succeeds.

Run unit tests

Once the contract compiles, run the unit tests to verify the logic. Move allows you to write test functions directly within the source code, marked with the #[test] attribute. Use sui move test for Sui or aptos move test for Aptos to execute these tests. Focus on testing critical paths, such as token transfers, ownership checks, and state changes. If a test fails, the output will show which assertion was violated, helping you pinpoint the bug. Ensure all tests pass before proceeding to deployment.

Pre-deployment checklist

  • Contract compiles without errors
  • All unit tests pass locally
  • Resource ownership rules are verified
  • Access control modifiers are correct
  • Gas estimation is reviewed

Deploy your Move contract to a testnet

Once your Move module compiles without errors, the next step is getting it onto a live network. Deploying to a testnet allows you to interact with your smart contracts using real blockchain mechanics without risking mainnet capital. Both Sui and Aptos provide robust testnet environments that mirror mainnet behavior, making them ideal for final validation before production.

1. Configure your wallet and fund it

Before deploying, ensure your wallet is set to the correct testnet network and has sufficient native tokens to cover gas fees. For Sui, switch your wallet (such as Sui Wallet or Ethos) to the Sui Testnet. For Aptos, select the Aptos Testnet. You can claim free testnet tokens from the official Sui Faucet or the Aptos Faucet to cover transaction costs.

2. Upload the compiled bytecode

Use the respective CLI tools to publish your compiled Move module. This process uploads the bytecode to the network and initializes the module with an optional initialization function.

For Sui, use the Sui CLI:

Shell
sui client publish --gas-budget 100000000 ./path/to/your_module

For Aptos, use the Aptos CLI:

Shell
aptos move publish --named-addresses your_module=0x123...

This command sends a transaction to the network. The CLI will return a transaction digest or hash. You can use this hash to verify the transaction status on the block explorer.

3. Verify the deployment

After the transaction is confirmed, verify that your module is live. On Sui, you can check the Published Objects section in the Sui Explorer. On Aptos, look for the module under the account address in the Aptos Explorer. You can then interact with your contract using the sui client or aptos move run commands to test functions.

Common deployment pitfalls

  • Gas estimation errors: If the transaction fails due to insufficient gas, increase the --gas-budget flag. Complex Move logic requires more computational resources.
  • Address mismatch: Ensure the address specified in your Move.toml matches the wallet address you are using to deploy. Mismatches will cause the transaction to revert.
  • Network confusion: Always double-check that your CLI is pointed to the testnet endpoint, not mainnet or localnet.

Interactive deployment checklist

  • Wallet switched to testnet network
  • Testnet tokens received from faucet
  • Move.toml configured with correct network
  • Contract compiled with sui move build or aptos move compile
  • Transaction digest recorded for verification

Common Move mistakes to avoid

Even with Move’s type system, developers frequently trip over resource management and ability modifiers. These errors often compile locally but fail at runtime or leave contracts vulnerable. Understanding these pitfalls is essential for writing secure code on Sui and Aptos.

Misusing the key and store abilities

The most common error is misapplying the key or store abilities. In Move, key marks a struct as a global resource that can be stored in the blockchain’s state store, while store allows a struct to be nested inside other stored resources.

If you mark a struct as key but it contains fields without store, the compiler will reject it. Conversely, if you need a struct to be part of a larger stored object but forget store, you’ll hit a type mismatch. Always ensure your nested types respect the store constraint before declaring a parent as key.

Violating resource invariants

Move’s resource model ensures that every resource is either used exactly once or destroyed. A frequent mistake is attempting to duplicate a resource or ignoring it entirely. For example, creating a Coin<T> but never transferring or destroying it results in a "resource leak" warning or a compilation error if the resource is moved.

MOVE
// Error: 'coin' is never used
fun mint_bad(account: &signer) {
    let coin = mint(account, 100);
    // Forgot to transfer or destroy coin
}

Always ensure resources are consumed. If you create a resource, you must either transfer it to another account, destroy it explicitly, or store it in a global resource. This strictness prevents accidental duplication and ensures predictable state transitions.

Ignoring ability constraints in function signatures

Another subtle error is ignoring ability constraints in function parameters. If a function expects a &T where T must have drop, but you pass a struct without drop, the code will fail. Similarly, passing a &mut T to a function that expects &T is allowed, but the reverse is not.

Always match the ability constraints in your function signatures to the types you intend to pass. This prevents runtime errors and ensures your contract behaves as expected under different network conditions.