Before we Begin

Terminal

Throughout this guide we will be referring to the CLI (Command Line Interface) as “Terminal.” Depending upon your system of choice, your “Terminal” will be located in one of three places.

  1. If on Mac OS X or Linux, open the application “Terminal”
  2. If using Windows Subsystem for Linux your “Terminal” is the Ubuntu 18.04 application.
  3. If using a VM through VirtualBox on older versions of Windows 10, your terminal is the “Terminal” application inside the Ubuntu 18.04 VM.

Please open your “Terminal” now.

C++ coding environment setup

We can use any text editor that, preferably, supports C++ highlighting. If you already have an editor/IDE that you are familiar and comfortable with, please use it.

However, if you do not presently have an Editor, we suggest downloading VS Code, it’s free, simple to use and works well with C++ development.

Remember: If you’re using Ubuntu from a VirtualBox Virtual Machine, download your editor from inside the VM

Create a Project Directory

You’ll need a project to work from for the duration of this guide. To make things easy, we’re asking that you follow our directory naming exactly, as it will enable you to follow our guide far more easily.

Windows Subsystem for Linux

From your Ubuntu 18.04 terminal, enter the following

mkdir /mnt/c/VTBootCamp
cd ~
ln -s /mnt/c/VTBootCamp
cd ~/VTBootCamp

Some instructions below will ask you to create and modify files. You can either use a terminal based editor (vim, nano, etc) within the Ubuntu 18.04 terminal, or you can use a GUI based editor within Windows. All the files can be found at C:\VTBootCamp. You may edit them there, and then resume the tutorial in the Ubuntu 18.04 terminal.

MacOS and Ubuntu 18.04

Enter the following into terminal

mkdir ~/VTBootCamp

to create the VTBootCamp directory in your home directory.

Change to the home directory.

cd ~/VTBootCamp

Start your dev environment:

Download VT Blockchain Bootcamp Starter Kit

The starter kit repository contains an example contract, a frontend that exposes the functionality of the contract and some convenience scripts for managing the blockchain. For more information about the starter kit, see the GitHub Repo.

cd ~/VTBootCamp
git clone https://github.com/EOSIO/vt-blockchain-bootcamp-starter.git
cd vt-blockchain-bootcamp-starter

First Time Setup

To install EOSIO binaries and their dependencies run the first_time_setup.sh script located in the vt-blockchain-bootcamp-starter directory.

If on Mac OS X

./first_time_setup.sh

If on Ubuntu 18.04

sudo ./first_time_setup.sh

Enter your system password when the password prompt appears.

Start the Blockchain

Next, start the blockchain.

./start_blockchain.sh

This script creates all the accounts and sets up the wallet for most of the accounts used within this guide.

Once you see the “EOSIO Blockchain Started” message, your EOSIO Node (nodeos) is successfully started.

Example Application

Inside of the starter kit are the components of a very simple example application. This application, “NoteChain,” allows users to create and update notes.

Open a new terminal window, and start the frontend server.

If you’re using Ubuntu 18.04 on Windows Subsystem for Linux simply open another Ubuntu 18.04 application window.

cd ~/VTBootCamp/vt-blockchain-bootcamp-starter
./start_frontend.sh

After a short setup process your browser should automatically open a new tab on http://localhost:3000/

NoteChain GUI

The lower-half of the interface contains accounts, public keys and private keys for users that were created when you ran the start_blockchain.sh script for the first time.

Copy one of the example account’s information into the UI of the NoteChain application.

Add some text in the “Note” field and press ‘Add/Update Note’. As a result you should see the note appear at the top of the page.

More in-depth documentation for the example app with additional commands can be found here: https://github.com/EOSIO/vt-blockchain-bootcamp-starter

Cleos

Open a new terminal window and execute the following

cd ~/VTBootCamp

cleos is a command line interface (CLI) to interact with the blockchain and to manage wallets.

Execute cleos --help in your terminal to get a top-level help text. You can also just call cleos or cleos subcomand without any parameters to output help text. For example, cleos wallet will output help text in the context of the wallet.

