/ blockchain

EOSIO Blockchain dApp Step by Step: Part 2 - Smart Contract

Preface

This is the 2nd part of the blog: EOSIO dApp on Blockchain Step-by-Step: Part 1. The last blog described the EOSIO setup processes steps-by-steps. This part will develop a Smart Contract for EOSIO platform.

Development

The purpose of the smart contract is to simulate an election. I created an EOSIO user to host the smart contract. Created two citizen users to vote a candidate. The voting records keep saving in the EOSIO blockchain. In this example, all operations are operating in command mode. Let's get started.

Develop the Smart Contract

EOSIO executes the smart contract which is developed in WebAssembly standard. So I developed the election smart contract in C++. Below is the full source code of election.cpp:

#include <eosiolib/eosio.hpp>

using namespace eosio;

class election : public contract
{
private:
  // create the multi index tables to store the data

  /// @abi table
  struct candidate {
    uint64_t _key;       // primary key
    std::string _name;   // candidate name
    uint32_t _count = 0; // voted count

    uint64_t primary_key() const { return _key; }
  };
  typedef eosio::multi_index<N(candidate), candidate> candidates;

  /// @abi table
  struct voter {
    uint64_t _key;
    uint64_t _candidate_key; // name of poll
    account_name _account;   // this account has voted, avoid duplicate voter

    uint64_t primary_key() const { return _key; }
    uint64_t candidate_key() const { return _candidate_key; }
  };
  typedef eosio::multi_index<N(voter), voter, indexed_by<N(_candidate_key), const_mem_fun<voter, uint64_t, &voter::candidate_key>>> voters;

  // local instances of the multi indexes
  candidates _candidates;
  voters _voters;
  uint64_t _candidates_count;

public:
  election(account_name s) : contract(s), _candidates(s, s), _voters(s, s), _candidates_count(0) {}

  // public methods exposed via the ABI
  // on candidates

  /// @abi action
  void version() {
    print("Election Smart Contract version 0.0.1\n");
  };

  /// @abi action
  void addc(std::string name) {
    print("Adding candidate ", name, "\n");

    uint64_t key = _candidates.available_primary_key();

    // update the table to include a new candidate
    _candidates.emplace(get_self(), [&](auto &p) {
      p._key = key;
      p._name = name;
      p._count = 0;
    });

    print("Candidate added successfully. candidate_key = ", key, "\n");
  };

  /// @abi action
  void reset() {
    // Get all keys of _candidates
    std::vector<uint64_t> keysForDeletion;
    for (auto &itr : _candidates) {
      keysForDeletion.push_back(itr.primary_key());
    }

    // now delete each item for that poll
    for (uint64_t key : keysForDeletion) {
      auto itr = _candidates.find(key);
      if (itr != _candidates.end()) {
        _candidates.erase(itr);
      }
    }

    // Get all keys of _voters
    keysForDeletion.empty();
    for (auto &itr : _voters) {
      keysForDeletion.push_back(itr.primary_key());
    }

    // now delete each item for that poll
    for (uint64_t key : keysForDeletion) {
      auto itr = _voters.find(key);
      if (itr != _voters.end()) {
        _voters.erase(itr);
      }
    }

    print("candidates and voters reset successfully.\n");
  };

  /// @abi action
  void results() {
    print("Start listing voted results\n");
    for (auto& item : _candidates) {
      print("Candidate ", item._name, " has voted count: ", item._count, "\n");
    }
  };

  /// @abi action
  void vote(account_name s, uint64_t candidate_key) {
    require_auth(s);

    bool found = false;

    // Did the voter vote before?
    for (auto& item : _voters) {
      if (item._account == s) {
        found = true;
        break;
      }
    }
    eosio_assert(!found, "You're voted already!");

    // Findout the candidate by id
    std::vector<uint64_t> keysForModify;
    for (auto& item : _candidates) {
      if (item.primary_key() == candidate_key) {
        keysForModify.push_back(item.primary_key());
        break;
      }
    }

    if (keysForModify.size() == 0) {
      eosio_assert(found, "Invalid candidate id!");
      return;
    }

    // Update the voted count inside the candidate
    for (uint64_t key : keysForModify) {
      auto itr = _candidates.find(key);
      auto candidate = _candidates.get(key);
      if (itr != _candidates.end()) {
        _candidates.modify(itr, get_self(), [&](auto& p) {
          p._count++;
        });

        print("Voted candidate: ", candidate._name, " successfully\n");
      }
    }

    // Add this user to voters array
    _voters.emplace(get_self(), [&](auto& p) {
      p._key = _voters.available_primary_key();
      p._candidate_key = candidate_key;
      p._account = s;
    });
  };
};

