_code と _self の違い - Qiita でも少し触りましたが、 EOS
には通知
という仕組みがあります。
公式の eosio.token
コントラクトはこの仕組を使っているので、使い方と動きを確認してみます。
eosio.token
の transfer
ソース確認
transfer
の処理では、require_recipient( from );
と require_recipient( to );
があります。
void token::transfer( account_name from,
account_name to,
asset quantity,
string memo )
{
eosio_assert( from != to, "cannot transfer to self" );
require_auth( from );
eosio_assert( is_account( to ), "to account does not exist");
auto sym = quantity.symbol.name();
stats statstable( _self, sym );
const auto& st = statstable.get( sym );
require_recipient( from );
require_recipient( to );
eosio_assert( quantity.is_valid(), "invalid quantity" );
eosio_assert( quantity.amount > 0, "must transfer positive quantity" );
eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );
sub_balance( from, quantity );
add_balance( to, quantity, from );
}
実際送信する時は、下記のように出力されています
$ cleos push action eosio.token transfer '["bob", "alice", "10.0000 EOS", "memo"]' -p bob
executed transaction: 7f7e3491e13f05926aee6bfdab44f9acfba2e333893d2322652772ac2f70dcf1 136 bytes 4445 us
# eosio.token <= eosio.token::transfer {"from":"bob","to":"alice","quantity":"10.0000 EOS","memo":"memo"}
# bob <= eosio.token::transfer {"from":"bob","to":"alice","quantity":"10.0000 EOS","memo":"memo"}
# alice <= eosio.token::transfer {"from":"bob","to":"alice","quantity":"10.0000 EOS","memo":"memo"}
warning: transaction executed locally, but may not be confirmed by the network yet ]
上記通りに、eosio.token
の transfer
アクションを呼び出したら、bob
とalice
向けにも同じパラメータで送信されました。
送信されたと言っても何も動いてないように見えますが、何故でしょう。
通知の動き
通知の動きは下記になっています。
- 通知先を配列に保持しておく
- 各通知先アカウントに対して、スマートコントラクトを確認し、スマートコントラクトがある場合は、同じパラメータを渡して実行する
通知先が普通のアカウント(スマートコントラクトデプロイされてない場合)は実行されないのはよくて、スマートコントラクトである場合でも特に何も発生してないのは何故でしょう。
それは、デフォルトの EOSIO_DISPATCH
処理では、アクションの呼び出し先がこのコントラクトでない場合はスキップする
ようにしているからです。
// https://github.com/EOSIO/eosio.cdt/blob/master/libraries/eosiolib/dispatcher.hpp#L123-L133
#define EOSIO_DISPATCH( TYPE, MEMBERS ) \
extern "C" { \
void apply( uint64_t receiver, uint64_t code, uint64_t action ) { \
if( code == receiver ) { \
switch( action ) { \
EOSIO_DISPATCH_HELPER( TYPE, MEMBERS ) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \
} \
スマートコントラクトを実装する時、最後に定義しているアクションをEOSI_DISPATCH
マクロに渡していると思いますが、
このマクロの中では、上記通りに、if( code == receiver ) {
、トランザクションにあるアクションの呼び出し先(code)が今処理されているアカウント(receiver)であるかどうかをチェックしています。
当たり前ですが、自分のアカウントの場合だけ、実行するようにしています。
eosio.token
から通知がきた場合は、code
が eosio.token
になっているので、ここでスキップされています。
_code と _self の違い - Qiita のように、通知を受け取るようにしてみます。
#include <eosiolib/eosio.hpp>
#include <eosiolib/asset.hpp>
#include <string>
using namespace eosio;
using std::string;
class[[eosio::contract]] alice : public eosio::contract{
public :
alice(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds){}
[[eosio::action]]
void transfer(name from, name to, asset quantity, string memo) {
print("_code : ", name{_code}, ", _self : ", name{_self});
}
};
#define EOSIO_DISPATCH_EX(TYPE, MEMBERS) \
extern "C" { \
void apply(uint64_t receiver, uint64_t code, uint64_t action) \
{ \
if (code == receiver || code == "eosio.token"_n.value) \
{ \
switch (action) \
{ \
EOSIO_DISPATCH_HELPER(TYPE, MEMBERS) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \
} \
EOSIO_DISPATCH_EX(alice, (transfer))
このコントラクトをビルドし alice
アカウントにデプロイしておいてから、再度 alice
向けに送金してみます。
$ cleos push action eosio.token transfer '["bob", "alice", "11.0000 EOS", "memo"]' -p bob
executed transaction: 2a13ad27b13dfed936a0a56d6892b5208f64f3ee4896103b464bd67d13a0301c 136 bytes 537 us
# eosio.token <= eosio.token::transfer {"from":"bob","to":"alice","quantity":"11.0000 EOS","memo":"memo"}
# bob <= eosio.token::transfer {"from":"bob","to":"alice","quantity":"11.0000 EOS","memo":"memo"}
# alice <= eosio.token::transfer {"from":"bob","to":"alice","quantity":"11.0000 EOS","memo":"memo"}
>> _code : eosio.token, _self : alice
warning: transaction executed locally, but may not be confirmed by the network yet ]
出力 >> _code: eosio.token, _self : alice
通りに、通知されたことが確認出来ました。
通知先のアクションでエラーになったら?
わざとtransfer
にエラーを発生させておいて、
[[eosio::action]]
void transfer(name from, name to, asset quantity, string memo) {
print("_code : ", name{_code}, ", _self : ", name{_self});
eosio_assert( false, "throw error" );
}
再度送金してみると
$ cleos push action eosio.token transfer '["bob", "alice", "11.0000 EOS", "memo"]' -p bob
Error 3050003: eosio_assert_message assertion failure
送金処理がエラーになってしまいました。
nodeos
側のログは下記になっています。
error 2018-12-11T20:17:34.146 thread-0 wasm_interface.cpp:933 eosio_assert ] message: throw error
error 2018-12-11T20:17:34.152 thread-0 http_plugin.cpp:580 handle_exception ] FC Exception encountered while processing chain.push_transaction
debug 2018-12-11T20:17:34.152 thread-0 http_plugin.cpp:581 handle_exception ] Exception Details: 3050003 eosio_assert_message_exception: eosio_assert_message assertion failure
assertion failure with message: throw error
{"s":"throw error"}
thread-0 wasm_interface.cpp:934 eosio_assert
pending console output: _code : eosio.token, _self : alice
{"console":"_code : eosio.token, _self : alice"}
thread-0 apply_context.cpp:72 exec_one
結論としては、inline action
と同じ動作になって、通知先の処理で失敗したら、通知元の処理もエラーになります。
アクションがなかったら?
もし通知先のアカウントで、通知は受ける定義をしていますが、実際にそのアクションがない場合はどうなりますか?
#include <eosiolib/eosio.hpp>
#include <eosiolib/asset.hpp>
#include <string>
using namespace eosio;
using std::string;
class[[eosio::contract]] alice : public eosio::contract{
public :
alice(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds){}
[[eosio::action]]
void trans(name from, name to, asset quantity, string memo) {
print("_code : ", name{_code}, ", _self : ", name{_self});
}
};
#define EOSIO_DISPATCH_EX(TYPE, MEMBERS) \
extern "C" { \
void apply(uint64_t receiver, uint64_t code, uint64_t action) \
{ \
if (code == receiver || code == "eosio.token"_n.value) \
{ \
switch (action) \
{ \
EOSIO_DISPATCH_HELPER(TYPE, MEMBERS) \
} \
/* does not allow destructor of thiscontract to run: eosio_exit(0); */ \
} \
} \
} \
EOSIO_DISPATCH_EX(alice, (trans))
何も起こりませんでした。
まとめ
inline action
と比べると、通知の仕組みを活用するほうが、権限設定が要らないので、より安全に処理できそうです。