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<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。

猜你喜欢

转载自blog.csdn.net/QStack/article/details/129479006