EOSIO_ABI(election, (version)(reset)(addc)(results)(vote))

Note the last line EOSIO_ABI() is a macro statement to generate ABI file automatically rather than write it manually. ABI file is to define the apply action handler. Which tells the EOSIO the definition of the handlers inside the smart contract.

EOSIO provides multi-index database API for us to persist data into the blockchain. In the above election smart contract, I defined two multi_index (similar to SQL table): candidates and voters. Actually which are two arrays to store the two struct: candidate and voter. I used C++ STL to manipulate the multi_index such as add, update, delete.

Note that the two struct are marked with /// @abi table at the beginning. This is to tell EOSIO abi generator to generate the ABI tables into the election.abi file. Which is very convenience.

To compile the election smart contract:

$ eosiocpp -o election.wast election.cpp

The WAST and WASM files are generated respectively. But it is not enough for EOSIO. We need to generate the ABI file as well:

$ eosiocpp -g election.abi election.cpp

Optional Files for Visual Studio Code

To enhance the development experience, I've created a properties file c_cpp_properties.json for Visual Studio Code (VSCode) to tell it how to find the header files. The file needs to be stored in .vscode directory as below:

The file content of .vscode/c_cpp_properties as below:

{
  "configurations": [
    {
      "name": "Linux",
      "includePath": [
        "${workspaceFolder}/**",
        "~/eos/contracts",
        "~/opt/boost/include"
      ],
      "defines": [],
      "compilerPath": "/usr/bin/clang++-4.0",
      "cStandard": "c11",
      "cppStandard": "c++17",
      "intelliSenseMode": "clang-x64"
    }
  ],
  "version": 4
}

Start the EOSIO

The machine is continuously using the virtual machine which was well configured (mentioned in part 1). To start the single-node Testnet server:

$ nodeos -e -p eosio --plugin eosio::wallet_api_plugin --plugin eosio::chain_api_plugin --plugin eosio::history_api_plugin --access-control-allow-origin=* --contracts-console

Click here for more info of the nodeos parameters.

Create the Accounts

Next task is to unlock the default wallet. EOSIO stores the keypairs in the wallet. It needs to be unlocked every time when the server restart or every 15 minutes. To unlock the wallet:

$ cleos wallet unlock --password ${wallet_password}

We need to create an owner keypairs and active keypairs respectively. Then import that private keys to the wallet. Type below command:

$ cleos create key # Create an owner key
$ cleos create key # Create an active key
$ cleos wallet import ${private_owner_key}
$ cleos wallet import ${private_active_key}

Don't forget to record those keypairs in somewhere

Next task is to create a new account election to hold the smart contract. Type below command:

$ cleos create account eosio election ${public_owner_key} ${public_active_key}

In addition, create two citizens for voting simulation:

$ cleos create account eosio voter1 ${public_owner_key} ${public_active_key}
$ cleos create account eosio voter2 ${public_owner_key} ${public_active_key}

Deploy the Smart Contract

Type below command to upload the election smart contract:

$ cleos set contract election ../election -p election

The resulting screenshot:
scr01

Run the Smart Contract

We can try to run the contract.

  1. Run the version action:
$ cleos push action election version '' -p election

We can inspect the console output from nodeos:

scr02

  1. Adding election candidates:
$ cleos push action election addc '["Hillary Clinton"]' -p election
$ cleos push action election addc '["Donald J. Trump"]' -p election
  1. Shows the candidates database which is storing in blockchain:
$ cleos get table election election candidate

The resulting screenshot:

scr03

  1. Simulate voting (both voters are voted to Donald J. Trump):
$ cleos push action election vote '["voter1", 1]' -p voter1
$ cleos push action election vote '["voter2", 1]' -p voter2

If voter1 votes again:

$ cleos push action election vote '["voter1", 0]' -p voter1

EOSIO returns exception:

scr04

  1. See the election result:
$ cleos get table election election candidate

scr05

As you can see, the vote count of candidate "Donald J. Trump" is 2. That means the Election Smart Contract was working!

That's all for this part.

In the next part, I will create a web app for demonstrating the interaction between web visitors and the blockchain.

The source code is host at this github repo/