Aptos DAPP之智能合約編寫

語言: CN / TW / HK

背景

在之前的文章中我們介紹瞭如何在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, set_task_event: event::EventHandle, 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, │ ^^^^^ 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, │ ^^^^^ 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(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(signer_address), 1);

let todo_list = borrow_global_mut(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。