DEV Community

Cover image for Using AtomicAssets in a Smart Contract
Ivan Montiel
Ivan Montiel

Posted on • Updated on

Using AtomicAssets in a Smart Contract

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
);

Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Building

Let’s build the contract to verify that it compiles:

( cd ./build && cmake .. && make )
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)