合约编写基础知识介绍基础篇

本文档介绍了合约编写的基础知识,包括合约初始化、action和权限的相关知识。适用于想要了解智能合约编写基础知识的初学者和开发者,帮助其快速了解和上手EOS智能合约的编写。作为智能合约的基础篇,本文仅涉及合约初始化、action和权限方面的内容。

01

智能合约介绍

区块链作为一种分布式可信计算平台,去中心化是其最本质的特征。每笔交易的记录不可篡改地存储在区块链上。智能合约中定义可以在区块链上执行的动作action和交易transaction的代码。可以在区块链上执行,并将合约执行状态作为该区块链实例不可变历史的一部分。

因此,开发人员可以依赖该区块链作为可信计算环境,其中智能合约的输入、执行和结果都是独立的,不受外部影响。

02

合约编写基础知识介绍

(一)合约初始化

1、合约的构造函数

在EOSIO中,智能合约通过C++编写,并通过WASM(WebAssembly)字节码形式部署到区块链网络上。当部署完成后,可以调用合约的action执行相应的操作。在合约被部署后,会自动执行其初始化函数,以初始化合约的数据和状态。

智能合约的构造函数是在合约部署时被调用的,用于初始化合约的状态和数据。构造函数是一个特殊的成员函数,它没有返回值类型,且函数名与合约名相同。

以下是一个简单的EOS智能合约的构造函数示例:

#include <eosio/eosio.hpp>

using namespace eosio;

class [[eosio::contract("hello")]] hello : public contract {
public:
   hello(name receiver, name code, datastream<const char*> ds)
      : contract(receiver, code, ds) {
        eosio::print_f("hello is ready. ");
      }
};

在上述代码中,构造函数的参数包括:

  • receiver:合约实例的接收者,指定该实例的账户名。

  • code:合约所属的账户名。

  • ds:数据流对象,用于序列化和反序列化合约数据。通常在构造函数中会使用它来初始化合约的状态。

构造函数必须继承自contract类,并调用contract类的构造函数来初始化合约状态和数据。

在构造函数中,你可以执行各种初始化操作,例如分配初始资源,初始化数据结构,加载配置等等。

需要注意的是,constructor函数只会在合约部署时执行一次,之后无法再次执行。因此,合约构造函数对于初始化合约状态以及设置合约的权限和授权等功能至关重要。如果需要修改合约的数据和状态,需要通过action来调用其他函数。

2、ds

ds的全称为datastream,是一个用于读写字节形式的数据流。数据流对象是EOS智能合约中的一个重要对象,用于序列化和反序列化合约数据。在EOS中,合约与区块链节点通信时需要将数据序列化为二进制格式,同时在合约内部也需要将二进制格式的数据反序列化为对应的数据类型进行处理。ds可以帮助合约开发人员方便地实现这些数据序列化和反序列化操作。

(1)ds的使用场景

  • 作为智能合约构造函数的参数

在智能合约中,ds用于序列化和反序列化合约数据。通常在构造函数中会使用它来初始化合约的状态。

  • 在智能合约中读取和写入参数

在智能合约中,通常需要读取和写入一些参数,例如在调用一个合约的action时需要传入一些参数,或者在向表中添加数据时需要指定一些字段值。对于这些参数,可以使用ds对它们进行序列化和反序列化,以便在智能合约中进行处理。

  • 与其他合约进行通信

在EOSIO中,多个合约可以相互调用和通信。当需要将数据传递给其他合约时,可以使用ds将数据进行序列化,以便在不同合约之间传递二进制数据流。

  • 在智能合约中处理复杂的数据类型

在智能合约中,您可以自定义一些复杂的数据类型,例如结构体、对象等等。当需要在智能合约中处理这些复杂的数据类型时,可以使用ds对它们进行序列化和反序列化,以便在智能合约中进行处理。

(2)使用ds的好处

  • 易于序列化和反序列化:使用ds对象,可以方便地将各种数据类型序列化为二进制格式,或者将二进制格式反序列化为对应的数据类型。这对于编写合约代码以及与区块链节点通信非常重要。

  • 减少空间开销:使用ds可以减少数据在内存中的占用空间,从而节省资源。序列化后的二进制数据通常比原始数据占用更少的空间,并且也更容易在网络上传输。

  • 提高效率:使用ds可以提高代码执行效率。序列化和反序列化操作通常需要大量的计算资源和时间,但ds可以提供高效的数据流处理功能,从而提高代码的执行效率。

