美文网首页
EOS 合约基础教程 - 智能合约简介

EOS 合约基础教程 - 智能合约简介

作者: 金牛茶馆 | 来源:发表于2019-01-21 22:01 被阅读59次

    1 EOS智能合约和以太坊合约的区别

    EOS的智能合约里面有一个action(动作)和transaction(交易)的概念。
    在以太坊中,基本上只有transaction的概念,如果我只要执行一种操作,而且是只读操作,就不需要签名。如果需要划资金,有一些写的操作,那就需要用户用公钥对这个操作去签名,然后pos的一个transaction,这是以太坊的概念。

    对于EOS,它多了一个action的概念,action其实它也是对一个智能合约中的某个函数的调用。transaction是由一个或者多个action组合而成的关系,就是在一个transaction里,可以包含多个action,这样你可以在一个transaction里签一次名,就可以调多个函数,做一组操作。

    2 EOS 合约

    2.1 基础知识

    EOS智能合约通过messages 及 共享内存数据库(比如只要一个合约被包含在transaction的读取域中with an async vibe,它就可以读取另一个合约的数据库)相互通信。异步通信导致的spam问题将由资源限制算法来解决。下面是两个在合约里可定义的通信模型:

    • Inline 内联通信模型,采用请求其他操作的形式,需要作为调用操作的一部分执行。Inline保证执行当前的transaction或unwind;无论成功或失败都不会有通知。Inline 操作的scopes和authorities和原来的transaction一样。
    • Deferred 延迟通信模型,采用发送到对等交易的动作通知的形式, 根据生产者的判断,延迟的操作最多可以安排在稍后的时间运行由区块生产者来安排。无法保证将执行延期操作,结果可能是传递通信结果或者只是超时。延期交易具有发送合约的权限,交易可以取消延期交易。

    2.2 目录结构

    2.2.1 创建智能合约文件

    eoscpp将创造三个智能合约文件,他们是你起步开发的框架。

     eoscpp -n ${contract}
    

    以上将在'./${project}'文件夹下创建一个新项目,包含三个文件:

    ${contract}.abi ${contract}.hpp ${contract}.cpp
    

    2.2.2 HPP

    HPP是包含CPP文件所引用的变量、常量、函数的头文件。

    2.2.3 CPP

    CPP文件是包含合约功能的源文件。

    2.2.4 WAST

    想要部署到EOS.IO区块链上的任何程序都需要先编译成WASM格式。这是区块链接受的唯一格式。
    一旦您完成了CPP文件的开发,您可以用eoscpp工具将它编译成一个文本版本的WASM (.wast) 文件。

    $ eoscpp -o ${contract}.wast ${contract}.cpp
    

    2.2.5 ABI

    Application Binary Interface (ABI)是一个基于JSON的描述文件,是关于转换JSON和二进制格式的用户actions的。ABI还描述了如何将数据库状态和JSON的互相转换。一旦您通过ABI描述了您的合约,开发者和用户就能够用JSON和您的合约无缝交互了。
    ABI文件可通过eoscpp工具从HPP文件生成:

    $ eoscpp -g ${contract}.abi ${contract}.hpp
    

    这里是一个合约的骨架ABI的例子:

    {
      "types": [{
          "new_type_name": "account_name",
          "type": "name"
        }
      ],
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
          "name": "account",
          "base": "",
          "fields": {
            "account": "name",
            "balance": "uint64"
          }
        }
      ],
      "actions": [{
          "action": "transfer",
          "type": "transfer"
        }
      ],
      "tables": [{
          "table": "account",
          "type": "account",
          "index_type": "i64",
          "key_names" : ["account"],
          "key_types" : ["name"]
        }
      ]
    }
    
    

    您肯定注意到了这个ABI 定义了一个叫transfer的action,它的类型也是transfer。这就告诉EOS.IO当${account}->transfer的message发生时,它的payload是transfer类型的。 transfer类型是在structs的列表中定义的,其中有个对象,name属性是transfer。

    ...
      "structs": [{
          "name": "transfer",
          "base": "",
          "fields": {
            "from": "account_name",
            "to": "account_name",
            "quantity": "uint64"
          }
        },{
    ...
    

    这部分包括from, toquantity等字段。这些字段都有对应的类型:account_nameuint64account_name 是一个用base32编码来表示uint64的内置类型。

    {
      "types": [{
          "new_type_name": "account_name",
          "type": "name"
        }
      ],
    ...
    
    

    上述types列表内,我们定义了一系列现有类型的别名。这里,我们把account_name定义为name的别名。

    2.3 合约内容结构

    虽然 EOS 合约是一个 C++ 类,但,一个 EOS 合约又不仅仅是一个普通的 C++ 类,它必须符合一定的条件才能成为合约:

    2.3.1 类的构造方法必须接受且只能接受三个参数

    contractName( name receiver, name code, datastream<const char*> ds );
    

    参数说明

    参数 类型 说明
    receiver eosio::name 合约的名字,准确的说,是部署当前合约的账号
    code eosio::name 调用合约 动作的账号,一般是当前合约所在的账号
    ds datastream<const char*> 保存了那些使用 eosio::ignore 忽略的字段

    注意: ds 参数目前没啥大的作用,但是,如果合约想要对未由调度程序处理的操作字段进行进一步反序列化(例如,要重新使用 ignore 忽略的字段),这会派上用场。

    2.3.2 导出合约中的动作

    任何一个合约,都必须定义一个 EOSIO_DISPATCH( hello, (hi)) 用于导出合约中的动作,这样,EOS 执行环境才知道该合约可以接受哪些动作

    EOSIO_DISPATCH() 是一个宏定义,它的原型如下

    #define EOSIO_DISPATCH( TYPE, MEMBERS ) ...
    

    参数说明

    参数 说明
    TYPE 合约的名字,注意,是合约的名字,而不是合约所在的账号的名字
    MEMBERS 由小括号扩起来的 0 个或多个动作名,比如 (hi) ,每个动作都是一个 C++ public 方法

    2.3.3 范例

    #include <eosiolib/eosio.hpp>
    
    using namespace eosio;
    using namespace std;
    
    class hello
    {
    public:
        hello( name receiver, name code, datastream<const char*> ds )
        {
            print("receiver:");
            print(receiver);
            print("    code:");
            print(code);
            print("    ds length:");
            print(ds.remaining());
    
            if( ds.remaining() > 0 ){
                std::string data;
                ds >> data;
                print("    ds:");
                print(std::string(data));
            }
        }
    
        [[eosio::action]]
        void hi(){
        }
    };
    
    EOSIO_DISPATCH(hello,(hi))
    

    然后使用下面的命令来编译

    eosio-cpp -o hello.wasm hello.cpp --abigen
    

    编译结果如下

    Warning, empty ricardian clause file
    Warning, empty ricardian clause file
    Warning, action <hi> does not have a ricardian contract
    

    上面这几个警告是可以忽略的,因为我们没有给合约和方法添加 李嘉图 (Ricardian) 说明文件

    李嘉图 (Ricardian) 说明文件如何编写,我们会在以后的章节中讲解

    接着使用下面的命令来部署合约

    cleos set contract hello ../hello -p hello
    

    运行结果如下

    Reading WASM from ../hello/hello.wasm...
    Skipping set abi because the new abi is the same as the existing abi
    Publishing contract...
    executed transaction: 683503d8936d27ffc6fdd1b604782757c8bedcae899c327b34475272ade24b00  2808 bytes  588 us
    #         eosio <= eosio::setcode               {"account":"hello","vmtype":0,"vmversion":0,"code":"0061736d0100000001681260017f006000006000017f6002...
    

    2.3.4 合约调用

    2.3.4.1 使用当前合约账号来执行合约,且不传递任何参数

    cleos push action hello hi '[]' -p hello
    executed transaction: 41ad0f8d064a6980151d979666df4103fe0364033fdfee6945cd40b7f39df70b  96 bytes  212 us
    #         hello <= hello::hi                    ""
    >> receiver:hello    code:hello    ds length:0
    

    2.3.4.2 使用当前合约账号来执行合约,且一些参数

    cleos push action hello hi '["hello","ni","hao"]' -p hello
    executed transaction: 4723aa8ec187e47ead46580d415ebe5b17fd0849b234fd54e092e08f088c6534  96 bytes  220 us
    #         hello <= hello::hi                    ""
    >> receiver:hello    code:hello    ds length:0
    

    2.3.4.3 使用其它账号来执行合约,不传递任何参数

    cleos push action hello hi '[]' -p hi
    executed transaction: c0c15799a02387078e7e9c9f0ba09f5987cc7b949b6d44ca4be78a606e58afa2  96 bytes  226 us
    #         hello <= hello::hi                    ""
    >> receiver:hello    code:hello    ds length:0
    

    2.3.4.4 使用其它账号执行合约,传递一些参数

    cleos push action hello hi '["hello","ni","hao"]' -p hi
    executed transaction: 27a9ade415ecb2f49cb82e2fc44e2c36116d4b5a6fa5fc5c1dfed30a5eab698f  96 bytes  242 us
    #         hello <= hello::hi                    ""
    >> receiver:hello    code:hello    ds length:0
    

    从上面的执行结果中可以看出,

    • 如果直接执行合约中的方法,那么 codereceiver 都是一样的,都是当前合约所在的账号
    • datastream<const char*> 一般情况下都是空的,也就是说,没有任何数据

    2.3.5 合约基础类 eosio::contract

    如果我们每写一个合约就要自己动手写一个长长的构造函数,肯定是不开心的,作为程序员,能偷懒就偷懒才是我们的理想。

    为此,我们可能希望把包含了三个参数的构造方法写在一个基础的合约里,比如,我们定义一个名为 contract 的基础合约

    2.3.5.1 basic.cpp

    #include <eosiolib/eosio.hpp>
    
    using namespace eosio;
    using namespace std;
    
    class basic
    {
    public:
        basic( name receiver, name code, datastream<const char*> ds ):
        _self(receiver),_code(code),_ds(ds) {}
    
        name _self;
        name _code;
        datastream<const char*> _ds;
    };
    

    然后其它合约在扩展自这个合约,并使用 using 关键字来引用基础类的构造方法

    2.3.5.2 hello.cpp

    #include <eosiolib/eosio.hpp>
    
    #include "basic.cpp"
    
    using namespace eosio;
    using namespace std;
    
    class hello:public basic {
    public:
        using basic::basic;
    
        [[eosio::action]]
        void hi(){
            print("receiver:");
            print(_self);
            print("    code:");
            print(_code);
            print("    ds length:");
            print(_ds.remaining());
    
            if( _ds.remaining() > 0 ){
                std::string data;
                _ds >> data;
                print("    ds:");
                print(std::string(data));
            }
        }
    };
    
    EOSIO_DISPATCH(hello,(hi))
    

    然后编译、部署、执行合约,会得到相同的结果

    cleos push action hello hi '["hello","ni","hao"]' -p hi
    executed transaction: 040514a09eb7fe268d5ce7d23a38080505ab73cdf72e3bdf47eff7228f2e1747  96 bytes  225 us
    #         hello <= hello::hi                    ""
    >> receiver:hello    code:hello    ds length:0
    

    EOS 官方也考虑到了这一点,创建了一个合约基础类 contract,并放在命名空间 eosio 下,头文件为 <eosiolib/contract.hpp>

    该文件在 Github 上的地址为 https://github.com/EOSIO/eosio.cdt/blob/master/libraries/eosiolib/contract.hpp

    同时,#include <eosiolib/eosio.hpp> 头文件也默认包含了该头文件,因此,只要包含了 #include <eosiolib/eosio.hpp> 就可以了

    我们把上面的范例改改,改成官方的合约基础类

    #include <eosiolib/eosio.hpp>
    
    using namespace eosio;
    using namespace std;
    
    class hello:public eosio::contract {
    public:
        using eosio::contract::contract;
    
        [[eosio::action]]
        void hi(){
            print("receiver:");
            print(_self);
            print("    code:");
            print(_code);
            print("    ds length:");
            print(_ds.remaining());
    
            if( _ds.remaining() > 0 ){
                std::string data;
                _ds >> data;
                print("    ds:");
                print(std::string(data));
            }
        }
    };
    
    EOSIO_DISPATCH(hello,(hi))
    

    3 EOS 动作action

    动作 ( action ) 是 EOS 合约基础组成单位。一个动作,在 C++ 合约类中,表示如下

    • 一个动作必须是 C++ 合约类的成员方法
    • 成为动作的成员方法,必须使用 [[eosio::action]] C++11 特性修饰,否则就是一个普通的类成员函数
    • 成为动作的成员方法,访问级别必须是公开的 public
    • 成为动作的成员方法,必须没有任何返回值,也不能返回任何值,也就是说,必须使用 void 作为返回值
    • 成为动作的成员方法,可以接受任意数量的参数
    • 成为动作的成员方法,必须在 EOSIO_DISPATCH 中导出

    一个 EOS 链是由多个块 ( block ) 组成的:


    image.png

    每个块中包含多笔交易:


    image.png

    交易由action组成:


    image.png

    4 调试智能合约

    现在user官方网站推荐的一个调试方法就是print,把信息打印出来。这个必须要我们搭建本地节点,因为如果没有本地节点,相当于你print打印在别人的节点上,你根本看不到这个打印信息是什么,所以说你必须要搭建一个本地节点。搭建本地节点后,你运行智能合约,就会看到print出来的输出结果。

    4.1 debug.hpp

    #include <eoslib/eos.hpp>
    #include <eoslib/db.hpp>
    
    namespace debug {
        struct foo {
            account_name from;
            account_name to;
            uint64_t amount;
            void print() const {
                eosio::print("Foo from ", eosio::name(from), " to ",eosio::name(to), " with amount ", amount, "\n");
            }
        };
    }
    

    4.2 debug.cpp

    #include <debug.hpp>
    
    extern "C" {
    
        void apply( uint64_t receiver, uint64_t code, uint64_t action ) {
            if (code == N(debug)) {
                eosio::print("Code is debug\n");
                if (action == N(foo)) {
                     eosio::print("Action is foo\n");
                    debug::foo f = eosio::unpack_action_data<debug::foo>();
                   if (f.amount >= 100) {
                        eosio::print("Amount is larger or equal than 100\n");
                    } else {
                        eosio::print("Amount is smaller than 100\n");
                        eosio::print("Increase amount by 10\n");
                        f.amount += 10;
                        eosio::print(f);
                    }
                }
            }
        }
    } // extern "C"
    

    4.3 编译运行

    $ eosiocpp -o debug.wast debug.cpp
    $ cleos set contract debug debug.wast debug.abi
    

    实战

    部署智能合约

    $ cleos set contract eosio build/contracts/eosio.bios -p eosio
    
    • eosio是要部署的账号,就是你用哪个账号去部署智能合约;
    • build/contracts/eosio.bios表示的是路径;
    • eos.bios是生成一个智能合约的目录。

    运行Token合约

    相关文章

      网友评论

          本文标题:EOS 合约基础教程 - 智能合约简介

          本文链接:https://www.haomeiwen.com/subject/crotdqtx.html