一、目标
本文具体介绍了开发、部署和测试一个地址簿的智能合约的流程,实用于 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_path
mkdir addressbook
cd addressbook
touch 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::name | key | 主键 账户名 |
string | first_name | 名字 |
string | last_name | 姓氏 |
uint64_t | age | 年龄 |
string | street | 街道 |
string | city | 城市 |
string | state | 州 |
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@active
cleos 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,阐明删除胜利:
六、常见问题
更改数据表构造
须要留神的是,如果合约曾经部署到合约账户上且表中曾经存储了数据,那在更改表的构造如增加删除索引或字段时,则须要将表中的数据全副删除后,再将新的合约部署到合约账户上。