Aptos DAPP之智慧合約編寫
背景
在之前的文章中我們介紹瞭如何在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
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
error[E03002]: unbound module
┌─ /Users/xilou/blockchain/blog/my-first-dapp/move/sources/todolist.move:4:25
│
4 │ set_task_event: Event::EventHandle
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
... } ```
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
let todo_list = borrow_global_mut
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。