Курс разработчика EOS. Часть 9. Eosio.token. Реализация

avatar igorart 8 months ago

Назад Содержание Вперед

В прошлой статье мы рассмотрели интерфейс смарт-контракта eosio.token для создания собственной монеты. В данной статье мы более подробно рассмотрим его реализацию. Это в последующем поможет нам в понимании и  написании наших собственных смарт-контрактов. Исходный код контракта, находится в директории - ./eos/contracts/eosio.token (два основных файла eosio.token.cpp и eosio.token.hpp ).

Изначально рассмотрим файл eosio.token.hpp

  
/**
 *  @file

 *  @copyright defined in eos/LICENSE.txt

 */

#pragma once

#include <eosiolib/asset.hpp>

#include <eosiolib/eosio.hpp>

#include <string>

namespace eosiosystem {

   class system_contract;

}

namespace eosio {

   using std::string;

   class token : public contract {

      public:

         token( account_name self ):contract(self){}

         void create( account_name issuer,

                      asset        maximum_supply);

         void issue( account_name to, asset quantity, string memo );

         void transfer( account_name from,

                        account_name to,

                        asset        quantity,

                        string       memo );

         inline asset get_supply( symbol_name sym )const;

         inline asset get_balance( account_name owner, symbol_name sym )const;

      private:

         struct account {

            asset    balance;

            uint64_t primary_key()const { return balance.symbol.name(); }

         };

         struct currency_stats {

            asset          supply;

            asset          max_supply;

            account_name   issuer;

            uint64_t primary_key()const { return supply.symbol.name(); }

         };

         typedef eosio::multi_index<N(accounts), account> accounts;

         typedef eosio::multi_index<N(stat), currency_stats> stats;

         void sub_balance( account_name owner, asset value );

         void add_balance( account_name owner, asset value, account_name ram_payer );

      public:

         struct transfer_args {

            account_name  from;

            account_name  to;

            asset         quantity;

            string        memo;

         };

   };

   asset token::get_supply( symbol_name sym )const

   {

      stats statstable( _self, sym );

      const auto& st = statstable.get( sym );

      return st.supply;

   }

   asset token::get_balance( account_name owner, symbol_name sym )const

   {

      accounts accountstable( _self, owner );

      const auto& ac = accountstable.get( sym );

      return ac.balance;

   }

} /// namespace eosio


Конструктор и все actions, представленные в интерфейсе предыдущей статьи  объявлены с модификатором public - значит доступны всем. Также с модификатором public   представлена структура

 struct transfer_args

В коде также есть еще две структуры account и currency_stats. Первая структура необходима нам для хранения в  таблице  account различных токенов (токенов с различными именами) . Таблица account представляет собой мультииндекс контейнер(о  них мы упоминали ранее) и может содержать несколько объектов account. Каждый из которых содержит имя и баланс токена. Следует также ввести такое понятие как scope. Scope - по сути представляет собой аккаунт для которого будут храниться данные. Лучше думать о нем как о некой области видимости или пространстве имен. Это способ разделить данные в контракте. Каждый аккаунт имеет свой scope. И для каждого аккаунта количество объектов хранящихся в таблице account будет свое. Ровно как и содержание. Тоесть, другими словами, для каждого конкретного аккаунта будет своя совокупность токенов различных  наименований (количество и баланс токенов). Например, у аккаунта eoswitnessacc может быть, например 10  токенов EOS и 20 (нами же созданных и выпущенных)токенов "WIT". Структура currency_stats отражает состояние конкретного токена(монеты) и содержит поля supply, max_supply и issuer.

supply - количество выпусщенных токенов

max_supply - максимально допустимое количество выпущенных токенов

 issuer - аккаунт, выпускающий токены

Ниже приведено содержание файла eosio.token.cpp


/**
 *  @file
 *  @copyright defined in eos/LICENSE.txt
 */

#include "eosio.token.hpp"

namespace eosio {

void token::create( account_name issuer,
                    asset        maximum_supply )
{
    require_auth( _self );

    auto sym = maximum_supply.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( maximum_supply.is_valid(), "invalid supply");
    eosio_assert( maximum_supply.amount > 0, "max-supply must be positive");

    stats statstable( _self, sym.name() );
    auto existing = statstable.find( sym.name() );
    eosio_assert( existing == statstable.end(), "token with symbol already exists" );

    statstable.emplace( _self, [&]( auto& s ) {
       s.supply.symbol = maximum_supply.symbol;
       s.max_supply    = maximum_supply;
       s.issuer        = issuer;
    });
}


void token::issue( account_name to, asset quantity, string memo )
{
    auto sym = quantity.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );

