你的第一个EOS dApp — 合约篇

在先前的文章中,我们创建了独立的EOS区块链,钱包以及账户。 在这篇文章中,我们将用代码实现合约,然后我们将会使用这个合约来运行学校入学抽签。 我们将会学到EOS的核心概念,包括:

  • 怎样创建一个EOS智能合约

  • web assembly 是什么,在EOS中有什么用处

  • 怎样部署EOS合约并与之交互

环境配置

我建议你使用一个IDE例如Visual Studio Code,但是你当然也可以使用任何编辑器。让我们开始吧,首先我们创建一下的文件和项目目录

mkdir lottery && cd lottery
mkdir contracts && cd contracts
mkdir lottery && cd lottery

代码

在路径 lottery/contracts/lottery/Lottery.cpp下创建Lottory.cpp文件

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>
#include <string>

namespace CipherZ {
    using namespace eosio;
    using std::string;

    class Lottery : public contract {
        using contract::contract;
        
        public:
            Lottery(account_name self):contract(self) {}

            //@abi action
            void addstudent(const account_name account, uint64_t ssn, string firstname, string lastname, uint64_t grade) {
            require_auth(account);
            
            // Grade logic
            gradeIndex grades(_self, _self);
            auto grade_iter = grades.find(grade);
            eosio_assert(grade_iter != grades.end(), "Grade must exist before adding a student");

            // Student insertion
            studentMultiIndex students(_self, _self);
            auto student_iter = students.find(ssn);
            eosio_assert(student_iter == students.end(), "student already exists");
            
            auto parent_index = students.template get_index<N(byparent)>();
            auto parent_iter = parent_index.find(account);
            eosio_assert(parent_iter == parent_index.end(), "A student has already been entered by this parent");


            students.emplace(account, [&](auto& student) {
                student.account_name = account;
                student.ssn = ssn;
                student.firstname = firstname;
                student.lastname = lastname;
                student.grade = grade;
            });

            grades.modify(grade_iter, account, [&](auto& grade) {
                grade.applicants = grade.applicants + 1;
            });

            }

            //@abi action
            void addgrade(const account_name account, uint64_t grade_num, uint64_t openings) {
                require_auth(account);
                gradeIndex grades(_self, _self);
                auto iterator = grades.find(grade_num);
                eosio_assert(iterator == grades.end(), "grade already exists");
                grades.emplace(account, [&](auto& _grade) {
                    _grade.account_name = account;
                    _grade.openings = openings;
                    _grade.grade_num = grade_num;
                });
            }

            //@abi action
            void getstudent(const account_name account, const uint64_t ssn) {
                require_auth(account);
                studentMultiIndex students(_self, _self);
                auto iterator = students.find(ssn);
                eosio_assert(iterator != students.end(), "student not found");
                auto student = students.get(ssn);
                eosio_assert(student.account_name == account, "only parent can view student");
                print(" **SSN: ", student.ssn, 
                    " First Name: ", student.firstname.c_str(), 
                    " Last Name: ", student.lastname.c_str(),
                    " Grade: ", student.grade,
                    " Result: ", student.result, "** ");
            }

            //@abi action
            void remstudent(const account_name account, const uint64_t ssn) {
                require_auth(account);
                studentMultiIndex students(_self, _self);
                auto iterator = students.find(ssn);
                eosio_assert(iterator != students.end(), "student not found");
                auto student = (*iterator);
                eosio_assert(student.account_name == account, "only parent can remove student");
                students.erase(iterator);
            }

            //@abi action
            void remgrade(const account_name account, const uint64_t grade_num) {
                require_auth(account);
                gradeIndex grades(_self, _self);
                auto iterator = grades.find(grade_num);
                eosio_assert(iterator != grades.end(), "grade not found");
                auto grade = (*iterator);
                eosio_assert(grade.account_name == account, "only supervisor can remove grade");
                grades.erase(iterator);
            }

            //@abi action
            void getgrade(const account_name account, uint64_t grade_num) {
                require_auth(account);
                gradeIndex grades(_self, _self);
                auto iterator = grades.find(grade_num);
                eosio_assert(iterator != grades.end(), "grade does not exist");
                auto current_grade = (*iterator);
                print(" **Grade: ", current_grade.grade_num,
                        " Account: ", current_grade.account_name, 
                        " Openings: ", current_grade.openings,
                        " Applicants: ", current_grade.applicants, "** ");
            }

            //@abi action
            void getstudents(const account_name account) {
                require_auth(account);
                studentMultiIndex students(_self, _self);
                auto iterator = students.begin();
                eosio_assert(iterator != students.end(), "no students exists");
                while (iterator != students.end()) {
                    auto student = (*iterator);
                    print(" First Name: ", student.firstname.c_str(), 
                        " Last Name: ", student.lastname.c_str(),
                        " Grade: ", student.grade,
                        " Result: ", student.result, "** ");
                    iterator++;
                }
            }

