Advanced EOS Series — Part 9 — Payable Actions
The purpose of this series is to bring together the missing pieces you’ll need to complete your skills as a distributed application developer on the EOSIO blockchain. Each post is ordered by difficulty, so if you’ve just stumbled in here and feel lost, I’d recommend starting with Part 1 and working your way up. The full code for these examples can be found here on GitHub.
By now you’ve managed to achieve all kinds of functionality with your smart contract, but your still missing a fundamental piece, receiving and transferring value…
Let’s Get Paid
Things are different on the EOSIO network… payments are made to the eosio.token
contract, and not directly to the contract’s action like on other blockchains. This means we need to listen for payments being made to our contract and provide the appropriate actions. Don’t worry, it’s simpler than it sounds! We are going to listen to the eosio.token::transfer
notification sent by the eosio.token
contract. Simply define our handler method by replacing our regular [[eosio::action]
definition with [[eosio.token::on_notify(“eosio.token::transfer”)]]
. This notification is sent when a token transfer occurs which involves your contract’s account. Let’s breakdown the complete transfer notification handler.
[[eosio::on_notify("eosio.token::transfer")]] void purchase(name user, name receiver, asset value, string memo) { if (receiver != get_self() || caller == get_self()) return;
eosio::symbol token_symbol("SYM", 0);
eosio::check(value.amount > 0, "Insufficient value");
eosio::check(value.symbol == token_symbol, "Illegal asset symbol");
wallet_table balances(get_self(), get_self().value);
auto wallet = balances.find(caller.value);
if (wallet != balances.end()) {
balances.modify(wallet, get_self(), [&](auto &row) {
row.balance += value.amount;
});
} else {
balances.emplace(get_self(), [&](auto &row) {
row.account = caller;
row.balance = value.amount;
});
}
}
You’ll notice our method looks very similar to a regular action, and that’s because it is an action! The difference being, this action is called by an INLINE_ACTION
invoked by the eosio.token
contract, and not directly by public accounts.
Breaking it down
Let’s pull apart each line and look at what it’s doing. The method definition includes our subscription to the eosio.token::transfer
notification. We need to handle all the arguments passed along with the notification.
[[eosio::on_notify("eosio.token::transfer")]] void purchase(name user, name receiver, asset value, string memo) {
... contract code ...
}
The first argument is the eosio::name
value of the transfer sender, the second is the eosio::name value of the receiver. This should be your contracts account name when receiving funds, we will validate this in the next part. Finally we have the transfer value as an eosio::asset
and a memo std::string
.
Okay, now before we do anything, we first need to validate the sending and receiving participants are who we expected. It’s time to validate!
if (receiver != get_self() || caller == get_self()) return;
This line is our sanity check, first making sure our contract get_self()
is the intended receiver, and secondly, that our contract did not call this transfer on itself.
eosio::symbol token_symbol("SYM", 0);
eosio::check(value.amount > 0, "Insufficient value");
eosio::check(value.symbol == token_symbol, "Illegal asset symbol");
We will continue validating the transferred asset by first defining the token symbol
, then checking the sent value greater than zero, and it’s symbol
matches that which we expect. You should do any other kind of validation you’d like here. You might want to check the current time is within your funding period, or that the caller is a registered and active user.
Now we’ve validated our arguments, we can perform the actions we’d like to undertake.
wallet_table balances(get_self(), get_self().value);
auto wallet = balances.find(caller.value);
First we will allocate our wallet_table
and search for our caller
. Allot of this will now look similar to the previous sections on multi_index
tables.
if (wallet != balances.end()) {
balances.modify(wallet, get_self(), [&](auto &row) {
row.balance += value.amount;
});
} else {
balances.emplace(get_self(), [&](auto &row) {
row.account = caller;
row.balance = value.amount;
});
}
The last piece of code simply checks if the wallet was found (user’s transferred before), then either creates a new entry or updates the existing balance.
And that’s it! If your deploying to a local blockchain, you’ll need to also deploy the eosio.token
contract and then create and issue tokens before you can execute a transfer to your account. Please follow the official documentation if you need a step-by-step guide on interacting with the eosio.token
contract.
Debugging
You might run into pesky errors working with transfers and assets. I stumbled into the symbol precision mismatch
error, thrown when you attempt to issue or transfer assets using a different precision than was specified at creation. So if I created my asset with 10000.00 SYS
, then I’d need to issue and transfer using two decimal precision like 12.00 SYS
and not 12 SYS
.
What’s next?
Make sure you click the follow button if you would like to be notified when I share more examples in this series. Alternatively follow my GitHub profile, and don’t forget to clap and star to show appreciation.