    auto sym_name = sym.name();
    stats statstable( _self, sym_name );
    auto existing = statstable.find( sym_name );
    eosio_assert( existing != statstable.end(), "token with symbol does not exist, create token before issue" );
    const auto& st = *existing;

    require_auth( st.issuer );
    eosio_assert( quantity.is_valid(), "invalid quantity" );
    eosio_assert( quantity.amount > 0, "must issue positive quantity" );

    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    eosio_assert( quantity.amount <= st.max_supply.amount - st.supply.amount, "quantity exceeds available supply");

    statstable.modify( st, 0, [&]( auto& s ) {
       s.supply += quantity;
    });

    add_balance( st.issuer, quantity, st.issuer );

    if( to != st.issuer ) {
       SEND_INLINE_ACTION( *this, transfer, {st.issuer,N(active)}, {st.issuer, to, quantity, memo} );
    }
}

void token::transfer( account_name from,
                      account_name to,
                      asset        quantity,
                      string       memo )
{
    eosio_assert( from != to, "cannot transfer to self" );
    require_auth( from );
    eosio_assert( is_account( to ), "to account does not exist");
    auto sym = quantity.symbol.name();
    stats statstable( _self, sym );
    const auto& st = statstable.get( sym );

    require_recipient( from );
    require_recipient( to );

    eosio_assert( quantity.is_valid(), "invalid quantity" );
    eosio_assert( quantity.amount > 0, "must transfer positive quantity" );
    eosio_assert( quantity.symbol == st.supply.symbol, "symbol precision mismatch" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );


    sub_balance( from, quantity );
    add_balance( to, quantity, from );
}

void token::sub_balance( account_name owner, asset value ) {
   accounts from_acnts( _self, owner );

   const auto& from = from_acnts.get( value.symbol.name(), "no balance object found" );
   eosio_assert( from.balance.amount >= value.amount, "overdrawn balance" );


   if( from.balance.amount == value.amount ) {
      from_acnts.erase( from );
   } else {
      from_acnts.modify( from, owner, [&]( auto& a ) {
          a.balance -= value;
      });
   }
}

void token::add_balance( account_name owner, asset value, account_name ram_payer )
{
   accounts to_acnts( _self, owner );
   auto to = to_acnts.find( value.symbol.name() );
   if( to == to_acnts.end() ) {
      to_acnts.emplace( ram_payer, [&]( auto& a ){
        a.balance = value;
      });
   } else {
      to_acnts.modify( to, 0, [&]( auto& a ) {
        a.balance += value;
      });
   }
}

} /// namespace eosio

EOSIO_ABI( eosio::token, (create)(issue)(transfer) )

Для того, чтобы нам создать новый токен мы вызываем action

void token::create( account_name issuer, asset        maximum_supply ) {}

в качестве параметров в него передаются issuer - название аккаунта, от имени которого мы будем выпускать токены(монеты) и maximum_supply - максимально допустимое количество выпускаемых токенов . Только аккаунт, создающий токены имеет право изменять maximum_supply. Следующая строка

 require_auth( _self );

говорит о том, что только владелец учетной записи(аккаунта) мог использовать нашу функцию. Последующие несколько строк производят проверку на корректность входныx данных парааметра maximum_supply - валидацию.

auto sym = maximum_supply.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( maximum_supply.is_valid(), "invalid supply");
    eosio_assert( maximum_supply.amount > 0, "max-supply must be positive");

Если возникнет ошибка, то наш action на этом этапе завершит выполнение. Транзакция является более широким понятием чем action. Она может содержать несколько action. Поэтому в случае возникновения ошибки наш action не станет частью транзакции и не попадет в блокчейн. Затем в строках мы создаем конструктор экземпляра структуры stats с названием  statstable. В него передаем название нашего аккаунта и название токена. Далее проверяем существует ли уже токен с таким название в нашей таблице. 

stats statstable( _self, sym.name() );
    auto existing = statstable.find( sym.name() );
    eosio_assert( existing == statstable.end(), "token with symbol already exists" );

