开发第一个Aptos Move合约:Todo list

工程创建

$ mkdir aptos_todo_list && cd aptos_todo_list

$ aptos move init --name  aptos_todo_list
{
  "Result": "Success"
}

合约开发

代码来自:https://learn.aptoslabs.com/zh/code-examples/todo-list

module todo_list_addr::todo_list {
    use std::bcs;
    use std::signer;
    use std::vector;
    use std::string::String;
    use aptos_std::string_utils;
    use aptos_framework::object;

    /// Todo list does not exist
    const E_TODO_LIST_DOSE_NOT_EXIST: u64 = 1;
    /// Todo does not exist
    const E_TODO_DOSE_NOT_EXIST: u64 = 2;
    /// Todo is already completed
    const E_TODO_ALREADY_COMPLETED: u64 = 3;

    struct UserTodoListCounter has key {
        counter: u64,
    }

    struct TodoList has key {
        owner: address,
        todos: vector<Todo>,
    }

    struct Todo has store, drop, copy {
        content: String,
        completed: bool,
    }

    // This function is only called once when the module is published for the first time.
    // init_module is optional, you can also have an entry function as the initializer.
    fun init_module(_module_publisher: &signer) {
        // nothing to do here
    }

    // ======================== Write functions ========================

    public entry fun create_todo_list(sender: &signer) acquires UserTodoListCounter {
        let sender_address = signer::address_of(sender);
        let counter = if (exists<UserTodoListCounter>(sender_address)) {
            let counter = borrow_global<UserTodoListCounter>(sender_address);
            counter.counter
        } else {
            let counter = UserTodoListCounter { counter: 0 };
            // store the UserTodoListCounter resource directly under the sender
            move_to(sender, counter);
            0
        };
        // create a new object to hold the todo list, use the contract_addr_counter as seed
        let obj_holds_todo_list = object::create_named_object(
            sender,
            construct_todo_list_object_seed(counter),
        );
        let obj_signer = object::generate_signer(&obj_holds_todo_list);
        let todo_list = TodoList {
            owner: sender_address,
            todos: vector::empty(),
        };
        // store the TodoList resource under the newly created object
        move_to(&obj_signer, todo_list);
        // increment the counter
        let counter = borrow_global_mut<UserTodoListCounter>(sender_address);
        counter.counter = counter.counter + 1;
    }

    public entry fun create_todo(sender: &signer, todo_list_idx: u64, content: String) acquires TodoList {
        let sender_address = signer::address_of(sender);
        let todo_list_obj_addr = object::create_object_address(
            &sender_address,
            construct_todo_list_object_seed(todo_list_idx)
        );
        assert_user_has_todo_list(todo_list_obj_addr);
        let todo_list = borrow_global_mut<TodoList>(todo_list_obj_addr);
        let new_todo = Todo {
            content,
            completed: false
        };
        vector::push_back(&mut todo_list.todos, new_todo);
    }

    public entry fun complete_todo(sender: &signer, todo_list_idx: u64, todo_idx: u64) acquires TodoList {
        let sender_address = signer::address_of(sender);
        let todo_list_obj_addr = object::create_object_address(
            &sender_address,
            construct_todo_list_object_seed(todo_list_idx)
        );
        assert_user_has_todo_list(todo_list_obj_addr);
        let todo_list = borrow_global_mut<TodoList>(todo_list_obj_addr);
        assert_user_has_given_todo(todo_list, todo_idx);
        let todo_record = vector::borrow_mut(&mut todo_list.todos, todo_idx);
        assert!(todo_record.completed == false, E_TODO_ALREADY_COMPLETED);
        todo_record.completed = true;
    }

    // ======================== Read Functions ========================

    // Get how many todo lists the sender has, return 0 if the sender has none.
    #[view]
    public fun get_todo_list_counter(sender: address): u64 acquires UserTodoListCounter {
        if (exists<UserTodoListCounter>(sender)) {
            let counter = borrow_global<UserTodoListCounter>(sender);
            counter.counter
        } else {
            0
        }
    }

    #[view]
    public fun get_todo_list_obj_addr(sender: address, todo_list_idx: u64): address {
        object::create_object_address(&sender, construct_todo_list_object_seed(todo_list_idx))
    }

    #[view]
    public fun has_todo_list(sender: address, todo_list_idx: u64): bool {
        let todo_list_obj_addr = get_todo_list_obj_addr(sender, todo_list_idx);
        exists<TodoList>(todo_list_obj_addr)
    }

