Getting Started with Aptos: An Easy Guide for New Users

John Fu LinJohn Fu Lin
11 min read

Aptos is a Layer 1 blockchain that is very fast, has low latency, and is a secure place for building decentralized apps (dApps). In this article, we’ll cover these main points:

  1. Setting up your development environment, including the Petra Wallet and necessary tools

  2. Understanding the basics of Move, Aptos' smart contract programming language

  3. Creating, testing, and deploying your first Aptos smart contract

  4. Interacting with your deployed contract using the Aptos CLI

This guide will give you the basic knowledge to get started with the Aptos ecosystem. By the time you finish reading, you'll have a good understanding of Aptos' features and be ready to start building your own decentralized apps.

Initial Setups

Setup Wallet

Install the Petra Wallet extension. Developed by Aptos Labs, it’s a secure crypto wallet that lets you:

  • Explore Web3 apps on Aptos

  • Manage keys and mnemonics

  • Transfer coins, view NFTs, and engage with DeFi and NFT marketplaces

  • Store multiple accounts securely

Faucet

Faucets distribute small amounts of cryptocurrency, in our case APT, for paying transaction fees and testing.

Replace <REPLACE_WITH_YOUR_WALLET_ADDRESS> with your address

curl --request POST \
  --url 'https://faucet.devnet.aptoslabs.com/mint?amount=10000&address=<REPLACE_WITH_YOUR_WALLET_ADDRESS>'

Setting up the Aptos CLI

Let's install the necessary tools like Node.js and the Aptos CLI. There are different installation methods available. For macOS and Linux users, you can use Homebrew.

brew install aptos

After the installation is complete, check if it is installed correctly with aptos --version.

If you have an older version installed with Homebrew, you can upgrade it with brew upgrade aptos.

You can also install the Aptos CLI from a binary file. Visit https://github.com/aptos-labs/aptos-core and open the Releases section.

Then search for "Aptos CLI". Open the "Assets" dropdown to find the binaries.

Setup VSCode IDE

Download Visual Studio Code and install the aptos-move-analyzer extension. This Visual Studio Code plugin, developed by MoveBit, provides language support for the Aptos Move language. It includes:

  • Basic grammar and language configuration for Move (.move) files

  • Syntax highlighting

  • Commenting/uncommenting functionality

  • Simple context-unaware completion suggestions while typing

  • Other basic language features for Move files

The move-analyzer extension enhances your Move development experience in Visual Studio Code.

Start a Basic Aptos DApp

Using the create-aptos-dapp tool, you can set up a boilerplate project for a decentralized application (dapp). To get started, run the following command in your terminal.

npx create-aptos-dapp

It will open the wizard to set up the project.

You can choose between 3 starters: Boilerplate, Digital Asset, Fungible Asset. Find More info about the templates https://aptos.dev/en/build/create-aptos-dapp/templates/boilerplate

When choosing a network, you'll have the following options:

  • Mainnet: Use real money.

  • Testnet: Use fake money.

  • Devnet: Use fake money, but be aware that it gets wiped every week. This is good for testing incompatible contracts since everything gets reset.

It takes several minutes to install the node modules. Once installed, open the folder with an IDE.

Understanding Move

The key difference is that Sui Move uses an object-centric data model like Solana, while Aptos Move uses an address-centric model like Ethereum. This makes Sui better at handling parallel transactions.

Interfaces and implementations are the same on Aptos. For example, Coin is a standard. You can find all the standards here. In Ethereum, we have ERC20, which are interfaces where people can create different implementations. With Aptos, you trade some flexibility for standardization.

Upgrading Smart Contract

Move code upgrades on the Aptos blockchain occur at the Move package level. A package defines an upgrade policy in the Move.toml file.

[package]
name = "MyApp"
version = "0.0.1"
upgrade_policy = "compatible" // Add policy
...

Aptos Blockchain allows for easy upgrades while keeping stable addresses, ensuring compatibility with previous versions through its native upgrade policies. This makes updating smart contracts simpler compared to other blockchain networks.

  • immutable: The code cannot be upgraded and will remain the same forever.

  • compatible: These upgrades must be backward compatible, especially for storage and API.

