2.6 添加Inline Actions
介绍
通过先前对addressbook
合约的授权演示了multi-index tables的基本操作.在本系列教程的这一部分你会学会如何构造一个actions,并从合约中发送这些actions.
Step 1:将eosio.code添加到权限
为了从addressbook
发送inline actions,添加eosio.code
权限到合约的账户的活动权限.打开terminal并执行下面命令:
cleos set account permission addressbook active '{"threshold": 1,"keys": [{"key": "EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV","weight": 1}], "accounts": [{"permission":{"actor":"addressbook","permission":"eosio.code"},"weight":1}]}' -p addressbook@owner
eosio.code
授权是增强安全性的伪授权,并允许合约可以执行inline actions.
Step2: 通知 Aciton
如果还没打开,打开上一个教程中授权好的addressbook.cpp
合约.写一个action以处理"transaction receipt".在addressbook
类中创建一个 private helper function来完成它.
[[eosio::action]]
void notify(name user, std::string msg) {}
该函数非常简单,只接收一个name
和string
.
Step 3:使用require_recipient拷贝action到sender
该交易需要被拷贝给user以让其可被视为收据.为了实现该需求,使用require_recipient 函数.
[[eosio::action]]
void notify(name user, std::string msg) {
require_recipient(user);
}
这个action非常简单,它会把给到的action拷贝给传入的user.但是,作为写入,任何用户都可以调用该函数以及从该合约做一个"假"的收据.这可能被用在恶意行为上,应该将其视为一个弱点.为了改善它,要求调用此action的权限来自合约,为此,使用 get_self.
[[eosio::action]]
void notify(name user, std::string msg) {
require_auth(get_self());
require_recipient(user);
}
现在,如果用户bob
直接调用该函数,但是传入的参数是alice
,这个action会抛出一个异常.
Step 4:通知helper发送inline transactions
因为inline action会被多次调用,写一个快捷的helper来让代码可以最大程度复用.在你合约的private区域定义一个新的函数:
...
private:
void send_summary(name user, std::string message){}
在这个helper中构造一个action并发送它.
Step 5: Action 构造函数
修改addressbook
合约,让用户每次操作合约action时都发送一个收据给他们.
首先,先处理"创建记录"的情况.当在表中找不到记录是触发该情况,当iterator == addresses.end()
为true
时.
将该对象存为一个action
variable命名为notification
.
...
private:
void send_summary(name user, std::string message){
action(
//permission_level,
//code,
//action,
//data
);
}
该action构造需要几个参数:
- permission_level struct
- 将要调用的合约(使用
eosio::name
来初始化它) - 将要调用的action(使用
eosio::name
来初始化它) - 传给action的数据,一个和将要被调用的actions相关的位置tuple
The Permission struct
在该合约中,permission应该通过合约使用get_self()
的active
权限来授权.提醒一下,要使用内联的active
权限,你需要你的合约给eosio.code
伪权限授权(上面提到过).
...
private:
void send_summary(name user, std::string message){
action(
permission_level{get_self(),"active"_n},
);
}
"code","account where contract is deployed"的别称
因为action将在该合约被调用,使用 get_self . "addressbook"_n
在现在的情况下也可以, 但如果合约被其他的账户名部署,就不可以了.所以,get_self()
是最佳选择.
...
private:
void send_summary(name user, std::string message){
action notification = action(
permission_level{get_self(),"active"_n},
get_self(),
//action
//data
);
}
The Action
先前定义好的notify
action将被该inline action调用.在这使用操作符n_
.
...
private:
void send_summary(name user, std::string message){
action(
permission_level{get_self(),"active"_n},
get_self(),
"notify"_n,
//data
);
}
The Data
最后,把要传给该action的data定义好.通知函数接受两个参数,name
和一个string
. action构造函数需要bytes
类型的数据,所以可以使用make_tuple
,这个函数由C++ std
library提供.传到tuple中的数据是有位置限制的,并由被调用的action中接受参数的顺序决定.
- 传入由
upsert()
action提供的user
变量. - 连接一个包含user name的字符串,并包含
message
来传给notify
action.
...
private:
void send_summary(name user, std::string message){
action(
permission_level{get_self(),"active"_n},
get_self(),
"notify"_n,
std::make_tuple(user, name{user}.to_string() + message)
);
}
发送action
最后,使用action struct的send
方法来发送该action.
...
private:
void send_summary(name user, std::string message){
action(
permission_level{get_self(),"active"_n},
get_self(),
"notify"_n,
std::make_tuple(user, name{user}.to_string() + message)
).send();
}
Step 6:调用helper并注入相关信息
现在helper已经定义好的,它应该在相关的位置被正确调用.这有三个特定的位置可以让这个新的notify
helper被调用:
当合约
emplaces
了一条新纪录后:send_summary(user, "successfully emplaced record to addressbook");
当合约
modifies
了一条现存记录后:send_summary(user, "successfully modified record in addressbook");
当合约
erases
了一条现存记录后:send_summary(user, "successfully erased record from addressbook");
Step 7:更新EOSIO_ABI宏
新的action notify
已经加到该合约中了,所以需要在文件底部将notify
action添加到EOSIO_DISPATCH
中.这样就能使eosio.cdt
的优化器不会将其清除了.
EOSIO_DISPATCH( addressbook, (upsert)(erase)(notify) )
现在,准备就绪,这是addressbook
合约的当前状态:
#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>
using namespace eosio;
class [[eosio::contract]] addressbook : public eosio::contract {
public:
using contract::contract;
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(_code, _code.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;
});
send_summary(user, " successfully emplaced record to addressbook");
}
else {
std::string changes;
addresses.modify(iterator, user, [&]( auto& row ) {
row.key = user;
row.first_name = first_name;
row.last_name = last_name;
row.street = street;
row.city = city;
row.state = state;
});
send_summary(user, " successfully modified record to addressbook");
}
}
[[eosio::action]]
void erase(name user) {
require_auth(user);
address_index addresses(_self, _code.value);
auto iterator = addresses.find(user.value);
eosio_assert(iterator != addresses.end(), "Record does not exist");
addresses.erase(iterator);
send_summary(user, " successfully erased record from addressbook");
}
[[eosio::action]]
void notify(name user, std::string msg) {
require_auth(get_self());
require_recipient(user);
}
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;}
};
void send_summary(name user, std::string message) {
action(
permission_level{get_self(),"active"_n},
get_self(),
"notify"_n,
std::make_tuple(user, name{user}.to_string() + message)
).send();
};
typedef eosio::multi_index<"people"_n, person,
indexed_by<"byage"_n, const_mem_fun<person, uint64_t, &person::get_secondary_1>>
> address_index;
};
EOSIO_DISPATCH( addressbook, (upsert)(notify)(erase))
Step 8:重新编译和重新生成ABI文件
打开terminal,进入/Users/zhong/coding/CLion/contracts/addressbook
cd /Users/zhong/coding/CLion/contracts/addressbook
现在,重新编译合约,添加--abigen
标记因为对合约的修改会影响到ABI.如果你细心的跟着说明操作,你应该不会看到错误.
eosio-cpp -o addressbook.wasm addressbook.cpp --abigen
EOSIO上的合约是可以升级的,因此合约改变后能重新部署.
cleos set contract addressbook /Users/zhong/coding/CLion/contracts/addressbook
Result:
Publishing contract...
executed transaction: 1898d22d994c97824228b24a1741ca3bd5c7bc2eba9fea8e83446d78bfb264fd 7320 bytes 747 us
# eosio <= eosio::setcode {"account":"addressbook","vmtype":0,"vmversion":0,"code":"0061736d0100000001a6011a60027f7e0060077f7e...
# eosio <= eosio::setabi {"account":"addressbook","abi":"0e656f73696f3a3a6162692f312e30010c6163636f756e745f6e616d65046e616d65...
成功!
Step 9:测试
现在合约已经修改并部署好,开始测试它.在先前的教程中,alice的addressbook记录在测试的时候删除了,所以调用upsert
可以触发刚刚写到"create"分支的inline action.
cleos push action addressbook upsert '["alice", "alice","liddell", 21, "123 drink me way", "wonderland", "amsterdam"]' -p alice@active
cleos
会返回一些数据,包括这次交易的所有action
executed transaction: e9e30524186bb6501cf490ceb744fe50654eb393ce0dd733f3bb6c68ff4b5622 160 bytes 9810 us
# addressbook <= addressbook::upsert {"user":"alice","first_name":"alice","last_name":"liddell","age":21,"street":"123 drink me way","cit...
# addressbook <= addressbook::notify {"user":"alice","msg":"alicesuccessfully emplaced record to addressbook"}
# alice <= addressbook::notify {"user":"alice","msg":"alicesuccessfully emplaced record to addressbook"}
你能在响应中看到addressboo::notify
拷贝了本次交易中alice
的一些信息.使用cleos get actions 来获得与alice相关的执行过的actions.
cleos get actions alice
Result:
# seq when contract::action => receiver trx id... args
================================================================================================================
# 62 2018-09-15T12:57:09.000 addressbook::notify => alice 685ecc09... {"user":"alice","msg":"alice successfully added record to ad...