2.6 添加Inline Actions

优质
小牛编辑
140浏览
2023-12-01

介绍

通过先前对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) {}

该函数非常简单,只接收一个namestring.

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...