Modules and Addresses

Modules are:

  • Building blocks of Move smart contracts

  • Contain functions and data structures

  • Stored on-chain as bytecode

  • Can be called by transaction scripts

  • Encapsulate logic for assets and interactions

Addresses are:

  • Account addresses are 32-byte (64 hex characters) unique identifiers

  • Can be prefixed with "0x", but removing leading zeros is generally limited to special addresses (0x0 to 0xa)

  • Used for transactions and asset transfers

  • Resource accounts:

    • Special type of account without a corresponding private key

    • Used by developers for autonomous operations on the blockchain

    • Primary purposes:

      • Storing resources independently

      • Publishing modules on-chain

    • Enable smart contracts to have autonomous control over assets and operations

    • Can automatically sign for transactions related to their published modules

    • Allow managing modules/resources without direct user involvement

    • Provide a way to separate contract logic from user accounts

State and Structs

State in Move consists of data stored on the blockchain, managed through resources and structs within modules. Resources ensure safety by preventing duplication and ensuring exclusive ownership. They are stored in the global state with the "key" attribute or within other structs with the "store" attribute.

There are four Move abilities for structs. These abilities control what actions are allowed for values of a given type.

copy: Allows value duplication

struct Copyable has copy { x: u64 } let a = Copyable { x: 10 }; 
let b = copy a; // Valid because Copyable has the copy ability

drop: Permits value deletion

struct Droppable has drop { y: u64 }
{
  let d = Droppable { y: 20 }; // d is discarded at end of scope
} // Valid because Droppable has the drop ability

key: Enables global storage access

struct Resource has key { z: u64 }
public fun store(account: &signer, value: u64) {
  move_to(account, Resource { z: value }); // Valid because Resource has the key ability
}

store: Allows storage within other structs

struct Inner has store { w: u64 }
struct Outer has key { inner: Inner }
// Valid because Inner has the store ability, allowing it to be stored within Outer

Functions

Functions are declared with the fun keyword, followed by name, parameters, return type, and body.

fun add(x: u64, y: u64): u64 {
    x + y
}

Function visibility

  • Default: internal (private)

  • public: accessible from anywhere

  • public(friend): accessible by friends

  • entry: can be directly invoked like scripts

acquires annotation is needed when accessing resources.

module 0x42::example {
    struct Balance has key {
        value: u64
    }

    public fun extract_balance(addr: address): u64 acquires Balance {
        let Balance { value } = move_from<Balance>(addr); // acquires needed
        value
    }

    public fun extract_and_add(sender: address, receiver: &signer) acquires Balance {
        let value = extract_balance(sender); // acquires needed here
        add_balance(receiver, value)
    }

    public fun add_balance(s: &signer, value: u64) {
        move_to(s, Balance { value })
    }
}

Using &signer as a parameter is common, especially for functions that need to act on a specific account or check permissions. It ensures that the caller has the authority to perform certain actions on the account.

module 0x42::example {
    use std::signer;

    struct MyResource has key {
        value: u64
    }

    public fun initialize(account: &signer) {
        let resource = MyResource { value: 0 };
        move_to(account, resource);
    }

    public fun increment(account: &signer) acquires MyResource {
        let addr = signer::address_of(account);
        let resource = borrow_global_mut<MyResource>(addr);
        resource.value += 1;
    }
}

Events

Events help you make historical queries. They are emitted during the execution of a transaction. Each Move module can define its own events and decide when to emit them during the module's execution.

  • Defined as struct types with the #[event] attribute

  • Emitted using 0x1::event::emit()

  • Accessible in tests via native functions

  • Queryable through the GraphQL API

Write the Module

In the Aptos protocol, a Move module is a smart contract. Let's cover the main components one by one to build our module.

Module Structure

module module_addr::my_module {
    // Module contents
}

This defines a module named my_module under the module_addr address.

Resource Definition

struct MessageHolder has key {
    message: string::String,
    message_change_events: event::EventHandle<MessageChangeEvent>,
}

This defines a resource struct MessageHolder with the key ability, allowing it to be stored in global storage. It contains a message and an event handle to track message changes.

Event Definition

