布景

在之前的文章中咱们介绍了怎么在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:163 │         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:254 │         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:1811 │         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。