Если токен с таким именем существует, то происходит ошибка. Если нет - вызывается функция emplace.В нее передаются параметры. Первый параметр - аккаунт, который будет платить за хранение данных. Второй параметр - лямбда-функция.

 statstable.emplace( _self, [&]( auto& s ) {
       s.supply.symbol = maximum_supply.symbol;
       s.max_supply    = maximum_supply;
       s.issuer        = issuer;
    });

Рассмотрим следующее действие

void token::issue( account_name to, asset quantity, string memo ){}

В качестве входных параметров в него передаются to - аккаунт, на который будут выпущены токены, quantity - количество выпускаемых токенов, и memo - сообщение. Тут также, как и в предыдущем action вначале происходит проверка на ошибки 

    auto sym = quantity.symbol;
    eosio_assert( sym.is_valid(), "invalid symbol name" );
    eosio_assert( memo.size() <= 256, "memo has more than 256 bytes" );

Далее поле имени токена, являющееся первичным ключом нашей таблицы statstable используется для поиска по таблице токена, который мы будем выпускать. Для этого мы объявляем переменную existing. Она является итератором. При помощи итератора ищем наш токен.

 
    auto sym_name = sym.name();
    stats statstable( _self, sym_name );
    auto existing = statstable.find( sym_name );
    eosio_assert( existing != statstable.end(), "token with symbol does not exist, create token 
    before issue" );
    const auto& st = *existing;

Мы объявляем переменную st, как - константную ссылку. Инициализируем ее нашим итератором existing. *existing со звездочкой потому, что мы разименовываем указатель перед тем как присвоить его нашей переменной st. Переменная st присутствует в коде для краткости и удобства. Ее можно было бы не вводить, но тогда нам пришлось бы обращаться ко всем полям нашей структуры не как st.имя_поля, а как existing->имя_поля. Тоесть не через объект, а через указатель на объект.

В следующей части кода мы вызываем функцию modify для изменения общего количества выпущенных токенов.


statstable.modify( st, 0, [&]( auto& s ) {
       s.supply += quantity;
    });
    add_balance( st.issuer, quantity, st.issuer );
    if( to != st.issuer ) {
       SEND_INLINE_ACTION( *this, transfer, {st.issuer,N(active)}, {st.issuer, to, quantity, memo} );
    }

Нам интересен макрос SEND_INLINE_ACTION в качестве входных параметров в который передаются: *this - сам объект контракта, transfer - имя action, которое будем вызывать, {st.issuer,N(active)} - права доступа, {st.issuer, to, quantity, memo} - параметры, передаваемые в action (в нашем случае transfer).

Следующее action

  void token::transfer( account_name from, account_name to, asset quantity, string memo ){}

предназначено для перевода токенов с аккаунта на аккаунт. Он принимает четыре параметра: from - аккаунт с которого будут переводиться токены, to - аккаунт на который будут переводиться токены, quantity - количество переводимых токенов, memo - сообщение.

Далее производится проверка, что отправитель и получатель токенов, это разные аккаунты. Также проверяется, что данная функция вызывается от имени отправителя токенов. Затем из входного параметра quantity по названию токена с использованием функции get() извлекаются нужные данные (объект) из структуры currency_stats.


    eosio_assert( from != to, "cannot transfer to self" );
    require_auth( from );
    eosio_assert( is_account( to ), "to account does not exist");
    auto sym = quantity.symbol.name();
    stats statstable( _self, sym );
    const auto& st = statstable.get( sym );
 

Следующие два вызова одной и той же функции отправляют уведомления отправителю и получателю об изменениях произошедших в данном действии.


require_recipient( from );
require_recipient( to );

Последующие 4 строки производят проверку (валидацию) для входных параметров quantity и memo. Функции


sub_balance( from, quantity );
add_balance( to, quantity, from );

вызываются для уменьшения баланса отправителя и увеличения баланса получателя на величину quantity.

Резюме

В данной статье мы рассмотрели реализацию токена для выпуска монет eosio.token.

В следующей статье мы более подробно рассмотрим работу с данным контрактом - эмиссию и перевод с аккаунта на аккаунт.

Для всех, кому интересен EOS, присоединяйтесь в эту группу в телеграме.

Назад Содержание Вперед