Your First Move Module
The Aptos blockchain allows developers to write Turing complete smart contracts (called “modules”) with the secure-by-design Move language. Smart contracts enable users to send money with the blockchain, but also write arbitrary code, even games! It all starts with the Aptos CLI creating an account which will store the deployed (”published”) Move module.
This tutorial will help you understand Move Modules by guiding you through setting up a minimal Aptos environment, then how to compile, test, publish and interact with Move modules on the Aptos Blockchain. You will learn how to:
- Setup your environment, install the CLI
- Create a devnet account and fund it
- Compile and test a Move module
- Publish (or “deploy”) a Move module to the Aptos blockchain
- Interact with the module
- Keep building with Aptos (next steps)
This tutorial is not meant to teach you the fundamentals of Move. That is a longer topic best learned through the Move Book.
1. Setup
Changes to the blockchain are called “transactions”, and they require an account to pay the network fee (”gas fee”). We will need to create an account with some APT to pay that fee and own the published contract. In order to do that, we will need to use the Aptos CLI.
Install the Aptos CLI
Install the Aptos CLI (if you haven’t already).
Open a new terminal
Open a new terminal window or tab.
Verify the installation
Run aptos --version
to verify you have it installed.
aptos --version
You should see a response like aptos 4.6.1
.
Create a project folder
Create a new folder for this tutorial by running:
mkdir my-first-module
Navigate to the project folder
Run cd my-first-module
to go into your new folder.
Initialize your account
Run aptos init
and press ‘enter’ for each step of setup to create a test account on devnet
.
As we are configuring your Aptos CLI for this folder, notice that this setup follows the logic of the blockchain itself:
- Which network are we working with (default
devnet
, which refreshes every week)? - What is the account we are transacting from (creating a unique private key, which in turn generates a cryptographic public key and account address)?
- How do I pay for “gas”? (For devnet, testnet, and local networks, the Aptos CLI will helpfully fund this account with Aptos Coin, APT).
For now, just press ‘enter’ repeatedly to accept all the defaults.
You should see a success message like this:
---
Aptos CLI is now set up for account 0x9ec1cfa30b885a5c9d595f32f3381ec16d208734913b587be9e210f60be9f9ba as profile default!
{
"Result": "Success"
}
What you might not have noticed is that the Aptos CLI has created a new hidden folder .aptos/
with a .gitignore
and config.yaml
which contains the account information, including private key, public key, and account address.
You can view hidden files with ls -a
in Unix/Mac terminal or dir /ah
in Windows.
2. (Optional) Explore What You Just Did On-Chain
Copy your account address
Copy the address from the command line for your new account.
The address looks like this 0x9ec1cfa30b885a5c9d595f32f3381ec16d208734913b587be9e210f60be9f9ba
and you can find it in the line:
Aptos CLI is now set up for account 0x9ec1cfa30b885a5c9d595f32f3381ec16d208734913b587be9e210f60be9f9ba as profile default!
Open the Aptos Explorer
Go to the Aptos Explorer.
This is the primary way to quickly check what is happening on devnet, testnet, or mainnet. We will use it later on to view our deployed contracts.
Ensure you are on Devnet
network.
Look for “Devnet” in the top right corner, or switch networks by clicking the “Mainnet” dropdown and selecting Devnet
Search for your account
Paste your newly created address into the search bar.
Do not press enter! There is a known bug where searching with Enter does not work.
View the search results
Wait for the results to appear, then click the top result.
Check the transaction
You should see your newly created account and a transaction with the faucet function, funding it with devnet tokens.
Verify your balance
Click the “Coins” tab to see that you have 1 APT of the Aptos Coin. This will allow you to publish and interact with smart contracts on the aptos devnet.
The explorer is an important tool to see the contracts we are deploying, and also offers a way to look up what a contract does. Just search for the address where a contract is deployed and you will be able to see the code for that module.
3. Writing and Compiling Your First Module
Now that we have our environment set up and an account created, let’s write and compile our first Move module. Unlike Ethereum where contracts exist independently, Move ties everything to accounts - both modules and their resources. Let’s start with a simple example to understand the core concepts.
This diagram illustrates the relationship between module ownership, token ownership, and the Move blockchain state. It helps visualize how modules and resources are tied to accounts, emphasizing the unique aspects of Move’s design compared to other blockchain platforms.
What is a Move Module?
Move modules are similar to smart contracts in other blockchains, with some key differences:
- Resources: Unlike Solidity where state is stored in contract variables, Move uses “resources” - special data types that can only exist in one place at a time and are always tied to an account
- Module-based: Rather than deploying entire contracts as independent units like in Solidity, Move code is organized into reusable modules that can share and handle resources across boundaries. Modules are more like standard library packages that can be published together or separately, offering finer-grained control over code organization.
- Safety by design: Move’s type system and resource semantics help prevent common smart contract vulnerabilities
If you’re familiar with Rust, you’ll find Move’s syntax very similar. If you’re coming from Solidity, think of modules as reusable smart contract libraries.
Your First Move Module
Our first module will be a simple message storage system that allows accounts to store and retrieve messages. Let’s create a new move project within our my-first-module
folder:
Initialize the project
Initialize a new move project with aptos move init --name my_first_module
This creates a project structure with a sources
directory and a Move.toml
file.
Create the module file
Create a new file sources/message.move
with our module code:
module my_first_module::message {
use std::string;
use std::signer;
struct MessageHolder has key, store, drop {
message: string::String,
}
public entry fun set_message(account: &signer, message: string::String) acquires MessageHolder {
let account_addr = signer::address_of(account);
if (exists<MessageHolder>(account_addr)) {
move_from<MessageHolder>(account_addr);
};
move_to(account, MessageHolder { message });
}
public fun get_message(account_addr: address): string::String acquires MessageHolder {
assert!(exists<MessageHolder>(account_addr), 0);
let message_holder = borrow_global<MessageHolder>(account_addr);
message_holder.message
}
}
Let’s break down this module:
- We define a
MessageHolder
resource type that can store a string message set_message
allows an account to store a messageget_message
allows anyone to retrieve a stored message- The
acquires
keyword indicates which resources the functions need access to (MessageHolder, in this case) move_to
andmove_from
handle the storage of resources under accounts
Move has some unique characteristics that make it different from other smart contract languages:
- Resource types are used to represent assets and state that can only exist in one place at a time
- Ability modifiers like
key
,store
, anddrop
control how values can be used - Explicit acquire annotations tell us which resources a function might access
Compile the module
Compile the Move module we just created with aptos move compile --named-addresses my_first_module=default
The --named-addresses
flag maps our module name to our account’s address. In Move, modules must be associated with an address at compile time - we’re using 'default'
which points to the account we just created.
You should see a message like this if it succeeded:
❯ aptos move compile --named-addresses my_first_module=default
Compiling, may take a little while to download git dependencies...
UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-framework.git
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_first_module
{
"Result": [
"9ec1cfa30b885a5c9d595f32f3381ec16d208734913b587be9e210f60be9f9ba::message"
]
}
Great job! We are now ready to test and debug.
4. Testing and Debugging
Testing and debugging are crucial parts of Move module development. Move has built-in support for unit testing and debug printing.
Add debug prints
First, let’s modify our message module to add some debug prints. Update your sources/message.move
:
module my_first_module::message {
use std::string;
use std::signer;
use std::debug; // Add this for debug prints
struct MessageHolder has key, store, drop {
message: string::String,
}
public entry fun set_message(account: &signer, message: string::String) acquires MessageHolder {
let account_addr = signer::address_of(account);
debug::print(&message); // Print the message being set
if (exists<MessageHolder>(account_addr)) {
debug::print(&b"Updating existing message"); // Print debug info
move_from<MessageHolder>(account_addr);
} else {
debug::print(&b"Creating new message"); // Print when creating new
};
move_to(account, MessageHolder { message });
}
public fun get_message(account_addr: address): string::String acquires MessageHolder {
assert!(exists<MessageHolder>(account_addr), 0);
let message_holder = borrow_global<MessageHolder>(account_addr);
debug::print(&message_holder.message); // Print the retrieved message
message_holder.message
}
}
Create test file
Create our tests: a new file sources/message_tests.move
with:
#[test_only]
module my_first_module::message_tests {
use std::string;
use std::signer;
use aptos_framework::account;
use my_first_module::message;
#[test]
fun test_set_and_get_message() {
// Set up test account
let test_account = account::create_account_for_test(@0x1);
// Test setting a message
message::set_message(&test_account, string::utf8(b"Hello World"));
// Verify the message was set correctly
let stored_message = message::get_message(signer::address_of(&test_account));
assert!(stored_message == string::utf8(b"Hello World"), 0);
}
}
Run the tests
Now run the tests with aptos move test --named-addresses my_first_module=default
You should see output if the tests pass: (See below for how to handle errors)
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_first_module
Running Move unit tests
[debug] "Hello World"
[debug] 0x4372656174696e67206e6577206d657373616765
[debug] "Hello World"
[ PASS ] 0x9ec1cfa30b885a5c9d595f32f3381ec16d208734913b587be9e210f60be9f9ba::message_tests::test_set_and_get_message
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}
If you encounter errors while testing, here are some common issues and solutions:
- Make sure all module dependencies are properly imported
- Check that your account address matches in the
-named-addresses
parameter - Verify that test functions have the
#[test]
attribute - Ensure string literals are properly encoded
Debugging Tips
- Use
debug::print()
in test functions - Debug prints will show up automatically during test execution
- Remember that debug statements will only work in tests, not in production code. They will have no impact on code performance.
- To debug module state:
- Print account addresses with
debug::print(&addr)
- Print string values with
debug::print(&some_string)
- Print boolean conditions with
debug::print(&some_bool)
- Print account addresses with
5. Publishing Your Module
After successfully compiling and testing your module, you can publish it to the Aptos blockchain. This process deploys your code so that it’s accessible on-chain.
Publish the module
Publish your module with aptos move publish --named-addresses my_first_module=default
You’ll see output showing the compilation process and then a prompt asking about gas fees:
Compiling, may take a little while to download git dependencies...
UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-framework.git
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_first_module
package size 1271 bytes
Do you want to submit a transaction for a range of [141300 - 211900] Octas at a gas unit price of 100 Octas? [yes/no] >
Confirm the transaction
Type y
and press Enter to confirm the transaction.
After confirmation, you’ll receive a response showing the transaction details:
{
"Result": {
"transaction_hash": "0x95fce7344b066abda10c07dbf1ffa83e0d9c7bd400e2b143682a6c8a5f179dc2",
"gas_used": 1413,
"gas_unit_price": 100,
"sender": "9ec1cfa30b885a5c9d595f32f3381ec16d208734913b587be9e210f60be9f9ba",
"sequence_number": 0,
"success": true,
"timestamp_us": 1735351260227638,
"version": 273029731,
"vm_status": "Executed successfully"
}
}
(Optional) Seeing Your Contract On-Chain
After successful publication, you can verify your module is on-chain by following these steps:
Open the Explorer
Go to the Aptos Explorer
Check the transaction
Search for your account address. You should notice that there is a new transaction in your account, the code::publish_package_txn
function.
View your balance
Click the “Coins” tab to see that you now have less than 1 APT of the Aptos Coin.
You have spent a small amount on gas to deploy the contract so should have around 0.99855 APT
remaining.
Find your module
Look under the “Modules” tab
Verify the module
You should see your “message” module listed
You can share the explorer link to your module and others can even interact with the module by connecting a wallet.
6. Interacting with Your Module
Now that your module is published, you can interact with it through the Aptos CLI:
Set a message
Set a message using the CLI:
aptos move run --function-id 'default::message::set_message' --args 'string:Hello, Aptos!'
You’ll see a gas fee prompt similar to what you saw during publishing.
Confirm the transaction
After confirming with y
, you should get a success response like:
Transaction submitted: https://explorer.aptoslabs.com/txn/0x0c0b1e56a31d037280278327eb8fdfcc469a20213e5e65accf6e7c56af574449?network=devnet
{
"Result": {
"transaction_hash": "0x0c0b1e56a31d037280278327eb8fdfcc469a20213e5e65accf6e7c56af574449",
"gas_used": 445,
"gas_unit_price": 100,
"sender": "9ec1cfa30b885a5c9d595f32f3381ec16d208734913b587be9e210f60be9f9ba",
"sequence_number": 1,
"success": true,
"timestamp_us": 1735351754495208,
"version": 273137362,
"vm_status": "Executed successfully"
}
}
View your message
View your stored message by checking under Resources on the Explorer.
Celebrate!
We did it!
How long did it take you to get through this guide? We want to hear from you!
Next Steps
Congratulations! You’ve successfully:
- Compiled your first Move module
- Added tests to help debug
- Published your module on-chain
- Used your contract through the CLI
Now your published Move module can be connected to just like an API via one of our many Official SDKs!
Here are some suggested next steps to get a deeper understanding of Move modules:
- Try modifying the module to add a new feature. You can use the Move Book to build your understanding of writing Move modules.
- To understand how Move works on-chain, you can learn about Move’s resource system.
- If you’re building an application to interact with contracts or look up data from on-chain, learn how to use the SDKs here.
- Join the Aptos Discord to connect with other developers.