If you would like to view the command reference for cleos, you can find it here: https://developers.eos.io/eosio-cleos/reference.

Before getting to the next section, please also read https://developers.eos.io/eosio-nodeos/docs/accounts-and-permissions to familiarize yourself with the concepts of accounts, wallets and permissions in EOSIO.

Wallets

The wallet can be thought of as a repository of public-private key pairs. These are needed to sign actions performed on the blockchain. Wallets and their content are managed by keosd. Wallets are accessed using cleos.

Create our first wallet:

cleos wallet create --to-console

The output of this command will give you a password. Save this password - you will need it throughout the remainder of this guide.

To work with a wallet, first open and unlock it.

cleos wallet open
cleos wallet unlock

Moments ago you save your password, enter it when prompted.

Next create a key in your wallet.

cleos wallet create_key
Created new private key with a public key of: "EOS74GhNdMRYtej..."

Your key is now imported into the wallet. We will be using this key a few more times throughout the guide, so copy it somewhere easy to access. For convenience, store it as a variable:

PUBLICKEY="the-public-key-from-above"
cleos wallet list

The output should be following:

Wallets:
[
  "default *",
  "eosiomain *",
  "notechainwal *"
]

The * shows which wallets are open.

cleos wallet keys

This will output your public keys:

[
  "EOS74GhNdMRYtejhr1mBBTkK21x33thf4cD2i3ndfeNnBq9s72WK5"
  ...
]

Every EOSIO blockchain has a default user, called eosio. This account has full privileges over the network, and can essentially do whatever it wants. On a public network, control over this account is resigned as one of prerequisite signals that any particular EOSIO blockchain is sufficient for public use. For development purposes eosio’s control is retained to enable more efficient development processes.

We’ll need to import the eosio account’s private key so we can sign transactions on it’s behalf. Run the following in your available terminal window and press enter,.

cleos wallet import

You’ll be presented with a password prompt, copy the key below and paste it into the prompt.

5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3

Important The key above is a publicly known private key. You should never use this key on a production network.

User account

Now let’s create some user accounts.

To create a new account on your single-node testnet, use cleos create account as demonstrated below.

cleos create account eosio helloworld $PUBLICKEY -p eosio@active

Let’s explore what just happened in the interaction between cleos (your CLI client) and keosd (the wallet process):

We can view that this process was success executed by calling the following.

cleos get account helloworld -j

You’ll now recieve a response that outlines the account’s details.

“Hello World” Smart Contract

This first contract is extremely simple. It contains a single action that accepts a single parameter as an argument. The argument is used to print out a message in the log of the nodeos process. Your nodeos process was started when you executed start_blockchain.sh earlier in this guide.

The helloworld contract is useless by design. It’s primary purpose is to demonstrate the motions of authoring, compiling and deploying the contract to an EOSIO blockchain.

We need to navigate to the directory you created earlier.

cd ~/VTBootCamp

Next, create and navigate into a directory to store the contract.

mkdir helloworld
cd helloworld

Create the file that will contain the logic for our simple contract.

touch helloworld.cpp

Open helloworld.cpp file in your editor and paste following code.

Note: If you’re using WSL on Windows 10, then open the file with Windows Explorer, if you’re using a VM on Windows 10, stay inside you VM.

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>

using namespace eosio;

class [[eosio::contract]] helloworld : public contract {
  public:
      using contract::contract;

      [[eosio::action]]
      void hi( name user ) {
         print( "Hello, ", user);
      }
};
EOSIO_DISPATCH( helloworld, (hi))

Let’s go through this contract piece by piece.

First, we need to include the EOSIO libraries necessary to expose the smart contract C++ APIs, as well EOSIO’s print wrapper.

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>

EOSIO contracts extend the contract class.

class [[eosio::contract]] helloworld : public contract {
  public:
      using contract::contract;
};

This is a standard implementation of a contract structure that has one method called hi that takes a user parameter of type name. Then it prints out a name of this user.


  [[eosio::action]]
  void hi( name user ) {
     print( "Hello, ", user);
  }
