一、目的
本文详细介绍了开发、部署和测试一个地址簿的智能合约的流程,适用于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
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 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>;
>;
说明:
- _n操作符用于定义一个name类型,上述代码将“people” 定义为name类型并作为表名;
- person结构体被传入作为表的结构;
- indexed_by结构用于实例化索引,第一个参数“byage”_n为索引名,第二个参数const_mem_fun为函数调用运算符,该运算符提取const值作为索引键。本例中,我们将其指向之前定义的getter函数get_secondary。
使用上述定义,现在我们有了一个名为people的表,目前addressbook.cpp的完整代码如下:
#include
using namespace eosio;
class [[eosio::contract("addressbook")]] addressbook : public eosio::contract {
public:
// 构造函数,调用基类构造函数
addressbook(name receiver, name code, datastream 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>>;
};
(三)定义数据操作方法
定义好表的结构后,我们通过[[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
using namespace eosio;
class [[eosio::contract("addressbook")]] addressbook : public eosio::contract {
public:
addressbook(name receiver, name code, datastream 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>>;
};
五、部署测试
(一)部署
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
六、常见问题
更改数据表结构
需要注意的是,如果合约已经部署到合约账户上且表中已经存储了数据,那在更改表的结构如添加删除索引或字段时,则需要将表中的数据全部删除后,再将新的合约部署到合约账户上。