Using AtomicAssets in a Smart Contract
Ivan Montiel
Posted on August 6, 2023
Introduction
Rather than interact with our AtomicAssets using the command line, we’ll encapsulate a lot of functionality into a Smart Contract. Our goal will be to create a contract that lets users send WAX to the contract and then we will mint an egg NFT for that user.
Creating the project
It’s been a while since we wrote a Smart Contract. Let’s review the commands to get started.
First let’s sh into the docker container:
docker exec -it <CONTAINER_ID> /bin/sh
Now that we are in the container, we can create a new project. We’ll call this one babychicks:
cd wax
eosio-init -project babychicks
Using AtomicAssets from our Smart Contract
Before we start implementing our contract’s header, we will need to add a bit of information about Atomic Assets to our contract.
In order for our contract to mint an NFT, we will need to send an action to the AtomicAssets contract. The name of that contract, the format of the data, and the datatypes will need to be included in our Smart Contract for this to work.
The full definition for the atomicassets contract can be viewed on Github. We will only need a subset of that definition for our purposes though. We won’t go through this line by line since we just need this to get started with our contract.
atomicassets.hpp
#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>
using namespace eosio;
/**
* @brief Namespace for the atomicassets contract
* Copied from:
https://github.com/pinknetworkx/atomicassets-contract/blob/master/include/atomicassets-interface.hpp
*/
namespace atomicassets {
static constexpr name ATOMICASSETS_ACCOUNT = name("atomicassets");
// Custom vector types need to be defined because otherwise a bug in the ABI
// serialization would cause the ABI to be invalid
typedef std::vector<int8_t> INT8_VEC;
typedef std::vector<int16_t> INT16_VEC;
typedef std::vector<int32_t> INT32_VEC;
typedef std::vector<int64_t> INT64_VEC;
typedef std::vector<uint8_t> UINT8_VEC;
typedef std::vector<uint16_t> UINT16_VEC;
typedef std::vector<uint32_t> UINT32_VEC;
typedef std::vector<uint64_t> UINT64_VEC;
typedef std::vector<float> FLOAT_VEC;
typedef std::vector<double> DOUBLE_VEC;
typedef std::vector<std::string> STRING_VEC;
typedef std::variant<
int8_t,
int16_t,
int32_t,
int64_t,
uint8_t,
uint16_t,
uint32_t,
uint64_t,
float,
double,
std::string,
INT8_VEC,
INT16_VEC,
INT32_VEC,
INT64_VEC,
UINT8_VEC,
UINT16_VEC,
UINT32_VEC,
UINT64_VEC,
FLOAT_VEC,
DOUBLE_VEC,
STRING_VEC>
ATOMIC_ATTRIBUTE;
typedef std::map<std::string, ATOMIC_ATTRIBUTE> ATTRIBUTE_MAP;
} // namespace atomicassets
The above C++ code defines the types of data we will need as well as the contract name we will interact with. Add this file to your ./babychicks/includes
folder.
Defining our Contract
Next let’s implement our header. First, let’s get the external files we need defined:
#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>
#include <eosio/system.hpp>
#include "atomicassets.hpp"
Next, we’ll define some static constants that we will use in our contract.
using namespace eosio;
namespace babychick_constants {
static constexpr symbol CORE_TOKEN_SYMBOL = symbol("WAX", 0);
static constexpr uint64_t EGG_PRICE_IN_WAX = 2000;
static constexpr name COLLECTION_NAME = name("babychicks");
static constexpr name EGG_SCHEMA_NAME = name("chickegg");
static constexpr uint32_t EGG_TEMPLATE_ID = 629366;
} // namespace babychick_constants
The CORE_TOKEN_SYMBOL
is the definition for what token we will accept and the precision. Since you can send fractional amount of tokens, it is important that you understand how the precision works. You can read more on the precision type on the Eosio docs. We set the precision to 0 since we don’t want to deal with fractional amounts of WAX.
Next. we have EGG_PRICE_IN_WAX
. This is the amount WAX we expect to be transferred to our contract in order to mint an egg. It is also in the given precision as our symbol.
The next two constants COLLECTION_NAME
and EGG_SCHEMA_NAME
are the names we gave our collection and schema when we created them in AtomicAssets.
Last, is the EGG_TEMPLATE_ID
which you will need to update to the template id AtomicAssets gave your template when we created it.
Next, we’ll build on the template the WAX utility created when we generated our project:
CONTRACT babychicks : public contract {
public:
using contract::contract;
// We will implement our public actions here
private:
// We will implement our private methods here
};
Let’s start with the most complicated public action:
[[eosio::on_notify("eosio.token::transfer")]] void receive_token_transfer(
name from, name to, asset quantity, std::string memo
);
Here we define a notify action. The method we define will be called when someone transfers a token to our contract. Since this is a catch all for any token defined on the WAX blockchain, we will need to add some checks when we implement the method. We wouldn’t want someone minting their own token and sending it to us.
Next, we define a utility to help us airdrop tokens to users:
ACTION airdropegg(name from, name receiver);
using airdropegg_action =
action_wrapper<"airdropegg"_n, &babychicks::airdropegg>;
This method will mint an egg and airdrop it to the receiving wallet.
For the private methods, we’ll have two utilities that we will rely on: mint_egg_check
and mint_egg
.
The first will perform validation checks before calling the second. You can think of mint_egg_check
as a guard check around our mint_egg
function, making sure that we don’t mint egg NFTs when we aren’t supposed to. This pattern is very common in Ethereum Solidity contracts, and you can read more about Guard Checks. I find the pattern very useful, so I use it in WAX Smart Contracts as well.
void mint_egg_check(name receiver, asset quantity);
void mint_egg(name receiver);
Putting it all together:
#include <eosio/asset.hpp>
#include <eosio/eosio.hpp>
#include <eosio/system.hpp>
#include "atomicassets.hpp"
using namespace eosio;
namespace babychick_constants {
static constexpr symbol CORE_TOKEN_SYMBOL = symbol("WAX", 8);
// Convert 2000 WAXP to asset precision
static constexpr uint64_t EGG_PRICE_IN_WAX = 200000000;
static constexpr name COLLECTION_NAME = name("babychicks");
static constexpr name EGG_SCHEMA_NAME = name("chickegg");
static constexpr std::string_view EGG_TEMPLATE_ID = "1234";
} // namespace babychick_constants
CONTRACT babychicks : public contract {
public:
using contract::contract;
[[eosio::on_notify("eosio.token::transfer")]] void receive_token_transfer(
name from, name to, asset quantity, std::string memo
);
ACTION airdropegg(name from, name receiver);
using airdropegg_action =
action_wrapper<"airdropegg"_n, &babychicks::airdropegg>;
private:
void mint_egg_check(name receiver, asset quantity);
void mint_egg(name receiver);
};
Implementing the mint function
We are going to work up the callstack here and start with the function that actually mints a ChickEgg. The implementation for mint_egg
that we defined in the header will use the Egg Template ID that we created and send an action to the AtomicAssets contract to mint an asset of that template ID and give it to the receiver.
/**
* @brief Mint an egg and send it to the sender
*
* @param receiver
*/
void babychicks::mint_egg(name receiver) {
std::string templateId(babychick_constants::EGG_TEMPLATE_ID);
action(
permission_level{get_self(), name("active")},
atomicassets::ATOMICASSETS_ACCOUNT,
name("mintasset"),
std::make_tuple(
get_self(),
babychick_constants::COLLECTION_NAME.value,
babychick_constants::EGG_SCHEMA_NAME.value,
babychick_constants::EGG_TEMPLATE_ID,
receiver,
// immutable data
(atomicassets::ATTRIBUTE_MAP){{"created_at", (uint64_t)now()}},
// mutable data
(atomicassets::ATTRIBUTE_MAP){},
// backing assets
(std::vector<asset>){}
)
)
.send();
}
Above, we use action
to call an action in a contract that isn’t ours. We call the AtomicAssets contract with the action mintasset
. All the options we send should look familiar since this is just the C++ implementation of the manual minting we did using JavaScript.
Adding a Mint Check
Before we call the mint function though, we should add additional checks to our Smart Contract to ensure that we don’t incorrectly mint an NFT for someone who shouldn’t get one. That’s where our mint_egg_check
function comes in as a guard for our mint_egg
function:
/**
* @brief Check if the quantity is valid and mint an egg
* If the quantity is valid, mint an egg and send it to the sender
*
* @param receiver
* @param quantity
*/
void babychicks::mint_egg_check(name receiver, asset quantity) {
// Check if the quantity is valid
if (quantity.symbol != babychick_constants::CORE_TOKEN_SYMBOL ||
quantity.amount != babychick_constants::EGG_PRICE_IN_WAX) {
check(false, "invalid quantity");
}
// Mint an egg and send it to the sender
mint_egg(receiver);
}
Here we check that the correct WAX token was sent to us and the correct quantity was sent. If neither is correct, then we will throw an exception.
Implementing receive_token_transfer
Next let’s wire it all up using receive_token_transfer
. When a user sends WAX to our Smart Contract, this method will be called. There are some special types of transfers that we need to ignore, but in general, the method is pretty straightforward:
/**
* @brief Listen for WAX transfers to this contract
*
* @param from
* @param to
* @param quantity
* @param memo
*/
void babychicks::receive_token_transfer(
name from, name to, asset quantity, std::string memo
) {
// Ignore EOSIO system account transfers
const std::set<name> ignore = std::set<name>{
name("eosio.stake"),
name("eosio.names"),
name("eosio.ram"),
name("eosio.rex"),
name("eosio")};
// Ignore transfers not to this contract
if (to != get_self() || ignore.find(from) != ignore.end()) {
return;
}
if (memo == "egg") {
mint_egg_check(from, quantity);
} else {
check(false, "invalid memo");
}
}
The main caveat here is that we expect the user to send our contract WAX with a specific memo. If the memo is not expected, we fail, which will cancel the WAX transfer.
Airdrops
Last, let’s implement the airdropegg
function. This function should only be callable by the Smart Contract account itself. After checking that the caller has auth on behalf of the contract, we can mint the egg for the given receiver:
/**
* @brief Let the owner of the contract airdrop Egg NFTs to wallets
*
* @param receiver
*/
ACTION babychicks::airdropegg(name receiver) {
require_auth(get_self());
// Make sure the receiver is valid
check(is_account(receiver), "invalid receiver");
mint_egg(receiver);
}
Building
Let’s build the contract to verify that it compiles:
( cd ./build && cmake .. && make )
If you need to rebuild the contract, you may want to delete the build targets to ensure that the build is up to date:
rm ./build/babychicks/CMakeFiles/babychicks.dir/babychicks.obj
Conclusion
In this section, we went over the AtomicAssets contract and the definitions that we will need in our contract. We defined our contract interface and added implementations for those functions.
Next, we’ll deploy and interact with our contract.
Next post: Interacting with Our NFTs
E-book
Get this entire WAX tutorial as an e-book on Amazon.
Additional links
- Github: https://github.com/CapsuleCat/wax-nft-tutorial/tree/main/babychicks
- Photo by SpaceX on Unsplash
Posted on August 6, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.