跳转到内容

创建智能合约

本节是在 Aptos 上构建端到端 dApp教程的第一步。如果你还没有完成介绍,请先阅读该页面,并确认环境满足其中列出的前置条件

环境准备好后,先了解 contract 目录。

contract-directory

Move.toml 是 Move 包的清单文件,负责定义包名、地址别名和依赖。它类似其他语言生态中的项目配置文件。

sources 目录存放要发布到链上的 Move 模块。Aptos 编译器会从这里收集包的生产代码。

tests 目录存放只在本地测试期间编译和执行的 Move 测试,不会随模块发布到链上。

创建 todolist 模块,并以 .env 中配置的发布者地址替换模块地址。不要提交包含真实私钥的 .env 文件。

按以下步骤配置或添加代码:

Terminal window
PROJECT_NAME=my-first-dapp
VITE_APP_NETWORK=devnet
VITE_APTOS_API_KEY=YOUR_API_KEY
VITE_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> {
}

地址别名中的 _ 是占位符。在发布时,CLI 会将其替换为实际模块发布者地址。

使用项目脚本统一运行编译、测试和发布命令,避免在不同终端中遗漏环境变量。

按以下步骤配置或添加代码:

...
namedAddresses: {
todolist_addr: process.env.VITE_MODULE_PUBLISHER_ACCOUNT_ADDRESS,
},
...

待办事项模块维护任务列表,并将每个任务与创建它的账户关联。下面的函数逐步补全这一状态和公开接口。

  1. 账户创建一个新列表。
  2. 账户在自己的列表中创建任务;每次创建任务时都发出 TaskCreated 事件。
  3. 账户可以将自己的任务标记为已完成。

TodoList 保存任务表和任务计数器;Task 保存任务 ID、创建者地址、内容以及完成状态。

按以下步骤配置或添加代码:

...
/// 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,
}
...

继续添加或验证以下内容:

...
use aptos_std::table::Table;
use std::string::String;
...

继续添加或验证以下内容:

Terminal window
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 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;

继续添加或验证以下内容:

/// 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);
...
}
/// 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;
}

继续添加或验证以下内容:

Terminal window
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 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);
}

继续添加或验证以下内容:

Terminal window
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"
}

配置发布账户和网络后编译并发布模块。发布前检查地址、私钥和 API Key 均来自本地环境变量。

按以下步骤配置或添加代码:

Terminal window
Transaction submitted: https://explorer.aptoslabs.com/txn/0x68dadf24b9ec29b9c32bd78836d20032de615bbef5f10db580228577f7ca945a?network=devnet
Code was successfully deployed to object address 0x2bce4f7bb8a67641875ba5076850d2154eb9621b0c021982bdcd80731279efa6
{
"Result": "Success"
}

以下是前述片段组合后的完整模块,便于对照最终源文件。

按以下步骤配置或添加代码:

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);
}
}