一、目标

本文具体介绍了开发、部署和测试一个地址簿的智能合约的流程,实用于EOS的初学者理解如何应用智能合约实现本地区块链上数据的长久化和对长久化数据的增删改查。

二、智能合约介绍

区块链作为一种分布式可信计算平台,去中心化是其最实质的特色。每笔交易的记录不可篡改地存储在区块链上。智能合约中定义能够在区块链上执行的动作action和交易transaction的代码。能够在区块链上执行,并将合约执行状态作为该区块链实例不可变历史的一部分。因而,开发人员能够依赖该区块链作为可信计算环境,其中智能合约的输出、执行和后果都是独立的,不受内部影响。

三、术语解释

EOS

EOS是Enterprise Operation System的缩写,是商用分布式应用设计的一款区块链操作系统。EOS引入了一种新的区块链架构EOSIO,用于实现分布式应用的性能扩大。与比特币、以太坊等货币不同,EOS是一种基于EOSIO软件我的项目公布的代币,也被称为区块链3.0。

索引

索引个别是指关系数据库中对某一列或多个列的值进行预排序的数据结构。在这里,索引是内存表的某一字段,咱们能够依据该字段操作内存表的数据。

多索引multi_index

EOS仿造Boost库中的Multi-Index Containers,开发了C++类 eosio::multi_index(以下简称为multi_index),中文也能够叫作多索引表类。通过这个API,咱们能够很简略地反对数据库表的多键排序、查找、应用上上限等性能。这个新的API应用迭代器接口,可显著晋升扫表的性能。

四、编写智能合约

(一)定义程序根本构造

在链所在目录下新建一个addressbook文件夹,在addressbook文件夹中创立一个addressbook.cpp文件。

cd your_contract_pathmkdir addressbookcd addressbooktouch addressbook.cpp

引入头文件、命名空间,

#include <eosio/eosio.hpp>using namespace eosio;

定义合约类addressbook和其构造函数。合约类该当继承自eosio::contract。eosio::contract具备三个爱护的成员,和泛滥私有成员函数。其中三个爱护成员如下:

类型名称意义
eosio::name_self部署此账户的合约名称
eosio::name_first_receiver首次收到传入操作的账户
datastream< const char * >_ds合约的数据流

在申明派生类构造函数时,须要指明这三个成员。

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

(二)定义数据表构造及索引

1、定义构造体

首先应用struct关键字创立一个构造体,而后用[[eosio::table]]标注这个构造体是一个合约表,这里申明了一个person构造体:

private:struct [[eosio::table]] person { name key; std::string first_name; std::string last_name; uint64_t age; std::string street; std::string city; std::string state; };
类型阐明:

name:名称类型,账号名、表名、动作名都是该类型,只能应用26个小写字母和1到5的数字,特殊符号能够应用小数点,必须以字母结尾且总长不超过12。 uint64_t:无符号64位整数类型,表主键、name本质都是该类型。这里须要留神,合约的表名与构造体的名称没有关系,因而构造体的名称不用遵循name类型的规定。

表的构造如下:
类型名称意义
eosio::namekey主键 账户名
stringfirst_name名字
stringlast_name姓氏
uint64_tage年龄
stringstreet街道
stringcity城市
stringstate

2、定义主键

传统数据库表通常有惟一的主键,它容许明确标识表中的特定行,并为表中的行提供规范排列程序。 EOS合约数据库反对相似的语义,然而在multi_index容器中主键必须是惟一的无符号64位整数(即uint64_t类型)。multi_index中的对象按主键索引,以无符号64位整数主键的升序排列。接下来咱们定义一个主键函数,上文中曾经阐明name类型本质上是 uint64_t类型,该函数应用key.value返回一个uint64_t类型的值,并且因为key字段的含意是EOS中的账户名,因而能够保障唯一性:

uint64_t primary_key() const { return key.value;}

3、定义二级索引

multi_index容器中非主键索引能够是:

  • uint64_t
  • uint128_t
  • double
  • long double
  • eosio::checksum256

罕用的是uint64_t和double类型。应用age字段作为二级索引:

uint64_t primary_key() const { return key.value;}

4、定义多索引表

合约里的表都是通过multi_index容器来定义,咱们将下面定义的person构造体传入multi_index容器并配置主键索引和二级索引:

