はじめに
本記事ではEOS Developer Portalで紹介されているaddressbookコントラクトを用いて、EOSにおけるデータの永続化と、コントラクト内でアクションを実行するインラインアクションについて、全3回で解説します。
第3回では、コントラクトの内部でアクションを作成・送信する方法として、インラインアクションの実装方法を紹介します。
本記事は、過去にコンセンサス・ベイスが主宰していたオンラインサロンの記事です。記事は2017年~2018年にかけて執筆されたため、一部は、既に古くなっている可能性があります。あらかじめご了承ください。
関連する記事
EOSIO Developer Portal 解説 最短でHello World!
EOSIO Developer Portal 解説 独自トークンの発行 第1回
EOSIO Developer Portal 解説 独自トークンの発行 第2回
EOSIO Developer Portal 解説 独自トークンの発行 第3回
EOSIO Developer Portal 解説 データの永続化とインラインアクション 第1回
EOSIO Developer Portal 解説 データの永続化とインラインアクション 第2回
EOSIO Developer Portal 解説 データの永続化とインラインアクション 第3回
インラインアクションとは
インラインアクションとは、コントラクトのアクションの中で他のアクションを実行する手法の一つです。インラインアクションを用いることで、自身のコントラクトの別のアクションを実行したり、他のコントラクトのアクションを実行することができます。
コントラクトの中でアクションを実行する手法は、インラインアクションの他にディファード(遅延)アクションがあります。ディファードアクションは、呼び出し元のアクションとは非同期に実行されるアクションで、呼び出し元のアクションではディファードアクションの予約のみが実行されます。そのため、ディファードアクションが成功するかどうかは、呼び出し元のアクションでは判断できません。
一方、インラインアクションは、アクションの中で同期的に実行されるアクションで、呼び出し元のアクションの一部として実行されます。そのため、インラインアクションが失敗した場合は呼び出し元のアクション自体が失敗します。
インラインアクションの実装と動作確認
それでは、インラインアクションの実装例として、アクション内から同じコントラクトの別アクションを実行してみます。
addressbookコントラクトの修正
前回のaddressbookコントラクトのパブリックアクションに、コード1に示すnotifyというアクションを追加します。notifyアクションは、アクションが実行されたことをアカウントに通知するアクションです。require_recipient関数を用いると、アクションのトランザクションのコピーが指定したアカウントに送信されます。これにより、アカウントはそのアクションが実行されたことを検知できます。
コード1. addressbookコントラクトにnotifyアクションを実装
public:
...
[[eosio::action]]
void notify(name user, std::string msg) {
require_auth(get_self());
require_recipient(user);
}
続いて、notifyアクションをインラインアクションとして実行する補助関数として、コード2に示すsend_summary関数を実装します。インラインアクションの作成には、アクションを実行するパーミッション、ターゲットアカウント、ターゲットアクション、アクションのパラメタを指定します。アクションの送信には、アクションのsendメソッドを実行します。
コード2. addressbookコントラクトにsend_summary関数を実装
private:
struct [[eosio::table]] person {
...
};
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();
};
send_summary関数の呼び出しを、addressesテーブルのデータの作成、更新、削除のタイミングでおこなうよう修正します。コード3に示すとおり、send_summary関数の呼び出しを3箇所に追記します。
コード3. addressbookコントラクトにsend_summary関数の呼び出しを追記
[[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) {
...
if( iterator == addresses.end() )
{
...
send_summary(user, " successfully emplaced record to addressbook");
}
else {
...
send_summary(user, " successfully modified record to addressbook");
}
}
[[eosio::action]]
void erase(name user) {
...
send_summary(user, " successfully erased record from addressbook");
}
最後に、コード4に示すとおり、EOSIO_DISPATCHにnotifyアクションを追加します。
コード4. addressbookコントラクトのDISPATCHにnotifyアクションを追加
EOSIO_DISPATCH( addressbook, (upsert)(notify)(erase))
addressbookコントラクトを修正したので、コンパイルをしてwasmファイルとabiファイルを更新します。
コマンド1. addressbookコントラクトのコンパイル
$ eosio-cpp -o addressbook.wasm addressbook.cpp --abigen
コンパイルが完了したら、再度addressbookコントラクトをデプロイします。
コマンド2. addressbookコントラクトのデプロイ
$ cleos set contract addressbook CONTRACTS_DIR/addressbook
パーミッションの追加と動作確認
addressbookコントラクトの修正は完了しましたが、このままではインラインアクションを実行することはできません。試しに、コマンド3を用いてupsertアクションを実行してみると、エラーが発生することが分かります。
コマンド3. upsertアクションの実行
$ cleos push action addressbook upsert '["alice", "alice", "liddell", 21, "123 drink me way", "wonderland", "amsterdam"]' -p alice@active
Error 3100006: Subjective exception thrown during block production
EOSでは、セキュリティ上の安全性を担保するために、デフォルトではコントラクトがアクションに署名をすることが許可されていません。インラインアクションを実行するためには、明示的にコントラクトのアカウントにパーミッションを追加してあげる必要があります。
コマンド4を用いて、現在のaddressbookアカウントのパーミッションを確認してみましょう。パーミッションにはownerとactiveがあり、アカウントを作成する際に指定した公開鍵が権限に紐付けられていることが分かります。
コマンド4. addressbookアカウントのパーミッションを確認
$ cleos get account addressbook
created: 2019-02-25T03:21:10.000
permissions:
owner 1: 1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
active 1: 1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
コマンド5を用いて、インラインアクションを実行するための「eosio.code」という疑似権限を、addressbookアカウントのactiveパーミッションに追加します。
コマンド5. addressbookアカウントのactiveパーミッションにeosio.codeを追加
$ cleos set account permission addressbook active --add-code
コマンド6を用いて、再度addressbookアカウントのパーミッションを確認してみます。すると、さきほどは存在しなかった addressbook@eosio.code という権限が、activeパーミッションに加わっていることが分かります。
コマンド6. addressbookアカウントのパーミッションを確認
$ cleos get account addressbook
created: 2019-02-25T03:21:10.000
permissions:
owner 1: 1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
active 1: 1 EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV, 1 addressbook@eosio.code
eosio.code権限が加わった状態で、再度コマンド3を実行すると、正常にupsertアクションが実行できます。
upsertアクションが実行されると、そのトランザクションのコピーが実行者に送られているはずです。コマンド7を用いて、アカウントが受け取ったアクションの履歴を確認してみると、確かにさきほど実行したアクションが通知されています。
コマンド7. aliceの受け取ったアクションを確認
$ cleos get actions alice
# 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...
外部コントラクトのアクション実行
続いて、addressbookコントラクトから、外部のコントラクトのアクションをインラインアクションで実行する例を紹介します。
abcounterコントラクトの実装
コマンド8を用いて、空のabcouter.cppを作成します。
コマンド8. abcounter.cppの作成
$ cd CONTRACTS_DIR
$ mkdir abcounter
$ cd abcounter
$ touch abcounter.cpp
abcounter.cppをエディタで開き、コード4の内容を記述します。abcounterコントラクトは、addressbookのデータの登録、更新、削除の実行回数を、ユーザーごとに保持するcountsテーブルを持ちます。
countsテーブルの更新アクションはabcounterコントラクトが実装しており、このアクションをaddressbookコントラクトから実行します。
コード4. abcounterコントラクトの実装
#include
using namespace eosio;
class [[eosio::contract]] abcounter : public eosio::contract {
public:
using contract::contract;
abcounter(name receiver, name code, datastream ds): contract(receiver, code, ds) {}
[[eosio::action]]
void count(name user, std::string type) {
require_auth( name("addressbook"));
count_index counts(name(_code), _code.value);
auto iterator = counts.find(user.value);
if (iterator == counts.end()) {
counts.emplace("addressbook"_n, [&]( auto& row ) {
row.key = user;
row.emplaced = (type == "emplace") ? 1 : 0;
row.modified = (type == "modify") ? 1 : 0;
row.erased = (type == "erase") ? 1 : 0;
});
}
else {
counts.modify(iterator, "addressbook"_n, [&]( auto& row ) {
if(type == "emplace") { row.emplaced += 1; }
if(type == "modify") { row.modified += 1; }
if(type == "erase") { row.erased += 1; }
});
}
}
private:
struct [[eosio::table]] counter {
name key;
uint64_t emplaced;
uint64_t modified;
uint64_t erased;
uint64_t primary_key() const { return key.value; }
};
using count_index = eosio::multi_index<"counts"_n, counter>;
};
EOSIO_DISPATCH( abcounter, (count));
コマンド9を用いて、abcounterアカウントの作成とコントラクトのコンパイル・デプロイをおこないます。
コマンド9. abcounterアカウントの作成とコントラクトのコンパイル・デプロイ
$ cleos create account eosio abcounter YOUR_PUBLIC_KEY
$ eosio-cpp -o abcounter.wasm abcounter.cpp --abigen
$ cleos set contract abcounter CONTRACTS_DIR/abcounter
addressbookコントラクトの修正
続いて、abcounterのアクションを実行するよう、addressbookコントラクトを修正します。コマンド10のとおりaddressbookフォルダに移動します。
コマンド10. addressbookフォルダに移動
$ cd CONTRACTS_DIR/addressbook
addressbook.cppをエディタで開き、コード5に示す修正を加えます。コード5では、プライベート関数としてincrement_counterを実装し、それをupsert, eraseアクション内の3箇所で呼び出しています。
increment_counter関数内で、abcounterコントラクトのcountアクションをインラインアクションとして呼び出しています。
コード5. addressbookコントラクトからabcounterのアクションを呼び出し
public:
...
[[eosio::action]]
void upsert(...) {
...
if( iterator == addresses.end() )
{
...
increment_counter(user, "emplace");
});
}
else {
...
increment_counter(user, "modify");
});
}
}
[[eosio::action]]
void erase(name user) {
...
increment_counter(user, "erase");
}
...
private:
struct [[eosio::table]] person {
...
};
void send_summary(name user, std::string message) {
...
};
void increment_counter(name user, std::string type) {
action counter = action(
permission_level{get_self(),"active"_n},
"abcounter"_n,
"count"_n,
std::make_tuple(user, type)
);
counter.send();
}
...
};
コードの修正が完了したら、コマンド11を用いて再度addressbookコントラクトのコンパイルとデプロイをおこないます。
コマンド11. addressbookコントラクトのコンパイルとデプロイ
$ eosio-cpp -o addressbook.wasm addressbook.cpp
$ cleos set contract addressbook CONTRACTS_DIR/addressbook
動作確認
デプロイが終わったら、コマンド12を用いて、再度aliceのデータを更新してみましょう。
コマンド12. upsertアクションを実行してaliceのデータを更新
$ cleos push action addressbook upsert '["alice", "alice", "liddell", 19, "123 drink me way", "wonderland", "amsterdam"]' -p alice@active
executed transaction: cc46f20da7fc431124e418ecff90aa882d9ca017a703da78477b381a0246eaf7 152 bytes 1493 us
# addressbook <= addressbook::upsert {"user":"alice","first_name":"alice","last_name":"liddell","street":"123 drink me way","city":"wonde...
# addressbook <= addressbook::notify {"user":"alice","msg":"alice successfully modified record in addressbook"}
# alice <= addressbook::notify {"user":"alice","msg":"alice successfully modified record in addressbook"}
# abcounter <= abcounter::count {"user":"alice","type":"modify"}
コマンド13を用いてabcounerコントラクトのcountsテーブルから、aliceのデータを確認してみると、modifiedの値が1になっていることが分かります。
コマンド13. abcounterのデータを確認
$ cleos get table abcounter abcounter counts --lower alice --limit 1
{
"rows": [{
"key": "alice",
"emplaced": 0,
"modified": 1,
"erased": 0
}
],
"more": false
}
続いて、コマンド14を用いて、aliceのデータを一度削除します。
コマンド14. eraseアクションを用いてaliceのデータを削除
$ cleos push action addressbook erase '["alice"]' -p alice@active
executed transaction: aa82577cb1efecf7f2871eac062913218385f6ab2597eaf31a4c0d25ef1bd7df 104 bytes 973 us
# addressbook <= addressbook::erase {"user":"alice"}
>> Erased
# addressbook <= addressbook::notify {"user":"alice","msg":"alice successfully erased record from addressbook"}
>> Notified
# alice <= addressbook::notify {"user":"alice","msg":"alice successfully erased record from addressbook"}
# abcounter <= abcounter::count {"user":"alice","type":"erase"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
aliceのデータが削除されたら、コマンド15を用いて再度aliceのデータを登録します。
コマンド15. upsertアクションを用いてaliceのデータを登録
$ cleos push action addressbook upsert '["alice", "alice", "liddell", 21,"1 there we go", "wonderland", "amsterdam"]' -p alice@active
executed transaction: c819ffeade670e3b44a40f09cf4462384d6359b5e44dd211f4367ac6d3ccbc70 152 bytes 909 us
# addressbook <= addressbook::upsert {"user":"alice","first_name":"alice","last_name":"liddell","street":"1 coming down","city":"normalla...
# addressbook <= addressbook::notify {"user":"alice","msg":"alice successfully emplaced record to addressbook"}
>> Notified
# alice <= addressbook::notify {"user":"alice","msg":"alice successfully emplaced record to addressbook"}
# abcounter <= abcounter::count {"user":"alice","type":"emplace"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
コマンド6を用いて、再度abcounterのaliceのデータを確認すると、erasedとemplacedの値も1にインクリメントされていることが分かります。
コマンド16. abcounterのデータを確認
$ cleos get table abcounter abcounter counts --lower alice
{
"rows": [{
"key": "alice",
"emplaced": 1,
"modified": 1,
"erased": 1
}
],
"more": false
}
本稿では、インラインアクションを用いて、同一コントラクト内のアクションや別コントラクトのアクションを同期的に呼び出す手法を紹介しました。インラインアクションにより、複数のアクションやコントラクトを連携して、複雑な処理を実現できるようになります。
参考文献:
- EOSIO Developer Portal – 2.6 Adding Inline Actions
https://developers.eos.io/eosio-home/docs/inline-actions - EOSIO Developer Portal – 2.7 Inline Action to External Contract
https://developers.eos.io/eosio-home/docs/sending-an-inline-transaction-to-external-contract - EOSIO Developer Portal – Communication Model
https://developers.eos.io/eosio-cpp/docs/communication-model - EOSIO Developer Portal – Adding eosio.code to active authority with cleos’ helper
https://developers.eos.io/eosio-cleos/docs/adding-eosiocode-to-active-authority-with-cleos-helper