布景
在之前的文章中咱们介绍了怎么在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。