            //@abi action
            void getgrades(const account_name account) {
                require_auth(account);
                gradeIndex grades(_self, _self);
                auto iterator = grades.begin();
                eosio_assert(iterator != grades.end(), "no grades exists");
                while (iterator != grades.end()) {
                    auto current_grade = (*iterator);
                    print(" **Grade: ", current_grade.grade_num, 
                        " Openings: ", current_grade.openings,
                        " Applicants: ", current_grade.applicants, "** ");
                    iterator++;
                }
            }

            //@abi action
            void runlottery(account_name account) {
                require_auth(account);
                studentMultiIndex students(_self, _self);
                auto student_index = students.template get_index<N(bygrade)>();
                gradeIndex grades(_self, _self);
                auto grade_iter = grades.begin();
                while(grade_iter != grades.end()) {
                    auto current_grade = (*grade_iter).grade_num;
                    auto student_iter = student_index.find(current_grade);
                    uint64_t result_index = 1;
                    while (student_iter != student_index.end()) {
                        auto current_student = (*student_iter);
                        if(current_student.grade == current_grade) {
                            student_index.modify(student_iter, account, [&](auto& student) {
                            student.result = result_index;
                        });
                        result_index++;
                        } 
                       student_iter++;
                    }
                 grade_iter++;
                }
            }


        private:

            //@abi table student i64
            struct student {
                uint64_t account_name;
                uint64_t ssn;
                string firstname;
                string lastname;
                uint64_t grade;
                uint64_t result;

                uint64_t primary_key() const { return ssn; }
                uint64_t grade_key() const { return grade; }
                uint64_t parent_key() const { return account_name; }

                EOSLIB_SERIALIZE(student, (account_name)(ssn)(firstname)(lastname)(grade)(result));
            };

            typedef multi_index<N(student), student, 
            indexed_by<N(bygrade), const_mem_fun<student, uint64_t, &student::grade_key>>,
            indexed_by<N(byparent), const_mem_fun<student, uint64_t, &student::parent_key>>> studentMultiIndex;
    
            //@abi table grade i64
            struct grade {
                uint64_t account_name;
                uint64_t grade_num;
                uint64_t openings;
                uint64_t applicants;

                uint64_t primary_key() const { return grade_num; }

                EOSLIB_SERIALIZE(grade, (account_name)(grade_num)(openings)(applicants))
            };

            //typedef multi_index<N(student), student> studentIndex;
            typedef multi_index<N(grade), grade> gradeIndex;
    };

    EOSIO_ABI(Lottery, (addstudent)(addgrade)(getstudents)(getgrades)(getstudent)(getgrade)(runlottery)(remstudent)(remgrade))
}

编译

EOS 选择使用Web Assembly作为自己的运行环境,并将自己的合约在该运行环境中执行。

Web Assembly 是一种新兴的产业标准,是Microsoft, Google和Apple等公司的合作产物。这个标准的目的是使在网络浏览器中运行不被信任的高性能代码成为可能。 Web Assembly 是一个规则改变者,它将使视频,图片编辑和游戏等高性能网络应用成为可能。 -Dan Larimer

现在我们使用eosiocpp从.cpp文件中生成Web Assembly(.wast)文件,eosiocpp在我们前面的文章中已经安装过了。注意你需要改变你的路径,如果你的当前目录不是 lottery/contracts/lottery的话。

eosiocpp -o Lottery.wast Lottery.cpp

ABI被普遍使用于大多数的区块链项目中。本质上来说,它是一个描述可被caller获得的智能合约的action和attribute的接口。注意Lottery.cpp中//@abi 语法告诉eosiocpp 编译器什么成员将会用什么方法被暴露。

eosiocpp -g Lottery.abi Lottery.cpp

部署

如果你的EOS区块链还未启动的话,你首先需要启动它,你可能也需要先解锁你的钱包。

启动区块链

首先你需要在EOS的安装目录中运行一下的命令。

注意我们现在在dawn-v4.1.0版本中运行,我们有了一个额外的参数允许我们使用print()方法和并且history plugin 已经改名为history_api_plugin

./build/programs/nodeos/nodeos -e -p eosio --plugin eosio::wallet_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin --contracts-console

解锁钱包

你需要使用一下命令来解锁你的钱包,输入你的密码,这个密码是你在最初建立lottery钱包的时候设置的。

cleos wallet unlock -n lottery

在完成前面文章中的任务后,你的lottery.code 账户应该还在。但是如果你已经升级了你的区块链并丢失了数据,那么你可以用这个命令来重新创建它。

cleos create account eosio lottery.code <owner public key> <active public key>

部署合约

注意我们现在有了.wast.abi文件,运行中的区块链和已经解锁了的钱包,我们可以部署合约了。注意一下的命令要在lottery项目根目录中运行。

cleos set contract lottery.code ./contracts/lottery ./contracts/lottery/Lottery.wast ./contracts/lottery/Lottery.abi

测试账户

我们需要额外创建两个账户来完整运行合约

  • admin —这个账户使为了管理抽签的学校管理员创建的
  • parent—这个账户给一对父母的,他们想要他们的孩子参加抽签

首先请过一遍创建账户的流程或者查看一下的创建账户的命令。

