开发第一个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"
}
}
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"
}
}
学习材料
https://aptos.dev/en/build/guides/first-move-module