Skip to content

Latest commit

 

History

History
743 lines (574 loc) · 19 KB

File metadata and controls

743 lines (574 loc) · 19 KB

Aptos Move Development Guide

A comprehensive guide based on building a SimpleCoin (ERC-20 equivalent) on Aptos.


Table of Contents

  1. Overview: Aptos vs Ethereum
  2. Project Setup
  3. Move Language Fundamentals
  4. Fungible Asset Standard (ERC-20 Equivalent)
  5. Aptos CLI Commands
  6. Frontend Integration
  7. Common Patterns
  8. Troubleshooting

Overview: Aptos vs Ethereum

Concept Ethereum/Solidity Aptos/Move
Smart Contract Language Solidity Move
Token Standard ERC-20 Fungible Asset (FA) or Coin
Account Model EOA + Contract Accounts Unified Resource Accounts
Storage Contract Storage Slots Global Storage with Resources
Authentication msg.sender &signer reference
Safety Runtime checks Compile-time + Runtime (resource safety)
Gas Token ETH APT
VM EVM MoveVM

Key Move Advantages

  • Resource-oriented: Assets can't be duplicated or destroyed accidentally
  • Formal verification: Built-in support for proving correctness
  • Module system: Better code organization and upgradeability
  • Type safety: Strong typing with abilities system

Project Setup

Directory Structure

my_project/
├── .aptos/
│   └── config.yaml          # Account keys and network config
├── sources/
│   └── my_module.move       # Move source files
├── scripts/                  # Transaction scripts (optional)
├── tests/                    # Test files
├── build/                    # Compiled output
└── Move.toml                 # Project manifest

Initialize Project

# Create project directory
mkdir my_project && cd my_project
mkdir sources scripts tests

# Initialize Aptos (creates .aptos/config.yaml with new keypair)
aptos init --network testnet

# This outputs your account address, e.g.:
# 0xde2454bdd311fafec119a096c2763ddea839cd1e188418981f90f3d1487bfe16

Move.toml Configuration

[package]
name = "MyProject"
version = "1.0.0"
authors = []

[addresses]
# Replace with your account address from `aptos init`
my_project = "de2454bdd311fafec119a096c2763ddea839cd1e188418981f90f3d1487bfe16"

[dependencies.AptosFramework]
git = "https://github.com/aptos-labs/aptos-framework.git"
rev = "mainnet"
subdir = "aptos-framework"

[dev-dependencies]

Fund Testnet Account

# Option 1: Web faucet
# Visit: https://aptos.dev/network/faucet?address=YOUR_ADDRESS

# Option 2: CLI with faucet URL
aptos account fund-with-faucet \
  --account YOUR_ADDRESS \
  --faucet-url https://faucet.testnet.aptoslabs.com

Move Language Fundamentals

Module Structure

/// Module documentation
module my_address::module_name {
    // 1. Imports
    use std::string::{Self, String};
    use std::signer;
    use aptos_framework::event;

    // 2. Constants
    const E_NOT_AUTHORIZED: u64 = 1;
    const E_INSUFFICIENT_BALANCE: u64 = 2;

    // 3. Structs (Resources)
    struct MyResource has key, store {
        value: u64,
    }

    // 4. Events
    #[event]
    struct MyEvent has drop, store {
        actor: address,
        value: u64,
    }

    // 5. Init function (called once on deploy)
    fun init_module(deployer: &signer) {
        // Initialization logic
    }

    // 6. Entry functions (callable from transactions)
    public entry fun do_something(account: &signer, value: u64) {
        // Transaction logic
    }

    // 7. View functions (read-only, no gas)
    #[view]
    public fun get_value(addr: address): u64 {
        // Read and return data
    }

    // 8. Internal functions
    fun helper_function(): u64 {
        42
    }
}

Abilities System

Move has 4 abilities that control what you can do with types:

Ability Meaning
copy Value can be copied
drop Value can be discarded
store Value can be stored in global storage
key Value can be a top-level resource in global storage
// Can be stored as a resource, and nested in other structs
struct Token has key, store {
    amount: u64,
}

// Can be emitted as event (must be droppable)
#[event]
struct TransferEvent has drop, store {
    from: address,
    to: address,
    amount: u64,
}

// Primitive types have all abilities by default
// Structs have NO abilities by default - you must declare them

Signer and Authentication

// The signer represents the transaction sender (authenticated)
public entry fun transfer(sender: &signer, to: address, amount: u64) {
    // Get address from signer
    let sender_addr = signer::address_of(sender);

    // Only the signer can authorize actions on their account
    // This is Move's equivalent to msg.sender in Solidity
}

