创建智能合约
本节是在 Aptos 上构建端到端 dApp教程的第一步。如果你还没有完成介绍,请先阅读该页面,并确认环境满足其中列出的前置条件。
环境准备好后,先了解 contract 目录。

什么是 Move.toml 文件?
Section titled “什么是 Move.toml 文件?”Move.toml 是 Move 包的清单文件,负责定义包名、地址别名和依赖。它类似其他语言生态中的项目配置文件。
为什么需要 sources 目录?
Section titled “为什么需要 sources 目录?”sources 目录存放要发布到链上的 Move 模块。Aptos 编译器会从这里收集包的生产代码。
什么是 tests 目录?
Section titled “什么是 tests 目录?”tests 目录存放只在本地测试期间编译和执行的 Move 测试,不会随模块发布到链上。
创建 Move 模块
Section titled “创建 Move 模块”创建 todolist 模块,并以 .env 中配置的发布者地址替换模块地址。不要提交包含真实私钥的 .env 文件。
按以下步骤配置或添加代码:
PROJECT_NAME=my-first-dappVITE_APP_NETWORK=devnetVITE_APTOS_API_KEY=YOUR_API_KEYVITE_MODULE_PUBLISHER_ACCOUNT_ADDRESS=0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb#This is the module publisher account's private key. Be cautious about who you share it with, and ensure it is not exposed when deploying your dApp.VITE_MODULE_PUBLISHER_ACCOUNT_PRIVATE_KEY=0x84638fd5c42d0937503111a587307169842f355ab661b5253c01cfe389373f43模板还包含预生成的 message_board.move、对应测试文件和 Move.toml。本教程不会使用 message_board.move,请删除它,然后在 sources 目录创建 todolist.move。
继续添加或验证以下内容:
module todolist_addr::todolist {
}继续添加或验证以下内容:
[package] name = "Todolist" version = "1.0.0" authors = []
[addresses] todolist_addr = "_"
[dependencies] AptosFramework = { git = "https://github.com/aptos-labs/aptos-framework.git", rev = "mainnet", subdir = "aptos-framework" }
[dev-dependencies]继续添加或验证以下内容:
module <account-address>::<module-name> {
}Move.toml 中的 '_' 是什么?
Section titled “Move.toml 中的 '_' 是什么?”地址别名中的 _ 是占位符。在发布时,CLI 会将其替换为实际模块发布者地址。
运行 move 命令的脚本
Section titled “运行 move 命令的脚本”使用项目脚本统一运行编译、测试和发布命令,避免在不同终端中遗漏环境变量。
按以下步骤配置或添加代码:
... namedAddresses: { todolist_addr: process.env.VITE_MODULE_PUBLISHER_ACCOUNT_ADDRESS, }, ...待办事项模块维护任务列表,并将每个任务与创建它的账户关联。下面的函数逐步补全这一状态和公开接口。
- 账户创建一个新列表。
- 账户在自己的列表中创建任务;每次创建任务时都发出
TaskCreated事件。 - 账户可以将自己的任务标记为已完成。
TodoList 保存任务表和任务计数器;Task 保存任务 ID、创建者地址、内容以及完成状态。
按以下步骤配置或添加代码:
.../// Main resource that stores all tasks for an accountstruct TodoList has key { tasks: Table<u64, Task>, task_counter: u64}
/// Individual task structurestruct Task has store, drop, copy { task_id: u64, creator_addr: address, content: String, completed: bool,}...继续添加或验证以下内容:
... use aptos_std::table::Table; use std::string::String; ...继续添加或验证以下内容:
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 Todolist { "Result": [ "1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist" ] }先创建列表资源并初始化任务存储;用户必须先创建自己的列表才能添加任务。
按以下步骤配置或添加代码:
public entry fun create_list(account: &signer){
}继续添加或验证以下内容:
/// Initializes a new todo list for the account public entry fun create_list(account: &signer) { let tasks_holder = TodoList { tasks: table::new(), task_counter: 0 }; // Move the TodoList resource under the signer account move_to(account, tasks_holder); }添加任务时验证调用者已拥有列表,再写入任务内容和状态。
按以下步骤配置或添加代码:
/// Creates a new task in the todo list public entry fun create_task(account: &signer, content: String) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Increment task counter let counter = todo_list.task_counter + 1;
// Create a new task let new_task = Task { task_id: counter, creator_addr: signer_address, content, completed: false };
// Add the new task to the tasks table todo_list.tasks.upsert(counter, new_task);
// Update the task counter todo_list.task_counter = counter;
// Emit a task created event event::emit(TaskCreated { task_id: counter, creator_addr: signer_address, content, completed: false }) }继续添加或验证以下内容:
#[event] struct TaskCreated has drop, store { task_id: u64, creator_addr: address, content: String, completed: bool, }继续添加或验证以下内容:
use aptos_framework::event; use aptos_std::table::{Self, Table}; // This one we already have, need to modify it use std::signer;完成任务时验证任务存在且调用者有权操作,然后更新完成状态。
按以下步骤配置或添加代码:
/// Marks a task as completed public entry fun complete_task(account: &signer, task_id: u64) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Get the task record let task_record = todo_list.tasks.borrow_mut(task_id);
// Mark the task as completed task_record.completed = true; }对空任务、越界索引和未初始化列表使用断言,避免无效交易写入状态。
按以下步骤配置或添加代码:
public entry fun create_task(account: &signer, content: String) acquires TodoList { // gets the signer address let signer_address = signer::address_of(account);
// assert signer has created a list assert!(exists<TodoList>(signer_address), 1);
... }继续添加或验证以下内容:
/// Marks a task as completed public entry fun complete_task(account: &signer, task_id: u64) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), 1);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Ensure the task exists assert!(todo_list.tasks.contains(task_id), 2);
// Get the task record let task_record = todo_list.tasks.borrow_mut(task_id);
// Ensure the task is not already completed assert!(task_record.completed == false, 3);
// Mark the task as completed task_record.completed = true; }继续添加或验证以下内容:
// Errors/// Account has not initialized a todo listconst ENOT_INITIALIZED: u64 = 1;/// Task does not existconst ETASK_DOESNT_EXIST: u64 = 2;/// Task is already completedconst ETASK_IS_COMPLETED: u64 = 3;继续添加或验证以下内容:
/// Creates a new task in the todo listpublic entry fun create_task(account: &signer, content: String) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
...}
/// Marks a task as completedpublic entry fun complete_task(account: &signer, task_id: u64) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Ensure the task exists assert!(todo_list.tasks.contains(task_id), ETASK_DOESNT_EXIST);
// Get the task record let task_record = todo_list.tasks.borrow_mut(task_id);
// Ensure the task is not already completed assert!(task_record.completed == false, ETASK_IS_COMPLETED);
// Mark the task as completed task_record.completed = true;}继续添加或验证以下内容:
Compiling, may take a little while to download git dependencies...UPDATING GIT DEPENDENCY https://github.com/aptos-labs/aptos-core.gitINCLUDING DEPENDENCY AptosFrameworkINCLUDING DEPENDENCY AptosStdlibINCLUDING DEPENDENCY MoveStdlibBUILDING MessageBoard{ "Result": [ "1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist" ]}测试覆盖创建列表、添加任务、完成任务和失败路径。测试应在发布前通过。
按以下步骤配置或添加代码:
// create a list // create a task // update task as completed继续添加或验证以下内容:
#[test] public entry fun test_flow() {
}继续添加或验证以下内容:
#[test(admin = @0x123)] public entry fun test_flow(admin: signer) acquires TodoList { // Create an admin account for testing account::create_account_for_test(signer::address_of(&admin));
// Initialize a todo list for the admin account create_list(&admin);
// Create a task and verify it was added correctly create_task(&admin, string::utf8(b"Create e2e guide video for aptos devs.")); let todo_list = borrow_global<TodoList>(signer::address_of(&admin)); assert!(todo_list.task_counter == 1, 5);
// Verify task details let task_record = todo_list.tasks.borrow(todo_list.task_counter); assert!(task_record.task_id == 1, 6); assert!(task_record.completed == false, 7); assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 8); assert!(task_record.creator_addr == signer::address_of(&admin), 9);
// Complete the task and verify it was marked as completed complete_task(&admin, 1); let todo_list = borrow_global<TodoList>(signer::address_of(&admin)); let task_record = todo_list.tasks.borrow(1); assert!(task_record.task_id == 1, 10); assert!(task_record.completed == true, 11); assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 12); assert!(task_record.creator_addr == signer::address_of(&admin), 13); }继续添加或验证以下内容:
#[test_only] use aptos_framework::account; #[test_only] use std::string::{Self};继续添加或验证以下内容:
Running Move unit tests [ PASS ] 0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist::test_flow Test result: OK. Total tests: 1; passed: 1; failed: 0 { "Result": "Success" }现在可以打开 Aptos Explorer 查看交易详情,也可以通过对象地址确认模块已发布到链上。
继续添加或验证以下内容:
#[test(admin = @0x123)] #[expected_failure(abort_code = ENOT_INITIALIZED)] public entry fun account_can_not_update_task(admin: signer) acquires TodoList { // Create an admin account for testing account::create_account_for_test(signer::address_of(&admin));
// Attempt to complete a task without creating a list first (should fail) complete_task(&admin, 2); }继续添加或验证以下内容:
Running Move unit tests [ PASS ] 0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist::account_can_not_update_task [ PASS ] 0x1cecfef9e239eff12fb1a3d189a121c37f48908d86c0e9c02ec103e0a05ddebb::todolist::test_flow Test result: OK. Total tests: 2; passed: 2; failed: 0 { "Result": "Success" }将 todolist 模块发布到链上
Section titled “将 todolist 模块发布到链上”配置发布账户和网络后编译并发布模块。发布前检查地址、私钥和 API Key 均来自本地环境变量。
按以下步骤配置或添加代码:
Transaction submitted: https://explorer.aptoslabs.com/txn/0x68dadf24b9ec29b9c32bd78836d20032de615bbef5f10db580228577f7ca945a?network=devnet Code was successfully deployed to object address 0x2bce4f7bb8a67641875ba5076850d2154eb9621b0c021982bdcd80731279efa6 { "Result": "Success" }完整的 Todolist 模块代码
Section titled “完整的 Todolist 模块代码”以下是前述片段组合后的完整模块,便于对照最终源文件。
按以下步骤配置或添加代码:
module todolist_addr::todolist { use aptos_framework::event; use aptos_std::table::{Self, Table}; use std::signer; use std::string::String;
#[test_only] use aptos_framework::account; #[test_only] use std::string::{Self};
// Errors /// Account has not initialized a todo list const ENOT_INITIALIZED: u64 = 1; /// Task does not exist const ETASK_DOESNT_EXIST: u64 = 2; /// Task is already completed const ETASK_IS_COMPLETED: u64 = 3;
#[event] struct TaskCreated has drop, store { task_id: u64, creator_addr: address, content: String, completed: bool, }
/// Main resource that stores all tasks for an account struct TodoList has key { tasks: Table<u64, Task>, task_counter: u64 }
/// Individual task structure struct Task has store, drop, copy { task_id: u64, creator_addr: address, content: String, completed: bool, }
/// Initializes a new todo list for the account public entry fun create_list(account: &signer) { let tasks_holder = TodoList { tasks: table::new(), task_counter: 0 }; // Move the TodoList resource under the signer account move_to(account, tasks_holder); }
/// Creates a new task in the todo list public entry fun create_task(account: &signer, content: String) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Increment task counter let counter = todo_list.task_counter + 1;
// Create a new task let new_task = Task { task_id: counter, creator_addr: signer_address, content, completed: false };
// Add the new task to the tasks table todo_list.tasks.upsert(counter, new_task);
// Update the task counter todo_list.task_counter = counter;
// Emit a task created event event::emit(TaskCreated { task_id: counter, creator_addr: signer_address, content, completed: false }) }
/// Marks a task as completed public entry fun complete_task(account: &signer, task_id: u64) acquires TodoList { // Get the signer address let signer_address = signer::address_of(account);
// Ensure the account has initialized a todo list assert!(exists<TodoList>(signer_address), ENOT_INITIALIZED);
// Get the TodoList resource let todo_list = borrow_global_mut<TodoList>(signer_address);
// Ensure the task exists assert!(todo_list.tasks.contains(task_id), ETASK_DOESNT_EXIST);
// Get the task record let task_record = todo_list.tasks.borrow_mut(task_id);
// Ensure the task is not already completed assert!(task_record.completed == false, ETASK_IS_COMPLETED);
// Mark the task as completed task_record.completed = true; }
#[test(admin = @0x123)] public entry fun test_flow(admin: signer) acquires TodoList { // Create an admin account for testing account::create_account_for_test(signer::address_of(&admin));
// Initialize a todo list for the admin account create_list(&admin);
// Create a task and verify it was added correctly create_task(&admin, string::utf8(b"Create e2e guide video for aptos devs.")); let todo_list = borrow_global<TodoList>(signer::address_of(&admin)); assert!(todo_list.task_counter == 1, 5);
// Verify task details let task_record = todo_list.tasks.borrow(todo_list.task_counter); assert!(task_record.task_id == 1, 6); assert!(task_record.completed == false, 7); assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 8); assert!(task_record.creator_addr == signer::address_of(&admin), 9);
// Complete the task and verify it was marked as completed complete_task(&admin, 1); let todo_list = borrow_global<TodoList>(signer::address_of(&admin)); let task_record = todo_list.tasks.borrow(1); assert!(task_record.task_id == 1, 10); assert!(task_record.completed == true, 11); assert!(task_record.content == string::utf8(b"Create e2e guide video for aptos devs."), 12); assert!(task_record.creator_addr == signer::address_of(&admin), 13); }
#[test(admin = @0x123)] #[expected_failure(abort_code = ENOT_INITIALIZED)] public entry fun account_can_not_update_task(admin: signer) acquires TodoList { // Create an admin account for testing account::create_account_for_test(signer::address_of(&admin));
// Attempt to complete a task without creating a list first (should fail) complete_task(&admin, 2); }}