    #[view]
    public fun get_todo_list(sender: address, todo_list_idx: u64): (address, u64) acquires TodoList {
        let todo_list_obj_addr = get_todo_list_obj_addr(sender, todo_list_idx);
        assert_user_has_todo_list(todo_list_obj_addr);
        let todo_list = borrow_global<TodoList>(todo_list_obj_addr);
        (todo_list.owner, vector::length(&todo_list.todos))
    }

    #[view]
    public fun get_todo_list_by_todo_list_obj_addr(todo_list_obj_addr: address): (address, u64) acquires TodoList {
        let todo_list = borrow_global<TodoList>(todo_list_obj_addr);
        (todo_list.owner, vector::length(&todo_list.todos))
    }

    #[view]
    public fun get_todo(sender: address, todo_list_idx: u64, todo_idx: u64): (String, bool) acquires TodoList {
        let todo_list_obj_addr = get_todo_list_obj_addr(sender, todo_list_idx);
        assert_user_has_todo_list(todo_list_obj_addr);
        let todo_list = borrow_global<TodoList>(todo_list_obj_addr);
        assert!(todo_idx < vector::length(&todo_list.todos), E_TODO_DOSE_NOT_EXIST);
        let todo_record = vector::borrow(&todo_list.todos, todo_idx);
        (todo_record.content, todo_record.completed)
    }

    // ======================== Helper Functions ========================

    fun assert_user_has_todo_list(user_addr: address) {
        assert!(
            exists<TodoList>(user_addr),
            E_TODO_LIST_DOSE_NOT_EXIST
        );
    }

    fun assert_user_has_given_todo(todo_list: &TodoList, todo_id: u64) {
        assert!(
            todo_id < vector::length(&todo_list.todos),
            E_TODO_DOSE_NOT_EXIST
        );
    }

    fun get_todo_list_obj(sender: address, todo_list_idx: u64): object::Object<TodoList> {
        let addr = get_todo_list_obj_addr(sender, todo_list_idx);
        object::address_to_object(addr)
    }

    fun construct_todo_list_object_seed(counter: u64): vector<u8> {
        // The seed must be unique per todo list creator
        //Wwe add contract address as part of the seed so seed from 2 todo list contract for same user would be different
        bcs::to_bytes(&string_utils::format2(&b"{}_{}", @todo_list_addr, counter))
    }

    // ======================== Unit Tests ========================

    #[test_only]
    use std::string;
    #[test_only]
    use aptos_framework::account;
    #[test_only]
    use aptos_std::debug;

    #[test(admin = @0x100)]
    public entry fun test_end_to_end(admin: signer) acquires TodoList, UserTodoListCounter {
        let admin_addr = signer::address_of(&admin);
        let todo_list_idx = get_todo_list_counter(admin_addr);
        assert!(todo_list_idx == 0, 1);
        account::create_account_for_test(admin_addr);
        assert!(!has_todo_list(admin_addr, todo_list_idx), 2);
        create_todo_list(&admin);
        assert!(get_todo_list_counter(admin_addr) == 1, 3);
        assert!(has_todo_list(admin_addr, todo_list_idx), 4);

        create_todo(&admin, todo_list_idx, string::utf8(b"New Todo"));
        let (todo_list_owner, todo_list_length) = get_todo_list(admin_addr, todo_list_idx);
        debug::print(&string_utils::format1(&b"todo_list_owner: {}", todo_list_owner));
        debug::print(&string_utils::format1(&b"todo_list_length: {}", todo_list_length));
        assert!(todo_list_owner == admin_addr, 5);
        assert!(todo_list_length == 1, 6);

        let (todo_content, todo_completed) = get_todo(admin_addr, todo_list_idx, 0);
        debug::print(&string_utils::format1(&b"todo_content: {}", todo_content));
        debug::print(&string_utils::format1(&b"todo_completed: {}", todo_completed));
        assert!(!todo_completed, 7);
        assert!(todo_content == string::utf8(b"New Todo"), 8);

        complete_todo(&admin, todo_list_idx, 0);
        let (_todo_content, todo_completed) = get_todo(admin_addr, todo_list_idx, 0);
        debug::print(&string_utils::format1(&b"todo_completed: {}", todo_completed));
        assert!(todo_completed, 9);
    }