EOSIO_DISPATCH( helloworld, (hi))

EOSIO_DISPATCH is a C++ macro that expands into a dispatcher. Requests to a smart contract are sent to compiled WASM as a binary blob which are then unpacked, and routed to your action based on the logic inside your pre-compilation smart contract logic. ABI files are a programatic and portable representation of the actions and data types accepted by your smart contract. An ABI simplifies the process of interfacing with any provided smart contract.

eosio.cdt

eosio.cdt is a toolchain for WebAssembly (WASM) and set of tools to facilitate contract writing and compilation for the EOSIO platform. We installed eosio.cdt for you when you ran the first_time_setup.sh script.

Compile the smart contract

First we need to generate a WASM file. A WASM file is a compiled smart contract ready to be uploaded to EOSIO network.

eosio-cpp is the WASM compiler and an ABI generator utility. Before uploading the smart contract to the network we will need to compile it from C++ to WASM.

eosio-cpp -o ~/VTBootCamp/helloworld/helloworld.wasm ~/VTBootCamp/helloworld/helloworld.cpp --abigen --contract helloworld

Run the following:

ls ~/VTBootCamp/helloworld

Now in the folder ~/VTBootCamp/helloworld you will see three files:

helloworld.cpp  # this is source code of the example contract
helloworld.abi  # this is the ABI file - describes the interface of the smart contract
helloworld.wasm # this is the compiled WASM file

Congratulations. You have created your first smart contract. Time to deploy this contract to the blockchain.

cleos set contract helloworld ~/VTBootCamp/helloworld --permission helloworld@active

Run the transaction:

cleos push action helloworld hi '["helloworld"]' -p helloworld@active

EOSIO token contract

EOSIO has a standard token interface that we’ll now explore. But before we begin, we’ll need pull that source from the repository

cd ~/VTBootCamp
git clone https://github.com/EOSIO/eosio.contracts.git

Change directories…

cd eosio.contracts/eosio.token

First, we need to create an account for the contract. Earlier, you created a variable named PUBLICKEY with the public key. We’ll use that again. If you cannot find it, no worries, just use cleos wallet list to list your public keys.

cleos create account eosio eosio.token $PUBLICKEY

Next we need to compile the eosio.token contract. Enter the following in your terminal:

eosio-cpp -I include -o eosio.token.wasm src/eosio.token.cpp --abigen

If you’re curious about the parameters used for eosio.cdt you can use eosio-cpp -help or view the eosio-cpp reference documentation

Then we need to deploy the eosio.token smart contract:

cleos set contract eosio.token ~/VTBootCamp/eosio.contracts/eosio.token -p eosio.token

Once that is complete, issue the a token. We’re going to call this token “SYS”.

cleos push action eosio.token create '{"issuer":"eosio", "maximum_supply":"1000000000.0000 SYS"}' -p eosio.token@active

This command created a new token SYS with a precision of 4 decimals and a maximum supply of 1000000000.0000 SYS. We pass -p eosio.token@active to inform cleos to tell keosd to sign the transaction using a key that authorizes with the active permission of the eosio.token account.

Issue Tokens to Account “helloworld”

Now that we have created the token, the issuer (eosio) can issue new tokens to the user account we created earlier.

cleos push action eosio.token issue '[ "helloworld", "100.0000 SYS", "memo" ]' -p eosio@active

This time the output contains several different actions: one issue and three transfers. While the only action we signed was issue, the issue action performed an “inline transfer” and the “inline transfer” notified the sender and receiver accounts. The output indicates all of the action handlers that were called, the order they were called in, and whether or not any output was generated by the action.

Check helloworld’s balance now:

cleos get table eosio.token helloworld accounts

You should see following output:

{
  "rows": [{
      "balance": "100.0000 SYS"
    }
  ],
  "more": false
}

Now, send some tokens to another user:

cleos push action eosio.token transfer '[ "helloworld", "bob", "25.0000 SYS", "m" ]' -p helloworld@active

