Notifications

This contract adds the following capabilities to the previous examples:

  • Receives notifications from eosio.token and tracks user balances
  • Deducts from the user balance whenever the user buys a dog

This example does not cover:

  • Removing empty balance records
  • Returning excess funds to users
  • Protecting against dust attacks on the balance table
  • Treating incoming funds from system accounts as special (e.g. unstaking, selling rex, selling ram)

Place notify.cpp and CMakeLists.txt in an empty folder.

notify.cpp

#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>

namespace example
{
   // Keep track of deposited funds
   struct balance
   {
      eosio::name owner;
      eosio::asset balance;

      uint64_t primary_key() const { return owner.value; }
   };
   EOSIO_REFLECT(balance, owner, balance)
   typedef eosio::multi_index<"balance"_n, balance> balance_table;

   // A purchased animal
   struct animal
   {
      eosio::name name;
      eosio::name type;
      eosio::name owner;
      eosio::asset purchase_price;

      uint64_t primary_key() const { return name.value; }
   };
   EOSIO_REFLECT(animal, name, type, owner, purchase_price)
   typedef eosio::multi_index<"animal"_n, animal> animal_table;

   struct example_contract : public eosio::contract
   {
      using eosio::contract::contract;

      // eosio.token transfer notification
      void notify_transfer(eosio::name from,
                           eosio::name to,
                           const eosio::asset& quantity,
                           std::string memo)
      {
         // Only track incoming transfers
         if (from == get_self())
            return;

         // The dispatcher has already checked the token contract.
         // We need to check the token type.
         eosio::check(quantity.symbol == eosio::symbol{"EOS", 4},
                      "This contract does not deal with this token");

         // Record the change
         add_balance(from, quantity);
      }

      // Action: user buys a dog
      void buydog(eosio::name user, eosio::name dog, const eosio::asset& price)
      {
         require_auth(user);
         eosio::check(price.symbol == eosio::symbol{"EOS", 4},
                      "This contract does not deal with this token");
         eosio::check(price.amount >= 50'0000, "Dogs cost more than that");
         sub_balance(user, price);
         animal_table table{get_self(), get_self().value};
         table.emplace(user, [&](auto& record) {
            record.name = dog;
            record.type = "dog"_n;
            record.owner = user;
            record.purchase_price = price;
         });
      }

      // This is not an action; it's a function internal to the contract
      void add_balance(eosio::name owner, const eosio::asset& quantity)
      {
         balance_table table(get_self(), get_self().value);
         auto record = table.find(owner.value);
         if (record == table.end())
            table.emplace(get_self(), [&](auto& a) {
               a.owner = owner;
               a.balance = quantity;
            });
         else
            table.modify(record, eosio::same_payer, [&](auto& a) { a.balance += quantity; });
      }

      // This is not an action; it's a function internal to the contract
      void sub_balance(eosio::name owner, const eosio::asset& quantity)
      {
         balance_table table(get_self(), get_self().value);
         const auto& record = table.get(owner.value, "user does not have a balance");
         eosio::check(record.balance.amount >= quantity.amount, "not enough funds deposited");
         table.modify(record, owner, [&](auto& a) { a.balance -= quantity; });
      }
   };

   EOSIO_ACTIONS(example_contract,
                 "example"_n,
                 notify("eosio.token"_n, transfer),  // Hook up notification
                 action(buydog, user, dog, price))
}  // namespace example

EOSIO_ACTION_DISPATCHER(example::actions)

EOSIO_ABIGEN(actions(example::actions),
             table("balance"_n, example::balance),
             table("animal"_n, example::animal))

Additional files

Building

This will create notify.wasm and notify.abi:

mkdir build
cd build
cmake `clsdk-cmake-args` ..
make -j $(nproc)

Trying the contract

# Create some users
cleos create account eosio alice EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos create account eosio bob EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV

# Set up eosio.token
# Note: the build system created a symlink to clsdk for easy access to the token contract
cleos create account eosio eosio.token EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi eosio.token clsdk/contracts/token.abi
cleos set code eosio.token clsdk/contracts/token.wasm

cleos push action eosio.token create '["eosio", "1000000000.0000 EOS"]' -p eosio.token
cleos push action eosio.token issue '["eosio", "1000000000.0000 EOS", ""]' -p eosio
cleos push action eosio.token open '["alice", "4,EOS", "alice"]' -p alice
cleos push action eosio.token open '["bob", "4,EOS", "bob"]' -p bob
cleos push action eosio.token transfer '["eosio", "alice", "10000.0000 EOS", "have some"]' -p eosio
cleos push action eosio.token transfer '["eosio", "bob", "10000.0000 EOS", "have some"]' -p eosio

# Install the contract
cleos create account eosio notify EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
cleos set abi notify notify.abi
cleos set code notify notify.wasm

# Try out the contract
cleos push action eosio.token transfer '["alice", "notify", "300.0000 EOS", "for purchases"]' -p alice
cleos push action eosio.token transfer '["bob", "notify", "300.0000 EOS", "for purchases"]' -p bob

cleos push action notify buydog '["alice", "fido", "100.0000 EOS"]' -p alice
cleos push action notify buydog '["alice", "rex", "120.0000 EOS"]' -p alice
cleos push action notify buydog '["bob", "lambo", "70.0000 EOS"]' -p bob

# See the remaining balances and the purchased animals
cleos get table notify notify balance
cleos get table notify notify animal