    #[test(admin = @0x100)]
    public entry fun test_end_to_end_2_todo_lists(admin: signer) acquires TodoList, UserTodoListCounter {
        let admin_addr = signer::address_of(&admin);
        create_todo_list(&admin);
        let todo_list_1_idx = get_todo_list_counter(admin_addr) - 1;
        create_todo_list(&admin);
        let todo_list_2_idx = get_todo_list_counter(admin_addr) - 1;

        create_todo(&admin, todo_list_1_idx, string::utf8(b"New Todo"));
        let (todo_list_owner, todo_list_length) = get_todo_list(admin_addr, todo_list_1_idx);
        assert!(todo_list_owner == admin_addr, 1);
        assert!(todo_list_length == 1, 2);

        let (todo_content, todo_completed) = get_todo(admin_addr, todo_list_1_idx, 0);
        assert!(!todo_completed, 3);
        assert!(todo_content == string::utf8(b"New Todo"), 4);

        complete_todo(&admin, todo_list_1_idx, 0);
        let (_todo_content, todo_completed) = get_todo(admin_addr, todo_list_1_idx, 0);
        assert!(todo_completed, 5);

        create_todo(&admin, todo_list_2_idx, string::utf8(b"New Todo"));
        let (todo_list_owner, todo_list_length) = get_todo_list(admin_addr, todo_list_2_idx);
        assert!(todo_list_owner == admin_addr, 6);
        assert!(todo_list_length == 1, 7);

        let (todo_content, todo_completed) = get_todo(admin_addr, todo_list_2_idx, 0);
        assert!(!todo_completed, 8);
        assert!(todo_content == string::utf8(b"New Todo"), 9);

        complete_todo(&admin, todo_list_2_idx, 0);
        let (_todo_content, todo_completed) = get_todo(admin_addr, todo_list_2_idx, 0);
        assert!(todo_completed, 10);
    }

    #[test(admin = @0x100)]
    #[expected_failure(abort_code = E_TODO_LIST_DOSE_NOT_EXIST, location = Self)]
    public entry fun test_todo_list_does_not_exist(admin: signer) acquires TodoList, UserTodoListCounter {
        let admin_addr = signer::address_of(&admin);
        account::create_account_for_test(admin_addr);
        let todo_list_idx = get_todo_list_counter(admin_addr);
        // account cannot create todo on a todo list (that does not exist
        create_todo(&admin, todo_list_idx, string::utf8(b"New Todo"));
    }

    #[test(admin = @0x100)]
    #[expected_failure(abort_code = E_TODO_DOSE_NOT_EXIST, location = Self)]
    public entry fun test_todo_does_not_exist(admin: signer) acquires TodoList, UserTodoListCounter {
        let admin_addr = signer::address_of(&admin);
        account::create_account_for_test(admin_addr);
        let todo_list_idx = get_todo_list_counter(admin_addr);
        create_todo_list(&admin);
        // can not complete todo that does not exist
        complete_todo(&admin, todo_list_idx, 1);
    }

    #[test(admin = @0x100)]
    #[expected_failure(abort_code = E_TODO_ALREADY_COMPLETED, location = Self)]
    public entry fun test_todo_already_completed(admin: signer) acquires TodoList, UserTodoListCounter {
        let admin_addr = signer::address_of(&admin);
        account::create_account_for_test(admin_addr);
        let todo_list_idx = get_todo_list_counter(admin_addr);
        create_todo_list(&admin);
        create_todo(&admin, todo_list_idx, string::utf8(b"New Todo"));
        complete_todo(&admin, todo_list_idx, 0);
        // can not complete todo that is already completed
        complete_todo(&admin, todo_list_idx, 0);
    }
}

环境初始化

$ aptos init
Configuring for profile default
Choose network from [devnet, testnet, mainnet, local, custom | defaults to devnet]
testnet
Enter your private key as a hex literal (0x...) [Current: None | No input: Generate new key (or keep one if present)]

No key given, generating key...
Account 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022 doesn't exist, creating it and funding it with 100000000 Octas
Account 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022 funded successfully

---
Aptos CLI is now set up for account 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022 as profile default!
 See the account here: https://explorer.aptoslabs.com/account/0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022?network=testnet
 Run `aptos --help` for more information about commands
{
  "Result": "Success"
}

注:生成的地址0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022就是账户地址,别名是default

执行上述命令,将在当前工程目录生成以下文件:

$ cat .aptos/config.yaml 
---
profiles:
  default:
    network: Testnet
    private_key: "0x2a88d0....0b45"
    public_key: "0xd6a31c....27ea6"
    account: cb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022
    rest_url: "https://fullnode.testnet.aptoslabs.com"
    faucet_url: "https://faucet.testnet.aptoslabs.com"

领水

$ aptos account fund-with-faucet --account default
# 等价命令
$ aptos account fund-with-faucet --account 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022

{
  "Result": "Added 100000000 Octas to account 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022"
}

合约编译

$ aptos move build
# 等价命令
$ aptos move compile
Compiling, may take a little while to download git dependencies...
UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.git
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING aptos_todo_list
{
  "Result": [
    "cb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022::todo_list"
  ]
}