Global Storage Operations

// Store a resource under an account
move_to<MyResource>(account, MyResource { value: 100 });

// Check if resource exists
let exists = exists<MyResource>(addr);

// Borrow immutable reference
let resource = borrow_global<MyResource>(addr);

// Borrow mutable reference
let resource = borrow_global_mut<MyResource>(addr);

// Remove resource from storage
let MyResource { value } = move_from<MyResource>(addr);

// IMPORTANT: Functions that access global storage must declare it:
public fun get_value(addr: address): u64 acquires MyResource {
    borrow_global<MyResource>(addr).value
}

Events

use aptos_framework::event;

#[event]
struct TransferEvent has drop, store {
    from: address,
    to: address,
    amount: u64,
}

// Emit event
event::emit(TransferEvent {
    from: sender_addr,
    to: recipient,
    amount: 100,
});

Error Handling

// Define error codes as constants
const E_NOT_OWNER: u64 = 1;
const E_INSUFFICIENT_BALANCE: u64 = 2;

// Assert with error code
assert!(sender_addr == @my_module, E_NOT_OWNER);

// The transaction aborts if assertion fails
// Error codes help identify the failure reason

Fungible Asset Standard (ERC-20 Equivalent)

Aptos has two token standards:

  1. Coin (legacy) - Simpler but less flexible
  2. Fungible Asset (modern) - More features, recommended for new projects

Complete Fungible Asset Example

module my_address::my_token {
    use std::string::{Self, String};
    use std::signer;
    use std::option;
    use aptos_framework::object::{Self, Object, ExtendRef};
    use aptos_framework::fungible_asset::{Self, Metadata, MintRef, TransferRef, BurnRef};
    use aptos_framework::primary_fungible_store;
    use aptos_framework::event;

    const E_NOT_ADMIN: u64 = 1;

    /// Seed for creating the token metadata object
    const ASSET_SYMBOL: vector<u8> = b"MYTOKEN";

    /// Stores management capabilities
    struct ManagedFungibleAsset has key {
        mint_ref: MintRef,       // Capability to mint
        transfer_ref: TransferRef, // Capability to transfer
        burn_ref: BurnRef,       // Capability to burn
        extend_ref: ExtendRef,   // Capability to extend object
    }

    #[event]
    struct MintEvent has drop, store {
        recipient: address,
        amount: u64,
    }

    /// Called once when module is deployed
    fun init_module(admin: &signer) {
        // Create a named object for the token metadata
        let constructor_ref = object::create_named_object(admin, ASSET_SYMBOL);

        // Initialize the fungible asset
        primary_fungible_store::create_primary_store_enabled_fungible_asset(
            &constructor_ref,
            option::none(),                              // max_supply (none = unlimited)
            string::utf8(b"My Token"),                   // name
            string::utf8(ASSET_SYMBOL),                  // symbol
            8,                                           // decimals
            string::utf8(b"https://example.com/icon.png"), // icon_uri
            string::utf8(b"https://example.com"),        // project_uri
        );

        // Generate management capabilities
        let mint_ref = fungible_asset::generate_mint_ref(&constructor_ref);
        let burn_ref = fungible_asset::generate_burn_ref(&constructor_ref);
        let transfer_ref = fungible_asset::generate_transfer_ref(&constructor_ref);
        let extend_ref = object::generate_extend_ref(&constructor_ref);

        // Store capabilities in the metadata object
        let metadata_signer = object::generate_signer(&constructor_ref);
        move_to(&metadata_signer, ManagedFungibleAsset {
            mint_ref,
            transfer_ref,
            burn_ref,
            extend_ref,
        });
    }

    /// Get the token metadata object
    #[view]
    public fun get_metadata(): Object<Metadata> {
        let addr = object::create_object_address(&@my_address, ASSET_SYMBOL);
        object::address_to_object<Metadata>(addr)
    }

    /// Mint tokens (admin only)
    public entry fun mint(admin: &signer, to: address, amount: u64) acquires ManagedFungibleAsset {
        // Check admin authorization
        assert!(signer::address_of(admin) == @my_address, E_NOT_ADMIN);

        let metadata = get_metadata();
        let managed = borrow_global<ManagedFungibleAsset>(object::object_address(&metadata));

        // Ensure recipient has a token store
        let to_wallet = primary_fungible_store::ensure_primary_store_exists(to, metadata);

        // Mint and deposit
        let fa = fungible_asset::mint(&managed.mint_ref, amount);
        fungible_asset::deposit_with_ref(&managed.transfer_ref, to_wallet, fa);

        event::emit(MintEvent { recipient: to, amount });
    }