struct MessageChangeEvent has drop, store {
    from_message: string::String,
    to_message: string::String,
}

This defines an event struct to record message changes, with drop and store abilities.

View Function

#[view]
public fun get_message(addr: address): string::String acquires MessageHolder {
    assert!(exists<MessageHolder>(addr), error::not_found(ENO_MESSAGE));
    *&borrow_global<MessageHolder>(addr).message
}

A public view function to retrieve the message stored at a given address.

Entry Function

public entry fun set_message(account: signer, message: string::String)
acquires MessageHolder {
    // Function implementation
}

An entry function to set or update the message. It either creates a new MessageHolder or updates an existing one, and emits an event for the change.

Test Function

text#[test(account = @0x1)]
public entry fun sender_can_set_message(account: signer) acquires MessageHolder {
    // Test implementation
}

A test function to verify theset_messagefunctionality.

Here is the final code with everything combined: module.move

module module_addr::my_module {
    use std::error;
    use std::signer;
    use std::string;
    use aptos_framework::account;
    use aptos_framework::event;

    struct MessageHolder has key {
        message: string::String,
        message_change_events: event::EventHandle<MessageChangeEvent>,
    }

    struct MessageChangeEvent has drop, store {
        from_message: string::String,
        to_message: string::String,
    }

    const ENO_MESSAGE: u64 = 0;

    #[view]
    public fun get_message(addr: address): string::String acquires MessageHolder {
        assert!(exists<MessageHolder>(addr), error::not_found(ENO_MESSAGE));
        *&borrow_global<MessageHolder>(addr).message
    }

    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_to(&account, MessageHolder {
                message,
                message_change_events: account::new_event_handle<MessageChangeEvent>(&account),
            })
        } else {
            let old_message_holder = borrow_global_mut<MessageHolder>(account_addr);
            let from_message = *&old_message_holder.message;
            event::emit_event(&mut old_message_holder.message_change_events, MessageChangeEvent {
                from_message,
                to_message: copy message,
            });
            old_message_holder.message = message;
        }
    }

    #[test(account = @0x1)]
    public entry fun sender_can_set_message(account: signer) acquires MessageHolder {
        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"),
          ENO_MESSAGE
        );
    }
}

This "Hello World" example demonstrates basic Move concepts such as module structure, resource definition, function types, and interaction with the Aptos framework. It offers a simple system for storing and retrieving messages, along with event logging.

Compiling, Testing, and Publishing

Now that we have the module, it is time to learn how to compile your Move code, run unit tests, and finally publish your module to the Aptos testnet.

Initiate the project

Open the terminal and run:

npm run move:init

It will generate a .aptos/config.yaml file containing a funded address on testnet with default settings.

Run the following script if you need more funds:

aptos account fund-with-faucet --account <PROJECT_NAME>

Compiling

This command compiles the Move package located in the move directory.

aptos move compile \
  --package-dir move \
  --named-addresses module_addr=my-aptos-dapp-testnet \
  --skip-fetch-latest-git-deps

The --named-addresses flag is crucial for resolving named addresses in the Move code. In this case, it maps the module_addr named address to the my-aptos-dapp-testnet account address. The --skip-fetch-latest-git-deps flag is used to prevent the compiler from fetching the latest versions of any Git dependencies, which can be useful for ensuring reproducible builds

Testing

aptos move test \
  --package-dir move \
  --named-addresses module_addr=my-aptos-dapp-testnet \
  --skip-fetch-latest-git-deps

Publishing

aptos move publish \
  --package-dir move \
  --named-addresses module_addr=my-aptos-dapp-testnet \
  --skip-fetch-latest-git-deps \
  --profile my-aptos-dapp-testnet
  1. aptos move publish: This is the main command to publish a Move package.

  2. --package-dir move: Specifies the directory containing the Move package.

  3. --named-addresses module_addr=my-aptos-dapp-testnet: Sets the named address for the module.

  4. --skip-fetch-latest-git-deps: Skips fetching the latest Git dependencies.

  5. --profile my-aptos-dapp-testnet: Specifies the profile to use for this operation.

The terminal response looks like this:

