背景
在之前的文章中我们介绍了如何在aptos上编译和发布模块,也就是智能合约,而智能合约发布之后就可以与之交互,而对于一般用户而言,与智能合约的交互就是通过DAPP,接下来几篇文章将会介绍如何从零开始在aptos上构建一个DAPP。
准备工作
- 首先我们需要创建一个目录my-first-dapp,然后进入该目录创建一个move目录用于存放智能合约的代码
- 然后我们在move目录下使用aptos move init --name my_todo_list命令,该命令会创建一个sources目录和Move.tom文件。
什么是Move.toml文件
一个Move.toml文件是一个配置文件,其中包括了一些元数据如名字、版本号和包的依赖,我们使用命令创建的Move.toml内容如下:
[package]
name = 'my_to_list'
version = '1.0.0'
[dependencies.AptosFramework]
git = 'https://github.com/aptos-labs/aptos-core.git'
rev = 'main'
subdir = 'aptos-move/framework/aptos-framework'
我们可以看到包信息和一个AptosFramework的依赖,其中的name属性就是我们使用--name指定的属性,其中的AptosFrame依赖指向github仓库main分支aptos-core/aptos-move/framework/aptos-framework。
sources目录
sources目录是包含一系列.move模块文件的目录,之后我们想要使用命令行编译时编译器会寻找sources目录以及与其相关的Move.toml文件。
创建Move模块
正如上篇文章我们所提到的,当我们发布一个Move模块时我们需要一个账户,所以我们需要创建一个帐户,一旦我们拥有了一个账户的私钥,我们就可以在该账户下创建一个模块,也可以使用该账户发布模块。
在move目录下使用aptos init --network devnet命令,当有提示时直接回车确。这个命令为我们创建了.aptos目录,其中包含了config.yaml文件,这个文件包含了一些描述信息,其中的内容如下:
profiles:
default:
private_key: "0x664449b9aefa4694d6871b0025e84dc173a64c58c5dbf413478e79048bc5f6e9"
public_key: "0xca1b0da9a12a3e51fdab6809e3c4bf2668379bdc62573f80b70da5b5635a0a19"
account: 6f2dea63c25fcfa946dd54d002e11ec0de56fb37b0cb215396dd079872fc49eb
rest_url: "https://fullnode.devnet.aptoslabs.com"
faucet_url: "https://faucet.devnet.aptoslabs.com"
从现在开始,我们在move目录下使用命令行时会自动带上这些默认信息,需要注意的是我们使用的是devnet网络,我们最后也会将我们的包发布到测试网上去。
正如之前所提到的我们的sources目录包含.move的模块文件,所以我们来添加我们第一个Move文件,打开Move.toml文件,在其中添加一下信息,其中的default-profile-account-addres就是我嘛从config.yaml文件中获取的account信息。
[addresses]
todolist_addr='<default-profile-account-address>'
所以我的Move.toml更改后如下:
[addresses]
todolist_addr='6f2dea63c25fcfa946dd54d002e11ec0de56fb37b0cb215396dd079872fc49eb'
然后在sources目录下创建todolist.move文件,其代码内容如下:
module todolist_addr::todolist {
}
一个Move模块需要存储在一个地址上,所以当它发布时可以通过该地址访问该模块,在我们的模块中,账户地址就是todolist_addr,也就是我们之前在Move.toml配置的,todolist是模块名。
合约逻辑
在正式去写代码前我们需要理解我们需要写的智能合约的功能,为易于理解我,我简化了智能合约的逻辑如下:
- 一个账户可以创建一个新的列表
- 一个账户可以在列表上创建一个新的任务,无论谁创建一个新的任务都会提交一个task_created的任务
- 一个账户可以将它们的任务标记为完成
创建一个事件不是必须的,但是如果一个开发者想要监控数据,比如多少用户创建了新的任务,可以使用Aotos_Indexer
我们可以定义一个TodoList结构体,其内容如下:
- task数组
- 一个新的task事件
- 一个task计数器,其用于记录创建的task的数量,我们可以以此区分不同的task。
我们也需要创建一个Task的结构体,其内容如下:
- task ID,从TodoList1的task计数器获取
- address,创建task的账户地址
- content,task的内容
- completed,一个boolean标记任务是否完成
这两个结构体的定义如下:
struct TodoList has key {
tasks: Table<u64, Task>,
set_task_event: event::EventHandle<Task>,
task_counter: u64
}
struct Task has store, drop, copy {
task_id: u64,
address: address,
content: String,
completed: bool
}
我们可以看到TodoList拥有key能力,key能力允许结构体被当作一个存储标识符,换句话说,key能力代表了可以被存储在顶层并且表现的像一个存储空间,在这里我们需要TodoList称为一个资源存储在用户的账户里,当一个结构体拥有key能力,这个结构体就会转化为一个资源(resource),资源是存储在一个账户下面,因此只能被这个账户赋值和获取。
Task则是拥有store,drop和copy的能力。
- store,Task需要能被存储在其他结构体内如TodoList
- copy, 值可以被拷贝
- drop,值可以被丢弃 关于结构体的四种能力更详细的可以看之前Move的相关文章。
我们应编写了需要结构体,现在来尝试编译一下代码,可以在move目录下使用aptos move compile编译代码,可以看到发生了Unbound type错误,错误如下:
error[E03004]: unbound type
┌─ /Users/xilou/blockchain/blog/my-first-dapp/move/sources/todolist.move:3:16
│
3 │ tasks: Table<u64, Task>,
│ ^^^^^ Unbound type 'Table' in current scope
error[E03002]: unbound module
┌─ /Users/xilou/blockchain/blog/my-first-dapp/move/sources/todolist.move:4:25
│
4 │ set_task_event: Event::EventHandle<Task>,
│ ^^^^^ Unbound module alias 'Event'
error[E03004]: unbound type
┌─ /Users/xilou/blockchain/blog/my-first-dapp/move/sources/todolist.move:11:18
│
11 │ content: String,
│ ^^^^^^ Unbound type 'String' in current scope
{
"Error": "Move compilation failed: Compilation error"
}
这是由于我们使用了一下没有import的类型,所以编译器无法获取他们,在模块的顶部加上以下代码
use aptos_framework::event;
use std::string::String;
use aptos_std::table::Table;
然后再编译就可以编译成功,其返回结果如下
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_to_list
{
"Result": [
"6f2dea63c25fcfa946dd54d002e11ec0de56fb37b0cb215396dd079872fc49eb::todolist"
]
}
创建列表
一个账户最先做的事情是创建一个新的列表,创建一个新的列表需要提交一次交易,所以我们需要知道signer,也就是谁提交了交易,其函数定义如下:
public entry fun create_list(account: &signer) {
}
我们来看看其中的关键
- entry,一个entry函数可以被一次交易调用,当我们需要发起一次链上交易时我们就需要调用一个entry函数
- &signer,singer参数是会被Move虚拟机劫持当做签名交易的地址
我们的代码有一个TodoList资源,资源是被存储在一个账户下的,所以其只能被该账户获取和赋值,这意味着我们创建一个TodoList我们需要将其赋值给一个账户,create_list函数需要处理TodoList的创建,其完整代码如下:
public entry fun create_list(account: &signer) {
let task_holer = TodoList {
tasks: table::new(),
set_task_event: account::new_event_handle<Task>(account),
task_count: 0
};
move_to(account, tasks_holder);
}
我们使用了account模块,所以需要使用以下代码添加
use aptos_framework::account;
创建task函数
正如之前所说,我们需要一个创建task的函数,从而能使一个账户创建一个新的task,创建一个task也是需要提交一个交易,所以我们需要知道signer和task的content:
public entry fun create_task(account: &signer, content: String) acquires TodoList {
//获取地址
let signer_address = signer::address_of(account);
//获取TodoList资源
let todo_list = borrow_global_mut<TodoList>(signer_address);
//task计数器计数
let counter = todo_list.task_counter + 1;
//创建一个新的task
let new_task = Task {
task_id: counter,
address: signer_address,
content,
completed: false
};
table::upsert(&mut todo_list.tasks, counter, new_task);
todo_list.task_counter = counter;
event::emit_event<Task>(
&mut borrow_global_mut<TodoList>(signer_address).set_task_event,
new_task,
)
}
由于我们使用了新的模块,我们需要引入signer和table,可以使用以下代码:
use std::signer;
use aptos_std::table::{Self, Table}; // This one we already have, need to modify it
task完成函数
我们还需要一个函数去标记task已经完成
public entry fun complete_task(account: &signer, task_id: u64) acquires TodoList {
// 获取signer地址
let signer_address = signer::address_of(account);
// 获取TodoList资源
let todo_list = borrow_global_mut<TodoList>(signer_address);
// 根据task id获取相应的task
let task_record = table::borrow_mut(&mut todo_list.tasks, task_id);
// 更新任务未已完成
task_record.completed = true;
}
然后我们还可以使用aptos move compile进行编译
增加验证
我们主要的逻辑已经写完了,但是还是希望在创建新task和更新task前加一些验证,从而保证我们的函数能够正常工作。
public entry fun create_task(account: &signer, content: String) acquires TodoList {
// gets the signer address
let signer_address = signer::address_of(account);
// 验证已经创建了一个列表
assert!(exists<TodoList>(signer_address), 1);
...
}
public entry fun complete_task(account: &signer,
task_id: u64) acquires TodoList {
// gets the signer address
let signer_address = signer::address_of(account);
// 验证已经创建了列表
assert!(exists<TodoList>(signer_address), 1);
let todo_list = borrow_global_mut<TodoList>(signer_address);
// 验证task存在
assert!(table::contains(&todo_list.tasks, task_id), 2);
let task_record = table::borrow_mut(&mut todo_list.tasks, task_id);
// 验证task未完成
assert!(task_record.completed == false, 3);
task_record.completed = true;
}
可以看到assert接受两个参数,第一个是检查内容,第二个是错误码,对于错误码我们最好可以提前定义。
const E_NOT_INITIALIZED: u64 = 1;
const ETASK_DOESNT_EXIST: u64 = 2;
const ETASK_IS_COMPLETED: u64 = 3;
添加测试
主要逻辑已经完成,现在需要添加测试,测试函数可以用#[test]标识,在代码最后添加如下代码:
#[test]
public entry fun test_flow() {
}
我们需要完成以下测试
- 创建列表
- 创建任务
- 更新任务已完成
代码如下
#[test(admin = @0x123)]
public entry fun test_flow(admin: signer) acquires TodoList {
account::create_account_for_test(signer::address_of(&admin));
create_list(&admin);
create_task(&admin, string::utf8(b"new task"));
let task_count = event::counter(&borrow_global<TodoList>(signer::address_of(&admin)).set_task_event);
assert!(task == 1, 4);
let todo_list = borrow_global<TodoList>(signer::address_of(&admin));
assert!(todo_list.task_counter == 1, 5);
let task_record = table::borrow(&todo_list.tasks, todo_list.task_count);
assert!(task_record.task_id == 1, 6);
assert!(task_record.completed == false, 7);
assert!(task_record.content == string::utf8(b"new task"), 8);
assert!(task_record.address == signer::address_of(&admin), 9);
complete_task(&admin, 1);
let todo_list = borrow_global<TodoList>(signer::address_of(&admin));
let task_record = table::borrow(&todo_list.tasks, 1);
assert!(task_record.task_id == 1, 10);
assert!(task_record.completed == true, 11);
assert!(task_record.content == string::utf8(b"new task"), 12);
assert!(task_record.address == signer::address_of(&admin), 13);
}
由于我们的测试运行在我们的账户的范围之外,所以需要创建一个测试账户,我是使用了一个admin账户,其地址为@0x123,在正式运行测试之前,我们需要使用以下语句引入模块
use std::string::{Self, String}; // already have it, need to modify
使用aptos move test进行测试,结果如下
INCLUDING DEPENDENCY AptosFramework
INCLUDING DEPENDENCY AptosStdlib
INCLUDING DEPENDENCY MoveStdlib
BUILDING my_to_list
Running Move unit tests
[ PASS ] 0x6f2dea63c25fcfa946dd54d002e11ec0de56fb37b0cb215396dd079872fc49eb::todolist::test_flow
Test result: OK. Total tests: 1; passed: 1; failed: 0
{
"Result": "Success"
}
发布模块
我们在move目录下使用命令aptos move compile编译模块,报错如下
use std::string::{Self, String};
│ ^^^^^^ Unused 'use' of alias 'string'. Consider removing it
那是因为我们在测试模块中使用了string,但是在正式合约代码中未使用,改成如下即可
use std::string::String; // change to this
...
#[test_only]
use std::string; // add this
使用aptos move puhlish发布模块,遇到提示直接回车继续 ,结果如下
{
"Result": {
"transaction_hash": "0x0e443ef21c8b19783c06741eb4a5306f11b1529664cf39e4f86fd6679e658686",
"gas_used": 1675,
"gas_unit_price": 100,
"sender": "6f2dea63c25fcfa946dd54d002e11ec0de56fb37b0cb215396dd079872fc49eb",
"sequence_number": 0,
"success": true,
"timestamp_us": 1678615900086281,
"version": 1605342,
"vm_status": "Executed successfully"
}
}
最后
这篇文章主要讲述了DAPP中智能合约的编写,更多文章可以关注公众号QStack。