    /// Transfer tokens
    public entry fun transfer(from: &signer, to: address, amount: u64) {
        let metadata = get_metadata();
        primary_fungible_store::transfer(from, metadata, to, amount);
    }

    /// Burn tokens
    public entry fun burn(account: &signer, amount: u64) acquires ManagedFungibleAsset {
        let metadata = get_metadata();
        let managed = borrow_global<ManagedFungibleAsset>(object::object_address(&metadata));
        let wallet = primary_fungible_store::primary_store(signer::address_of(account), metadata);
        fungible_asset::burn_from(&managed.burn_ref, wallet, amount);
    }

    /// Get balance
    #[view]
    public fun balance(account: address): u64 {
        let metadata = get_metadata();
        if (primary_fungible_store::primary_store_exists(account, metadata)) {
            primary_fungible_store::balance(account, metadata)
        } else {
            0
        }
    }

    #[view]
    public fun name(): String { fungible_asset::name(get_metadata()) }

    #[view]
    public fun symbol(): String { fungible_asset::symbol(get_metadata()) }

    #[view]
    public fun decimals(): u8 { fungible_asset::decimals(get_metadata()) }
}

Key Fungible Asset Concepts

Concept Description
Metadata Token information (name, symbol, decimals)
FungibleStore Holds token balance for an account
MintRef Capability to create new tokens
BurnRef Capability to destroy tokens
TransferRef Capability to move tokens (bypass frozen)
primary_fungible_store Default token store for each account

Aptos CLI Commands

Project Management

# Initialize new project with testnet account
aptos init --network testnet

# Initialize with specific network
aptos init --network mainnet
aptos init --network devnet

# Compile Move modules
aptos move compile

# Compile with named addresses override
aptos move compile --named-addresses my_module=0x123...

# Run tests
aptos move test

# Publish/deploy module
aptos move publish

# Publish with gas options
aptos move publish --max-gas 10000 --gas-unit-price 100

# Publish and assume yes to prompts
aptos move publish --assume-yes

Account Management

# Check account balance
aptos account balance --account YOUR_ADDRESS

# Fund account (testnet only)
aptos account fund-with-faucet \
  --account YOUR_ADDRESS \
  --faucet-url https://faucet.testnet.aptoslabs.com

# List account resources
aptos account list --account YOUR_ADDRESS

Interacting with Contracts

# Call a view function
aptos move view \
  --function-id 'ADDRESS::module::function_name' \
  --args 'address:0x123'

# Execute entry function
aptos move run \
  --function-id 'ADDRESS::module::function_name' \
  --args 'u64:100' 'address:0x456'

Network Configuration

# Config stored in .aptos/config.yaml
profiles:
  default:
    network: Testnet
    private_key: "0x..."
    public_key: "0x..."
    account: "your_address_without_0x"
    rest_url: "https://fullnode.testnet.aptoslabs.com"

Frontend Integration

Dependencies

npm install @aptos-labs/ts-sdk @aptos-labs/wallet-adapter-react

Wallet Provider Setup

// main.tsx
import { AptosWalletAdapterProvider } from '@aptos-labs/wallet-adapter-react'

createRoot(document.getElementById('root')!).render(
  <AptosWalletAdapterProvider autoConnect={true}>
    <App />
  </AptosWalletAdapterProvider>
)

Using the Wallet Hook

import { useWallet } from '@aptos-labs/wallet-adapter-react'
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk'

const MODULE_ADDRESS = '0x...'
const MODULE_NAME = 'my_module'

// Initialize Aptos client
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }))

function App() {
  const {
    connect,           // Connect wallet
    disconnect,        // Disconnect wallet
    account,          // Current account info
    connected,        // Connection status
    signAndSubmitTransaction,  // Submit transactions
    wallets           // Available wallets
  } = useWallet()

  // Connect to first available wallet
  const handleConnect = async () => {
    if (wallets && wallets.length > 0) {
      await connect(wallets[0].name)
    }
  }

  // Get account address as string
  const addressStr = account?.address?.toString()

  return (...)
}

Calling View Functions