Compiling, may take a little while to download git dependencies...
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING boilerplate_template
package size 1818 bytes
Do you want to submit a transaction for a range of [163300 - 244900] Octas at a gas unit price of 100 Octas? [yes/no] >
y
Transaction submitted: https://explorer.aptoslabs.com/txn/0x0e733f22749d0fd0b1dfbd2ee7c0e92cd866c475878eb2dfb857cbe4ca493621?network=testnet
{
  "Result": {
    "transaction_hash": "0x0e733f22749d0fd0b1dfbd2ee7c0e92cd866c475878eb2dfb857cbe4ca493621",
    "gas_used": 1633,
    "gas_unit_price": 100,
    "sender": "2e89176470e3eff01391f9ce7861defebdaf4df829f6333cf5adc322dfccb379",
    "sequence_number": 0,
    "success": true,
    "timestamp_us": 1721339368844105,
    "version": 5494476946,
    "vm_status": "Executed successfully"
  }
}

Interacting with your contract

We can interact with Let's see how to interact with your deployed contract using the CLI.

aptos move run --assume-yes \
  --profile my-aptos-dapp-testnet \
  --function-id 'my-aptos-dapp-testnet::my_module::set_message' \
  --args 'string:hello, blockchain'
  1. aptos move run: This is the main command to run a Move function on the Aptos blockchain.

  2. --assume-yes: This flag automatically confirms any prompts during the command execution, making the process smoother.

  3. --profile my-aptos-dapp-testnet: This specifies the profile to use for the transaction. Profiles in Aptos CLI contain details like the account address and private key.

  4. --function-id 'my-aptos-dapp-testnet::my_module::set_message': This identifies the specific function to call. The function ID follows this structure: address::module_id::function_name. It has three parts:

    • my-aptos-dapp-testnet: The account address where the module is published.

    • my_module: The name of the Move module.

    • set_message: The name of the function within the module.

  5. --args 'string:hello, blockchain': This specifies the arguments to pass to the function. The arguments follow this format: type:value. In this case, it passes a string argument with the value "hello, blockchain".

On success, it will print:

Transaction submitted: https://explorer.aptoslabs.com/txn/0x429bab349cacff691ebfd1cf654ef0ff7bb18dc3910804a7a2b12542a6866155?network=testnet
{
  "Result": {
    "transaction_hash": "0x429bab349cacff691ebfd1cf654ef0ff7bb18dc3910804a7a2b12542a6866155",
    "gas_used": 467,
    "gas_unit_price": 100,
    "sender": "2e89176470e3eff01391f9ce7861defebdaf4df829f6333cf5adc322dfccb379",
    "sequence_number": 1,
    "success": true,
    "timestamp_us": 1721341068307503,
    "version": 5494518608,
    "vm_status": "Executed successfully"
  }
}

Viewing with your contract

To view the get_message function for the given Aptos module, you can use the following bash command:

aptos move view \
    --profile my-aptos-dapp-testnet \
    --function-id module_address::module_id::function_name \
    --args address:<account_address>

In my case

aptos move view \
--profile my-aptos-dapp-testnet \
    --function-id 0x2e89176470e3eff01391f9ce7861defebdaf4df829f6333cf5adc322dfccb379::my_module::get_message \
    --args address:0x2e89176470e3eff01391f9ce7861defebdaf4df829f6333cf5adc322dfccb379

We can also go to the explorer, open the Modules section in the view tab, and use the explorer's user interface directly.

What Next?

To further your journey with Aptos development, you can continue experimenting with different modules and functions.

  • Explore more complex Move smart contracts

  • Integrate your dApp with a frontend using the Aptos SDK

  • Experiment with different Aptos standards (e.g., Tokens, NFTs)

Support

Need some help or want to chat with other Aptos developers? There are two main places you can go:

GitHub Discussions: The Aptos Labs team has an active GitHub Discussions forum where you can ask questions and join conversations. Check it out here: https://github.com/aptos-labs/aptos-developer-discussions/discussions

Discord Community For real-time help and to hang out with the community, the Aptos Discord server is a great spot: https://discord.com/invite/aptosnetwork

0
Subscribe to my newsletter

Read articles from John Fu Lin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

John Fu Lin
John Fu Lin

Follow your curiosity.