using address_index = eosio::multi_index<"people"_n, person,indexed_by<"byage"_n, const_mem_fun<person, uint64_t, &person::get_secondary>>;>;
阐明:
  • _n操作符用于定义一个name类型,上述代码将“people” 定义为name类型并作为表名;
  • person构造体被传入作为表的构造;
  • indexed_by构造用于实例化索引,第一个参数“byage”_n为索引名,第二个参数const_mem_fun为函数调用运算符,该运算符提取const值作为索引键。本例中,咱们将其指向之前定义的getter函数get_secondary。

应用上述定义,当初咱们有了一个名为people的表,目前addressbook.cpp的残缺代码如下:

#include <eosio/eosio.hpp>using namespace eosio;class [[eosio::contract("addressbook")]] addressbook : public eosio::contract { public:    // 构造函数,调用基类构造函数 addressbook(name receiver, name code,  datastream<const char*> ds): contract(receiver, code, ds) {}  private:  // 表构造 struct [[eosio::table]] person {  name key;  std::string first_name;  std::string last_name;  uint64_t age;  std::string street;  std::string city;  std::string state;    uint64_t primary_key() const { return key.value; }  uint64_t get_secondary_1() const { return age; }   };  // 定义多索引表 using address_index = eosio::multi_index<"people"_n, person, indexed_by<"byage"_n, const_mem_fun<person, uint64_t, &person::get_secondary_1>>>; };

(三)定义数据操作方法

定义好表的构造后,咱们通过[[eosio::action]]来定义对数据进行增删改的根本动作。

1、削减和批改动作

首先是提供了插入或批改数据的动作upsert,为了简化用户体验,应用繁多办法负责行的插入和批改,并将其命名为 “upsert” ,即 “update” 和 “insert” 的组合。该办法的参数该当包含所有须要存入people表的信息成员。

public:[[eosio::action]]void upsert( name user, std::string first_name, std::string last_name, uint64_t age, std::string street, std::string city, std::string state) {}

一般来说,用户心愿只有本人能对本人的记录进行更改,因而咱们应用 require_auth() 来验证权限,此办法接管name类型参数,并断言执行该动作的账户等于接管的值,具备执行动作的权限:

[[eosio::action]]void upsert(name user, std::string first_name, std::string last_name, uint64_t age, std::string street, std::string city, std::string state) {  require_auth( user );}

当初咱们须要实例化曾经定义配置好的表,下面咱们已配置多索引表并将其申明为address_index,当初要实例化表,须要两个参数:

  • 第一个参数code,它指定此表的所有者,在这里,表的所有者为合约所部署的账户,咱们应用get_first_receiver()函数传入,get_first_receiver() 函数返回该动作的第一个接受者的name类型名字。
  • 第二个参数scope,它确保表在此合约范畴内的唯一性。在这里,咱们应用get_first_receiver().value传入。
address_index addresses(get_first_receiver(), get_first_receiver().value);

接下来,查问迭代器,并用变量iterator来接管:

auto iterator = addresses.find(user.value);