const fetchBalance = async () => {
  if (!account?.address) return

  try {
    const result = await aptos.view({
      payload: {
        function: `${MODULE_ADDRESS}::${MODULE_NAME}::balance`,
        functionArguments: [account.address.toString()],
      },
    })

    // result is an array, first element is the return value
    const balance = Number(result[0]) / 1e8  // Adjust for decimals
    console.log('Balance:', balance)
  } catch (error) {
    console.error('View call failed:', error)
  }
}

Submitting Transactions

const handleMint = async (amount: number) => {
  if (!account?.address) return

  try {
    const response = await signAndSubmitTransaction({
      data: {
        function: `${MODULE_ADDRESS}::${MODULE_NAME}::mint`,
        functionArguments: [
          account.address.toString(),  // recipient
          Math.floor(amount * 1e8),    // amount with decimals
        ],
      },
      options: {
        maxGasAmount: 10000,  // Important: set gas limit
      },
    })

    // Wait for transaction confirmation
    await aptos.waitForTransaction({ transactionHash: response.hash })

    console.log('Transaction successful:', response.hash)
  } catch (error) {
    console.error('Transaction failed:', error)
  }
}

Transaction Options

const response = await signAndSubmitTransaction({
  data: {
    function: `${MODULE_ADDRESS}::${MODULE_NAME}::function`,
    functionArguments: [...],
    typeArguments: [],  // For generic functions
  },
  options: {
    maxGasAmount: 10000,           // Max gas units
    gasUnitPrice: 100,             // Price per gas unit in octas
    expireTimestamp: Date.now() + 60000,  // Expiration
  },
})

Common Patterns

Admin-Only Functions

const E_NOT_ADMIN: u64 = 1;

public entry fun admin_function(admin: &signer) {
    assert!(signer::address_of(admin) == @my_module, E_NOT_ADMIN);
    // Admin logic here
}

Storing Configuration

struct Config has key {
    admin: address,
    fee_percentage: u64,
    paused: bool,
}

fun init_module(deployer: &signer) {
    move_to(deployer, Config {
        admin: signer::address_of(deployer),
        fee_percentage: 100,  // 1%
        paused: false,
    });
}

public entry fun update_fee(admin: &signer, new_fee: u64) acquires Config {
    let config = borrow_global_mut<Config>(@my_module);
    assert!(signer::address_of(admin) == config.admin, E_NOT_ADMIN);
    config.fee_percentage = new_fee;
}

Working with Timestamps

use aptos_framework::timestamp;

public fun is_expired(deadline: u64): bool {
    timestamp::now_seconds() > deadline
}

Working with Coins (APT)

use aptos_framework::coin;
use aptos_framework::aptos_coin::AptosCoin;

public entry fun deposit_apt(account: &signer, amount: u64) {
    let coins = coin::withdraw<AptosCoin>(account, amount);
    // Do something with coins
    coin::deposit(signer::address_of(account), coins);
}

Troubleshooting

Common Errors

Error Cause Solution
E_NOT_ADMIN / ENOT_AUTHORIZED Wrong account calling admin function Use the deployer account
EINSUFFICIENT_BALANCE Not enough tokens Check balance first
module_not_found Contract not deployed Run aptos move publish
MAX_GAS_UNITS_BELOW_MIN Gas limit too low Add options: { maxGasAmount: 10000 }
RESOURCE_NOT_FOUND Accessing non-existent resource Check exists<T>(addr) first

Debugging Tips

  1. Check transaction on explorer: https://explorer.aptoslabs.com/?network=testnet

  2. View function testing: Use CLI before integrating frontend

    aptos move view --function-id 'ADDR::module::view_func'
  3. Account mismatch: Ensure wallet is on correct network (testnet/mainnet)

  4. Address format:

    • Move uses addresses without 0x prefix in Move.toml
    • Frontend/CLI use 0x prefix
    • account.address in wallet adapter is an object, use .toString()

Resources


Quick Reference

# Setup
aptos init --network testnet

# Development cycle
aptos move compile          # Compile
aptos move test            # Test
aptos move publish         # Deploy

# Fund testnet account
# Visit: https://aptos.dev/network/faucet?address=YOUR_ADDRESS
// Module template
module my_addr::my_module {
    use std::signer;

    struct MyResource has key { value: u64 }

    fun init_module(deployer: &signer) {
        move_to(deployer, MyResource { value: 0 });
    }

    public entry fun set_value(account: &signer, val: u64) acquires MyResource {
        borrow_global_mut<MyResource>(signer::address_of(account)).value = val;
    }

    #[view]
    public fun get_value(addr: address): u64 acquires MyResource {
        borrow_global<MyResource>(addr).value
    }
}