(3)ignore关键字

ignore类型指令告诉数据流忽略某个类型,但是让ABI生成器添加正确的类型信息。当前非ignore类型不能紧随ignore类型在方法定义中出现,例如void foo(float, ignore)是允许的,而void foo(float, ignore, int) 不允许。

在EOSIO智能合约中,使用ignore关键字可以标记不需要使用的参数。在函数参数中使用ignore关键字可以告诉虚拟机不要解析这个参数,而是让我们手动解析这个参数。

当ACTION函数执行过程中需要使用被ignore忽略的字段时,可以使用预定义的数据流对象_ds进行进一步的反序列化操作。_ds对象可以帮助我们对操作数据进行进一步的反序列化和处理,尤其是对被ignore忽略的字段进行处理。例如:

#include <eosio/eosio.hpp>
#include <eosio/ignore.hpp>

using namespace eosio;

struct person
{
  name key;
  std::string first_name;
  std::string last_name;
  uint64_t age;
  std::string street;
  std::string city;
  std::string state;
};

class [[eosio::contract("addressbook")]] addressbook : public eosio::contract
{
public:
  addressbook(name receiver, name code, datastream<const char *> ds)
      : contract(receiver, code, ds) {}

[[eosio::action]] 
  void test( name user, ignore<uint64_t>, ignore<person>) {
     print( "Hello", user );

     // 读取 ignore 数据。
     uint64_t id;
     person p;
     _ds >> id >> p;
     print(id);
     print("##")
     print(p.city,"##",p.street);
   }
};

使用以下命令调用test动作:

cleos push action addressbook test '["alice",5,{"key":"alice","first_name":"alice","last_name":"final","age":32,"street":"Beijin","city":"heping","state":"amsterdam"}]' -p alice@active

运行结果如下:

executed transaction: c4195834f803c38964b00e0c0baf7ee6fd15b2ca17df7782641c8ec4f81dc70d  160 bytes  260 us
#   addressbook <= addressbook::test            "0000000000855c3405000000000000000000000000855c3405616c6963650566696e616c2000000000000000064265696a6...
>> Helloalice5##heping##Beijin

该示例中,通过_ds数据流对象,从输入数据流中反序列化了一个uint64_t类型的变量id,以及一个person类型的变量p。这些数据本来被标记为ignore,但通过对ignore数据进行反序列化,可以将这些数据存储到相应的变量中,并在ACTION函数中进一步处理这些数据。

需要注意的是,当我们使用ignore关键字时,虚拟机并不会跳过这个字段的解析。相反,虚拟机会将这个字段解析为一个空值,并将其留在数据流中。如果我们需要使用这个字段,我们可以使用_ds对象来手动解析这个字段。

(二)action

一个关于action的打包表示方式,同时还包含了有关授权级别的元数据信息。在EOSIO中,当一个action被发送时,它将被打包成二进制格式,并在网络上进行传输。这个二进制格式包含了action的操作名称、操作数据以及授权级别等信息,以便EOSIO网络节点能够正确地解析和执行该 action。元数据信息包括了关于执行该action所需的授权级别、签名以及其他验证信息。

1、action示例

#include <eosio/eosio.hpp>
class [[eosio::contract]] hello : public eosio::contract {
  public:
      using eosio::contract::contract;
      // 定义一个名为hi的action,需要传入用户名
      [[eosio::action]] void hi( eosio::name user ) {
         // 打印"Hello,用户名"
         print( "Hello, ", user);
      }
};

这里类名和合约账户名没有关系,但是为了方便管理,合约文件名、类名以及合约账户最好一致。合约定义了一个名为"hi"的action,它接受一个"name" ,里面只有一句打印的语句,任何用户都可以调用该action,相应的合约会打印Hello,用户名作为回应。一个合约里面可以定义多个action,而且这些action还可以互相调用。

注意:

  • 一个action必须是C++合约类的成员方法

  • 使用[[eosio::action]]来标识这是一个action,否则就是一个普通的类成员函数

  • 访问级别必须是公开的public

  • 返回值必须是空值

  • 可以接受任意数量的输入参数

2、action_wrapper

action_wrapper是action的包装器,方便自身或者其他合约调用声明的action。

