本文档介绍了合约编写的基础知识,包含合约初始化、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-