对于每个账户:

//生成owner的公钥和私钥
cleos create key
//导入owner的私钥到钱包中
cleos wallet import <owner private key hash> -n lottery
//生成active 公钥/私钥
cleos create key
//导入 active 私钥到钱包中
cleos wallet import <active private key hash> -n lottery
//创建账户
cleos create account eosio <account name> <owner public key> <active public key>

只要你创建了admin和parent账号,就能够运行这篇文章中的所有测试脚本了。

与合约进行交互

现在我们的合约已经部署在区块链上了,我们可以开始与它进行交互了。现在我们有三中面向用户的函数:

  • Add Grade —我们需要添加年级并开放它们(这个功能开放给管理员)
  • Remove Grade —我们需要删除年级(这个功能只开放给管理员)
  • Get Grade —获得年级以及开放抽签的年级(这个功能开放给所有的账户)
  • Add Student —我们需要能够允许父母输入他们孩子信息的方法,并让孩子参加抽签。每个账户都应该具有这个功能,但是我们将创建一个parent账户来测试这个功能。现在一对父母只被允许输入一个孩子的信息。
  • Remove student — 输入孩子信息的父母应当能够从抽签中删除该孩子的信息
  • Get student — 输入孩子信息的父母和管理员应当能够查看孩子的抽签状态
  • Run Lottery — 管理员账户应当能够为所有的年级抽签

测试脚本

为了更有效率,这个test脚本将会被运行来证明前面要求的功能都实现了

否认声明:我从没有能够探索智能合约的权限管理,现在用到的方法要求caller都是授权的账户,他们拥有他们自己插入的数据。如果谁能够帮我指出某个锁定智能合约的特定action的方法的话,那真是太有帮助了。我能想到的唯一一个方法就是为每个用户都维持一个权限的表?

-- Tests --

cleos push action lottery.code remgrade '["admin",1]' -p admin@active 
// Error - 年级不存在 

cleos push action lottery.code addstudent '["parent",123456789, "jimmy", "stewart", 1]' -p parent@active
// Error - 学生的年级必须在学生之前存在

cleos push action lottery.code addgrade '["admin",1, 30]' -p admin@active 
// Success

cleos push action lottery.code remstudent '["parent",123456789]' -p parent@active 
// Error - 学生不存在

cleos push action lottery.code addstudent '["parent",123456789, "jimmy", "stewart", 1]' -p parent@active
// Success

cleos push action lottery.code remstudent '["admin",123456789]' -p admin@active 
// Error - 学生只能被输入该学生信息的账户获取

cleos push action lottery.code getstudent '["admin",123456789]' -p admin@active
// Error - 学生只能被输入该学生信息的账户获取

cleos push action lottery.code getstudent '["parent",123456789]' -p parent@active
// Success

cleos push action lottery.code getstudents '["parent"]' -p parent@active
// Success - 所有授权账户都可以获取 - 去除了个人信息 - 将来会进行权限管理

cleos push action lottery.code getstudents '["admin"]' -p admin@active
// Success - 所有授权账户都可以获取 - 去除了个人信息 - 将来会进行权限管理

cleos push action lottery.code getgrade '["parent",1]' -p parent@active
// Success - 所有授权账户都可以获取

cleos push action lottery.code getgrade '["admin", 1]' -p admin@active
// Success - 所有授权账户都可以获取

cleos push action lottery.code getgrades '["parent"]' -p parent@active
// Success - 所有授权账户都可以获取

cleos push action lottery.code getgrades '["admin"]' -p admin@active
// Success - 所有授权账户都可以获取

cleos push action lottery.code runlottery '["admin"]' -p admin@active
// Success - 所有授权账户都可以获取 - 将来会进行权限管理

cleos push action lottery.code getstudent '["parent",123456789]' -p parent@active
// Success - 当前使用的是随机采样的先入先出队列 - 将来会使用像筛子一样的合约来进行随机 

cleos push action lottery.code remstudent '["parent",123456789]' -p parent@active 
// Success

cleos push action lottery.code remgrade '["admin",1]' -p admin@active 
//Success

cleos push action lottery.code getstudent '["parent",123456789]' -p parent@active
// Error - 没有找到该学生

cleos push action lottery.code getgrade '["admin", 1]' -p admin@active
// Error - 没有找到该年级

上面的这些测试现在并不是理想的测试,但是实际上在开发者环境下没有任何一个测试是理想的。这个方法与当前的开发者环境相比已经落后了几年了,但是在下一篇文章中,我将会探索着使用Tokenika’s product, EOS Factory 来展现一个更真实的开发流程。这么说吧,使用一个困难的方法来对EOS CLI tools做一些基础理解并没有什么错。

排除故障

在本地运行EOS区块链是一个有挑战性的工作,你可能会遇到一些无效的状态。每天早上我都需要重置我的区块链,因为交易已经失效了而且变得不稳定

rm -rf "~/Library/Application Support/eosio/nodeos/data"
<eos dir>/build/programs/nodeos/nodeos -e -p eosio --plugin eosio::wallet_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin --contracts-console