Nailed it! Let’s check the balance is correct:

cleos get table eosio.token bob accounts

Should give you:

{
  "rows": [{
      "balance": "25.0000 SYS"
    }
  ],
  "more": false
}
cleos get table eosio.token helloworld accounts

Should give you:

{
  "rows": [{
      "balance": "75.0000 SYS"
    }
  ],
  "more": false
}

Awesome! Let’s move to the next part.

Persistence API

Now we want to store our information in a table-like structure, similar to a database.

Let’s imagine we are building an address book where users can add their social security number, age and name.

First, create a directory:

cd ~/VTBootCamp
mkdir addressbook
cd addressbook

And create a new .cpp file:

touch addressbook.cpp

Now open this file in your code editor.

Let’s create the standard structure for a contract within the file:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>

using namespace eosio;

class addressbook : public eosio::contract {
   public:

   private:

};

Before a table can be configured and instantiated, a struct that represents the data structure of the address book needs to be written. Since it’s an address book, the table will contain people, so create a struct called person under the private section of the addressbook class.

struct person {};

When defining the struct for a multi_index table, you will require a unique value to use as the primary key.

For this contract, use a field called key with type name. This contract will have one unique entry per user, so this key will be a consistent and guaranteed unique value based on the user’s name.

struct person {
   name key;
};

Since this contract is an address book it probably should store some relevant details for each entry or person.

struct person {
   name        key;
   std::string full_name;
   std::string street;
   std::string city;
   uint32_t    phone;
};

The data structure for person is now complete. Next, define a primary_key member function, which will be used by multi_index class. Every struct to be used with multi_index requires a primary key. To accomplish this you simply create a member function called primary_key() that returns a value of type uint64_t, in this case, the raw value representation of the key member field in the person struct.

struct person {
   name        key;
   std::string full_name;
   std::string street;
   std::string city;
   uint32_t    phone;

   uint64_t primary_key() const { return key.value; }
};

Note: A table’s data structure cannot be modified while it has data in it. If you need to make changes to a table’s schema in any way, you first need to remove all its rows. Thus, it’s important to design your multi_index data structures carefully.

The code thus far should look like the following:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>

using namespace eosio;

class addressbook : public eosio::contract {
public:
   using contract::contract;

private:
   struct person {
      name        key;
      std::string full_name;
      std::string street;
      std::string city;
      uint32_t    phone;

      uint64_t primary_key() const { return key.value; }
   };
};

Now that the data structure of the table has been defined with a struct, we need to configure the table. The eosio::multi_index constructor needs to be named and configured to use the struct we previously defined. We use the name people to refer to the table.

// We setup the table using multi_index container:
using addressbook_type = eosio::multi_index<"people"_n, person>;

We need to initialize the class in the constructor and pass the name as a parameter in the constructor. This name will be set to the account that deploys the contract.


addressbook(name receiver, name code, datastream<const char*> ds) : contract(receiver, code, ds) {}

Let’s sum it all up in one file so far:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>

using namespace eosio;

class addressbook : public eosio::contract
{
public:
   using contract::contract;

   addressbook(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds) {}

private:
   struct person {
      name        key;
      std::string full_name;
      std::string street;
      std::string city;
      uint32_t    phone;

      uint64_t primary_key() const { return key.value; }
   };

   using addressbook_type = eosio::multi_index<"people"_n, person>;
};

Next, define an action for the user to add or update a record. This action will need to accept any values that this action needs to be able to emplace (create) or modify.

void upsert(name user, std::string full_name, std::string street, std::string city, uint32_t phone) {
}

Earlier, it was mentioned that only the user has control over their own record, as this contract is opt-in. To do this, utilize the require_auth method provided by the eosio.cdt library.

This method accepts one argument, a name type, and asserts that the account executing the transaction equals the provided value. If this assertion fails, i.e. the user provided as an argument in upsert is not the authorizing user, the action will undo all side-effects that have occurred in the transaction up to that point before failing.

void upsert(name user, std::string full_name, std::string street, std::string city, uint32_t phone) {
   require_auth( user );
}