之后咱们能够应用emplace() 函数和modify() 函数来插入或批改记录。当多索引表中未查问到该账户的记录时,应用emplace() 向表中增加;当多索引表中查问到过往记录时,应用modify() 批改原有记录。

 if( iterator == addresses.end() )  {    //削减    addresses.emplace(user, [&]( auto& row ) {      row.key = user;      row.first_name = first_name;      row.last_name = last_name;      row.age = age;      row.street = street;      row.city = city;      row.state = state;    });  }  else {    //批改    addresses.modify(iterator, user, [&]( auto& row ) {      row.key = user;      row.first_name = first_name;      row.last_name = last_name;      row.age = age;      row.street = street;      row.city = city;      row.state = state;    });  }

2、删除动作

与上文相似,定义erase动作提供删除数据的动作,查问迭代器。

[[eosio::action]]  void erase(name user) {    require_auth(user);    address_index addresses(get_self(), get_first_receiver().value);    auto iterator = addresses.find(user.value);    }

这里应用check() 判断表中是否存在该记录,若不存在则给出报错,存在则应用erase删除该项。

check(iterator != addresses.end(), "Record does not exist");    addresses.erase(iterator);

3、保留文件

退出以上两个动作后,目前addressbook.cpp的残缺代码如下:

#include <eosio/eosio.hpp>using namespace eosio;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 upsert(name user, std::string first_name, std::string last_name, uint64_t age, std::string street, std::string city, std::string state) {    require_auth( user );    address_index addresses(get_first_receiver(),get_first_receiver().value);    auto iterator = addresses.find(user.value);    if( iterator == addresses.end() )    {      addresses.emplace(user, [&]( auto& row ) {       row.key = user;       row.first_name = first_name;       row.last_name = last_name;       row.age = age;       row.street = street;       row.city = city;       row.state = state;      });    }    else {      addresses.modify(iterator, user, [&]( auto& row ) {        row.key = user;        row.first_name = first_name;        row.last_name = last_name;        row.age = age;        row.street = street;        row.city = city;        row.state = state;      });    }  }  [[eosio::action]]  void erase(name user) {    require_auth(user);    address_index addresses(get_self(), get_first_receiver().value);    auto iterator = addresses.find(user.value);    check(iterator != addresses.end(), "Record does not exist");    addresses.erase(iterator);  }private:  struct [[eosio::table]] person {    name key;    std::string first_name;    std::string last_name;    uint64_t age;    std::string street;    std::string city;    std::string state;    uint64_t primary_key() const { return key.value; }    uint64_t get_secondary_1() const { return age; }  };  using address_index = eosio::multi_index<"people"_n, person, indexed_by<"byage"_n, const_mem_fun<person, uint64_t, &person::get_secondary_1>>>;};

五、部署测试

(一)部署

1、创立addressbook账户

cleos create account eosio addressbook EOS7yK5K2mRCijPpRzStvucn53D4SybVge8uQ3hSnLaUvT4CPXzgo

留神其中:EOS7yK5K2mRCijPpRzStvucn53D4SybVge8uQ3hSnLaUvT4CPXzgo 是存储于cleos钱包中的一个公钥,理论开发状况中需依据本人钱包中存储的公钥进行替换。 

2、进入智能合约所在目录

cd your_contract_path/addressbook

3、编译

eosio-cpp -abigen -o addressbook.wasm addressbook.cpp

4、部署合约到addressbook账户上

cd ..cleos set contract addressbook addressbook

(二)测试

1、创立两个测试账户alice和bob

cleos create account eosio alice 公钥cleos create account eosio bob 公钥

2、插入数据

调用upsert动作插入数据

cleos push action addressbook upsert '["alice", "alice", "liddell", 9, "123 drink me way", "wonderland", "amsterdam"]' -p alice@activecleos push action addressbook upsert '["bob", "bob", "is a guy", 49, "doesnt exist", "somewhere", "someplace"]' -p bob@active

插入胜利,运行后果如下

3、查问数据

查问信息个别在命令行应用cleos get table进行:

cleos get table 领有表的账户 表所在合约名 表名

此条命令能够查问出表内的所有信息,也能够通过增加后续的束缚来查问指定信息:

  • --upper XX等于或在此之前
  • --lower XX等于或在此之后
  • --key-type XXX类型
  • --index X依据第几个索引
  • --limit XX显示前几个数据

咱们能够通过主键key查问表的数据

cleos get table addressbook addressbook people --lower alice

--lower alice示意查问的下界,以“alice”作为下界能够查问到两条记录:

接下来通过二级索引age查问数据

cleos get table addressbook addressbook people --upper 10 \--key-type i64 \--index 2

--upper 10示意查问上界,即查问索引字段小于等于10的记录。--index 2示意应用二级索引查问。查问到一条记录:

4、批改数据

调用upsert动作批改数据:

cleos push action addressbook upsert '["alice", "mary", "brown", 9, "123 drink me way", "wonderland", "amsterdam"]' -p alice@active

查问后能够看到记录的first_name和last_name字段已被批改:

5、删除数据

调用erase动作删除数据:

cleos push action addressbook erase '["alice"]' -p alice@active

删除后查问不到alice,阐明删除胜利:

六、常见问题

更改数据表构造

须要留神的是,如果合约曾经部署到合约账户上且表中曾经存储了数据,那在更改表的构造如增加删除索引或字段时,则须要将表中的数据全副删除后,再将新的合约部署到合约账户上。