注:如果Move.toml中未定义address,需使用命令:

$ aptos move compile --named-addresses todo_list_addr=default

执行单测

$ aptos move test
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING aptos_todo_list
Running Move unit tests
[debug] "todo_list_owner: @0x100"
[debug] "todo_list_length: 1"
[debug] "todo_content: \"New Todo\""
[debug] "todo_completed: false"
[debug] "todo_completed: true"
[ PASS    ] 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022::todo_list::test_end_to_end
[ PASS    ] 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022::todo_list::test_end_to_end_2_todo_lists
[ PASS    ] 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022::todo_list::test_todo_already_completed
[ PASS    ] 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022::todo_list::test_todo_does_not_exist
[ PASS    ] 0xcb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022::todo_list::test_todo_list_does_not_exist
Test result: OK. Total tests: 5; passed: 5; failed: 0
{
  "Result": "Success"
}

合约部署

$ aptos move publish
Compiling, may take a little while to download git dependencies...
UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.git
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING aptos_todo_list
package size 4576 bytes

Do you want to submit a transaction for a range of [274000 - 411000] Octas at a gas unit price of 100 Octas? [yes/no] >
yes
Transaction submitted: https://explorer.aptoslabs.com/txn/0x0b7b5b75a81040352741c1342db44a57faea5b8492f2248bab54577b6fd146a3?network=testnet
{
  "Result": {
    "transaction_hash": "0x0b7b5b75a81040352741c1342db44a57faea5b8492f2248bab54577b6fd146a3",
    "gas_used": 2740,
    "gas_unit_price": 100,
    "sender": "cb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022",
    "sequence_number": 0,
    "success": true,
    "timestamp_us": 1725804736732089,
    "version": 5954581859,
    "vm_status": "Executed successfully"
  }
}

合约调用

create_todo_list

$ aptos move run \
  --function-id 'default::todo_list::create_todo_list' 
Do you want to submit a transaction for a range of [138600 - 207900] Octas at a gas unit price of 100 Octas? [yes/no] >
yes
Transaction submitted: https://explorer.aptoslabs.com/txn/0x4aad67f5976cb875e7aa4a10f6210fca79dbacaaccc004a3184eafe7b72a5fe2?network=testnet
{
  "Result": {
    "transaction_hash": "0x4aad67f5976cb875e7aa4a10f6210fca79dbacaaccc004a3184eafe7b72a5fe2",
    "gas_used": 1386,
    "gas_unit_price": 100,
    "sender": "cb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022",
    "sequence_number": 3,
    "success": true,
    "timestamp_us": 1725807418268333,
    "version": 5954728316,
    "vm_status": "Executed successfully"
  }
}

create_todo

$ aptos move run \
  --function-id 'default::todo_list::create_todo' \
  --args 'u64:0' 'string:study'
Do you want to submit a transaction for a range of [800 - 1200] Octas at a gas unit price of 100 Octas? [yes/no] >
yes
Transaction submitted: https://explorer.aptoslabs.com/txn/0x0559761e6e0e537832a7293a9afc02779a501534052a5ba1b8632c1804cbe6d3?network=testnet
{
  "Result": {
    "transaction_hash": "0x0559761e6e0e537832a7293a9afc02779a501534052a5ba1b8632c1804cbe6d3",
    "gas_used": 8,
    "gas_unit_price": 100,
    "sender": "cb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022",
    "sequence_number": 4,
    "success": true,
    "timestamp_us": 1725807739852836,
    "version": 5954745955,
    "vm_status": "Executed successfully"
  }
}

image-20240908230348639

complete_todo

$ aptos move run \
  --function-id 'default::todo_list::complete_todo' \
  --args 'u64:0' 'u64:0'
Do you want to submit a transaction for a range of [600 - 900] Octas at a gas unit price of 100 Octas? [yes/no] >
yes
Transaction submitted: https://explorer.aptoslabs.com/txn/0x27bddde7703199720412a67854e4f04bac62c0213b1836f7bb289625ef93f503?network=testnet
{
  "Result": {
    "transaction_hash": "0x27bddde7703199720412a67854e4f04bac62c0213b1836f7bb289625ef93f503",
    "gas_used": 6,
    "gas_unit_price": 100,
    "sender": "cb3303e6b96a1a666347bd06e27d24cc0cb09e204fedb6223a7bc9091dc49022",
    "sequence_number": 5,
    "success": true,
    "timestamp_us": 1725807879910254,
    "version": 5954753634,
    "vm_status": "Executed successfully"
  }
}

image-20240908230558475

学习材料

https://aptos.dev/en/build/guides/first-move-module