We can add other checks on the inputs using the eosio::check function. Again, if any of the assertions fail, all side-effects that have occurred so far will be undone and the entire transaction will be rejected.

void upsert(name user, std::string full_name, std::string street, std::string city, uint32_t phone) {
   require_auth( user );
   check(full_name.size() <= 30, "Full name is too long");
   check(street.size() <= 30, "Street name is too long");
   check(city.size() <= 20, "City name is too long");
}

Instantiate the table. Earlier, a multi_index table was configured and given a type alias of addressbook_type. To instantiate this table, consider its two required arguments:

void upsert(name user, std::string full_name, std::string street, std::string city, uint32_t phone) {
   require_auth( user );
   check(full_name.size() <= 30, "Full name is too long");
   check(street.size() <= 30, "Street name is too long");
   check(city.size() <= 20, "City name is too long");

   addressbook_type addresses(get_self(), get_self().value);
}

Next, use the find function to lookup the table record that has a primary key equivalent to user.value. The return value of the find function is an iterator that we will store as a variable called iterator since this iterator may be used several times.

void upsert(name user, std::string full_name, std::string street, std::string city, uint32_t phone) {
   require_auth( user );
   check(full_name.size() <= 30, "Full name is too long");
   check(street.size() <= 30, "Street name is too long");
   check(city.size() <= 20, "City name is too long");

   addressbook_type addresses(get_self(), get_self().value);
   auto iterator = addresses.find(user.value);
}

Now our function needs to actually add or update the record (if it already exists) in the table:

if( iterator == addresses.end() )
{
   // The user isn't in the table
}
else
{
   // The user is in the table
}

If the record doesn’t exist, we need to create it. To do this use the emplace function:

addresses.emplace(user, [&]( auto& row ) {
   row.key       = user;
   row.full_name = full_name;
   row.street    = street;
   row.city      = city;
   row.phone     = phone;
});

If it already exists - we will update it using modify function:

addresses.modify(iterator, user, [&]( auto& row ) {
   row.key       = user;
   row.full_name = full_name;
   row.street    = street;
   row.city      = city;
   row.phone     = phone;
});

Let’s put it all together:

#include <eosiolib/eosio.hpp>
#include <eosiolib/print.hpp>

using namespace eosio;

class addressbook : public eosio::contract
{
public:
   using contract::contract;

   addressbook(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds) {}

   void upsert(name user, std::string full_name, std::string street, std::string city, uint32_t phone)
   {
      require_auth(user);
      check(full_name.size() <= 30, "Full name is too long");
      check(street.size() <= 30, "Street name is too long");
      check(city.size() <= 20, "City name is too long");

      addressbook_type addresses(get_self(), get_self().value);
      auto iterator = addresses.find(user.value);

      if (iterator == addresses.end())
      {
         // The user isn't in the table
         addresses.emplace(user, [&](auto &row) {
            row.key       = user;
            row.full_name = full_name;
            row.street    = street;
            row.city      = city;
            row.phone     = phone;
         });
      }
      else
      {
         // The user is in the table
         addresses.modify(iterator, user, [&](auto &row) {
            row.key       = user;
            row.full_name = full_name;
            row.street    = street;
            row.city      = city;
            row.phone     = phone;
         });
      }
   }

private:
   struct person {
      name        key;
      std::string full_name;
      std::string street;
      std::string city;
      uint32_t    phone;

      uint64_t primary_key() const { return key.value; }
   };

   using addressbook_type = eosio::multi_index<"people"_n, person>;
};

We also may want to add the erase method. Please remember it doesn’t remove the record from the history. However, it does remove it from the current state of the database, freeing resources if you are on a resource-limited network. Presently, you are on a single-node local testnet that does not have resource-limitations imposed.

void erase(name user) {
   require_auth(user);

   addressbook_type addresses(get_self(), get_self().value);

   auto iterator = addresses.find(user.value);
   check(iterator != addresses.end(), "Record does not exist");
   addresses.erase(iterator);
}