(1)如何使用action_wrapper

在名为hello.hpp的文件中,有一个名为hi的操作。

#include <eosio/eosio.hpp>

class [[eosio::contract("hello")]] hello : public contract {
 public:
  using eosio::contract::contract;
  [[eosio::action]] void hi( eosio::name user ) ;
  [[eosio::action]] void sayhi( eosio::name user ); 
};

为了定义hi动作的Action Wrapper,可以使用eosio::action_wrapper模板。模板的第一个参数为eosio::name类型的动作名称,第二个参数为指向动作方法的引用。

using hi_action = eosio::action_wrapper<"hi"_n, &hello::hi>;

要使用action wrapper,需要包含定义action wrapper的头文件。在上面的例子中,可以将以下代码添加到合约的头文件中:

#include <hello.hpp>

然后,实例化上述定义的hi_action,将要发送该action的合约作为第一个参数指定。在这种情况下,假定合约部署到了hello账户,然后通过调用get_self()方法获取自身账户,最后指定active权限(你可以根据需求修改这两个参数)来定义一个包含两个参数的结构体。

hi_action wrapper{"hello"_n,{get_self(), "active"_n}};

最后,调用操作包装器的send方法,并将hi操作的参数作为位置参数传递进去。

wrapper.send(user);

完整代码如下:

hello.hpp​​​​​​​

#include <eosio/eosio.hpp>

class [[eosio::contract("hello")]] hello : public contract {
 public:
  using eosio::contract::contract;
  [[eosio::action]] void hi( eosio::name user ) ;
  [[eosio::action]] void sayhi( eosio::name user ); 
};
using hi_action = eosio::action_wrapper<"hi"_n, &hello::hi>;

hello.cpp​​​​​​​

#include "hello.hpp"

[[eosio::action]] 
void hello::hi(name user){
   require_auth(user);
   print("Hello, ", name{user});
}

[[eosio::action]] 
void hello::sayhi(name user){
  hi_action wrapper{"hello"_n,{get_self(), "active"_n}};
  wrapper.send(user);
  print("action_wrapper");
}

在该示例中,定义了一个名为hi_action的action_wrapper对象,它将hi操作封装起来。在sayhi操作中,创建了一个hi_action对象wrapper,并将其发送给指定的用户。需要注意的是,在调用send函数时需要指定调用方的授权信息,可以通过eosio::permission_level类来进行指定。在这个例子中,我们使用了合约账户的active权限进行授权。

(2)action_wrapper的send和普通action的send有什么区别

当使用action_wrapper时,参数在编译时被检查,这可以帮助避免运行时可能出现的潜在错误。而使用action方法直接发送操作时,没有这样的编译时检查。

使用action方法不需要在合约中包含目标合约的头文件,但是在使用action_wrapper声明操作时,需要在合约中包含目标合约的头文件或声明操作的函数签名。

尽管更加推荐使用action_wrapper,但如果您确切知道您在做什么,使用action方法也不会出现问题。

(3) 使用action_wrapper的好处

  • 方便操作调用:使用action_wrapper可以方便地调用操作。通过封装操作成一个对象,可以直接调用封装后的对象,而不必每次都手动指定操作的名称、所属合约等参数,从而提高操作调用的方便性。

  • 提高安全性:使用action_wrapper可以提高合约的安全性。在调用操作时,需要指定操作的名称、所属合约等参数,如果这些参数不正确,就有可能导致操作执行失败或者执行了不正确的操作。通过使用action_wrapper,可以避免手动指定这些参数,从而减少出错的可能性。

  • 提高效率:使用action_wrapper可以提高代码的执行效率。在调用操作时,如果需要反复指定操作的名称、所属合约等参数,就会增加代码的执行时间。通过使用action_wrapper,可以避免这种额外的开销,从而提高代码的执行效率。

(三)权限

对于一些特定的action,不希望他人也可以操作,这时候就需要加入权限的校验。

1、函数说明

(1)require_auth函数​​​​​​​

void require_auth(
    capi_name name
)

验证指定账户是否与调用动作的账户相符,不相符则直接报错失败。

参数说明:

  • name-需要被验证的账户名

示例:​​​​​​​

[[eosio::action]] void hi(eosio::name user ) {
   require_auth( user );
   print( "Hello, ", name{user} );
}

