本文档介绍了合约编写的基础知识,包含合约初始化、action和权限的相干常识。实用于想要理解智能合约编写基础知识的初学者和开发者,帮忙其疾速理解和上手EOS智能合约的编写。作为智能合约的根底篇,本文仅波及合约初始化、action和权限方面的内容。
01
智能合约介绍
区块链作为一种分布式可信计算平台,去中心化是其最实质的特色。每笔交易的记录不可篡改地存储在区块链上。智能合约中定义能够在区块链上执行的动作action和交易transaction的代码。能够在区块链上执行,并将合约执行状态作为该区块链实例不可变历史的一部分。
因而,开发人员能够依赖该区块链作为可信计算环境,其中智能合约的输出、执行和后果都是独立的,不受内部影响。
02
合约编写基础知识介绍
(一)合约初始化
1、合约的构造函数
在EOSIO中,智能合约通过C++编写,并通过WASM(WebAssembly)字节码模式部署到区块链网络上。当部署实现后,能够调用合约的action执行相应的操作。在合约被部署后,会主动执行其初始化函数,以初始化合约的数据和状态。
智能合约的构造函数是在合约部署时被调用的,用于初始化合约的状态和数据。构造函数是一个非凡的成员函数,它没有返回值类型,且函数名与合约名雷同。
以下是一个简略的EOS智能合约的构造函数示例:
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动作:
运行后果如下:
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示例
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的操作。
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-