一、目的
本文详细介绍了开发、部署和测试一个地址簿的智能合约的流程,适用于 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 具有三个保护的成员,和众多公有成员函数。其中三个保护成员如下:
在声明派生类构造函数时,需要指明这三个成员。
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 类型的规则。
表的结构如下:
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>>;
>;
复制代码
说明:
使用上述定义,现在我们有了一个名为 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,现在要实例化表,需要两个参数:
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,说明删除成功:
六、常见问题
更改数据表结构
需要注意的是,如果合约已经部署到合约账户上且表中已经存储了数据,那在更改表的结构如添加删除索引或字段时,则需要将表中的数据全部删除后,再将新的合约部署到合约账户上。
评论