The contract is now mostly complete. Users can create, modify and erase records. However, the contract is not quite ready to be compiled.

At the bottom of the file, utilize the EOSIO_DISPATCH macro, passing the name of the contract, and our actions upsert and erase.

This macro handles the apply handlers used by WASM to dispatch calls to specific actions in our contract.

Adding the following to the bottom of addressbook.cpp will make our cpp file compatible with EOSIO’s WASM interpreter. Failing to include this declaration may result in an error when deploying the contract.

EOSIO_DISPATCH( addressbook, (upsert)(erase))

ABI Type Declarations

eosio.cdt includes an ABI Generator, but for it to work will require some minor declarations to our contract.

There are three main types of the ABI annotation that you need to use in order for the ABI Generator to recognize relevant functions and export them to the ABI file correctly:

[[eosio::contract]] # This annotation is needed at the contract class definition
[[eosio::action]] # This annotation is needed on the publicly available functions
[[eosio::table]] # This annotation is needed for the multi index table structs

The final version of our file will look like this:

#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 full_name, std::string street, std::string city, uint32_t phone)
   {
      require_auth(user);
      check(full_name.size() <= 30, "Full name is too long");
      check(street.size() <= 30, "Street name is too long");
      check(city.size() <= 20, "City name is too long");

      addressbook_type addresses(get_self(), get_self().value);
      auto iterator = addresses.find(user.value);

      if (iterator == addresses.end())
      {
         // The user isn't in the table
         addresses.emplace(user, [&](auto &row) {
            row.key       = user;
            row.full_name = full_name;
            row.street    = street;
            row.city      = city;
            row.phone     = phone;
         });
      }
      else
      {
         // The user is in the table
         addresses.modify(iterator, user, [&](auto &row) {
            row.key       = user;
            row.full_name = full_name;
            row.street    = street;
            row.city      = city;
            row.phone     = phone;
         });
      }
   }

   [[eosio::action]]
   void erase(name user) {
      require_auth(user);

      addressbook_type addresses(get_self(), get_self().value);

      auto iterator = addresses.find(user.value);
      check(iterator != addresses.end(), "Record does not exist");
      addresses.erase(iterator);
   }

private:
   struct [[eosio::table]] person {
      name        key;
      std::string full_name;
      std::string street;
      std::string city;
      uint32_t    phone;

      uint64_t primary_key() const { return key.value; }
   };

   using addressbook_type = eosio::multi_index<"people"_n, person>;
};

EOSIO_DISPATCH( addressbook, (upsert)(erase))

We have our table, let’s test it now. First, we need to create couple of accounts.

Create a user account so we can test adding contact details to the address book.

cleos create account eosio josh $PUBLICKEY

Create another account, this one will be used to store the smart contract for the address book.

cleos create account eosio addressbook $PUBLICKEY

Now we need to compile the smart contract:

eosio-cpp -o ~/VTBootCamp/addressbook/addressbook.wasm ~/VTBootCamp/addressbook/addressbook.cpp --abigen --contract addressbook

Next, we need to upload the smart contract:

cleos set contract addressbook ~/VTBootCamp/addressbook -p addressbook@active

And let’s add josh to the database:

cleos push action addressbook upsert '["josh", "Joshua A", "Springfield St", "San Francisco, CA", 123456]' -p josh

Looks good! Is Josh in?

cleos get table addressbook addressbook people

The result should look like this:

{
  "rows": [{
      "key": "josh",
      "full_name": "Joshua A",
      "street": "Springfield St",
      "city": "San Francisco, CA",
      "phone": 123456
    }
  ],
  "more": false
}

What if we now need to update his address?

cleos push action addressbook upsert '["josh", "Joshua A", "Market St", "San Francisco, CA", 123456]' -p josh

You should get the following result by repeating the previous cleos get table command:

{
  "rows": [{
      "key": "josh",
      "full_name": "Joshua A",
      "street": "Market St",
      "city": "San Francisco, CA",
      "phone": 123456
    }
  ],
  "more": false
}