该示例实现了一个名为hi的action,要求只有传递进来的参数user才能执行该action。在这个动作函数中,使用require_auth函数来检查当前执行该action的账户是否有user的授权,如果没有授权则会触发一个默认的授权错误信息,阻止该action执行。如果授权成功,则会输出一条以Hello,为开头,user为结尾的消息。

(2)require_auth2函数

void eosio::require_auth2(capi_name name, capi_name permission)

验证指定账户和权限是否与调用动作的账户和权限相符,不相符则直接报错失败。

参数说明:

  • name-需要被验证的账户名

  • permission-需要被验证的权限级别

示例:​​​​​​​

#include <capi/eosio/action.h>

[[eosio::action]]void hi( eosio::name user ) {
   require_auth2(user.value, "active"_n.value);
   print( "Hello, ", name{user} );
}

这段代码是一个EOSIO智能合约的动作(action)函数,用于向指定用户发送一条问候消息。在这个动作函数中,使用了require_auth2函数来确保只有拥有active权限的指定用户才能调用该动作函数。

如果在调用这个动作函数的时候,提供的用户权限不符合要求,那么将会抛出异常并导致动作函数执行失败。因为这个函数中没有提供自定义的错误信息,因此当授权失败时,错误信息将会是默认的授权错误信息。

(3)has_auth函数​​​​​​​

bool eosio::has_auth(
    name n
)

验证指定账户是否与调用动作的账户相符

参数说明:

  • n-需要被验证的账户名

示例:​​​​​​​

[[eosio::action]] void hi( eosio::name user ) {
   if(has_auth( user )){
      print("Hello, ", eosio::name{user} );
      }else{
      print("This is not ",eosio::name{user} );
   }
}

该示例定义了一个hi的action,接收一个eosio::name类型的参数user。在执行这个action的时候,它会检查当前执行者是否拥有传入的user账户的权限,如果有,它会输出"Hello, "后面跟上user账户的名字,否则输出"This is not "后面跟上user账户的名字。这个操作可以确保只有传入的user账户可以执行这个action,而不受执行者所使用的权限(如owner、active、code)的影响。另外,这个action的错误信息是自定义的,可以提供更好的用户体验。

(4)check函数​​​​​​​

void eosio::check(
    bool pred,
    const char * msg
)

断言,如果pred为假,则使用提供的消息进行反馈。

示例:​​​​​​​

#include <capi/eosio/action.h>

void hi( name user ) {
   check(has_auth(user), "User is not authorized to perform this action.");
   print( "Hello, ", name{user} );
}

该示例定义了一个hi的action,接收一个eosio::name类型的参数user,使用has_auth函数来检查账户是否被授权执行此动作,如果没有授权,则使用check函数抛出一个自定义错误信息"User is not authorized to perform this action."。在授权检查通过后,程序会输出"Hello, " 后接收到的账户名。

注意:

只有被指定为参数的账户可以执行此动作,不论该账户使用哪个权限签署交易。

(5)is_account函数​​​​​​​

bool eosio::is_account(
    name n
)

检查账户是否存在。

参数说明:

  • n-需要验证的账户名

示例:​​​​​​​

#include <capi/eosio/action.h>

void hi( name user ) {
   check(is_account(user), "The provided name is not an existing account");
   print( "Hello, ", name{user} );

该示例通过is_account函数来检查账户是否存在。如果传入的账户名不存在,那么就会抛出一个异常并在错误信息中包含相应的提示。

2、require_auth、require_auth2和has_auth的区别

has_auth、require_auth和 require_auth2都是EOSIO中用于权限检查的函数。

  • has_auth:has_auth用于验证指定账户是否与调用动作的账户相符。如果指定账户与调用动作的账户相符,则返回true,否则返回false,这个函数不会抛出异常。

  • require_auth:指定账户必须与调用动作的账户相符,否则该函数会抛出异常,不会往下执行。

  • require_auth2:require_auth2在require_auth基础上,增加了对账户权限的限制。

总之,require_auth函数要求调用者必须是指定账户,否则合约执行失败;require_auth2函数要求调用者必须是指定账户的指定权限,否则合约执行失败;has_auth函数返回调用者是否是指定账户,不会抛出异常。

-END-

猜你喜欢

转载自blog.csdn.net/BSN_yanxishe/article/details/131637957