В наших предыдущих статьях (часть 1 и часть 2) мы описали, как использовать децентрализованные приложения на основе смарт-контрактов, если вы не против выступать в роли узла. Однако чем меньше дополнительных действий пользователь должен выполнить, тем лучше. К сожалению, из-за того, что для работы со смарт-контрактами клиенту необходимо подписывать транзакции закрытым ключом, полностью избавиться от дополнительных действий невозможно. В этой статье мы рассмотрим два подхода. Первое - это полноценное децентрализованное приложение (DApp) на javascript с использованием библиотеки Web3.js и плагина MetaMask. Второе - похожее приложение, но использует Ruby on Rails API и гем Ethereum.rb для доступа к блокчейну.

Чтобы продемонстрировать работу реального DApp, рассмотрим приложение, вдохновленное официальным примером. Вместо Демократии с возможностью голосования и выполнением пользовательских транзакций мы создадим упрощенный контракт Благотворительность. В этом контракте любой желающий может сделать предложение о распределении валюты (эфира), участники голосуют, и по истечении крайнего срока предложение либо выполняется, либо нет, в зависимости от результата голосования. В этом случае мы не следуем логике смарт-контракта. Мы скорее стремимся показать разные способы взаимодействия пользователя и блокчейна. Сначала мы более подробно рассмотрим инструменты, которые собираемся использовать, а затем перейдем к самому приложению.

Используемые инструменты

1. MetaMask

Это плагин для браузера Chrome. Ожидается, что он также станет доступен для Firefox, но теперь вы можете использовать его только с Chrome. Вы можете скачать это здесь". Этот плагин позволяет делать следующие две вещи:

  • Он позволяет пользователю подключаться к необходимой сети Ethereum через ее удаленный узел. Благодаря этому пользователю не нужно самостоятельно развертывать узел.
  • Он позволяет создать новую или импортировать существующую учетную запись для работы с блокчейном. Это позволяет пользователю подписывать транзакции с помощью закрытого ключа, который хранится у пользователя локально.

Чтобы узнать больше о плагине, посетите официальную страницу.

2. Использование библиотеки Web3.js с плагином MetaMask

Web3.js - это библиотека JavaScript, которую мы использовали в Geth в нашей предыдущей статье. MetaMask внедряет Web3.js в каждую открытую страницу, что позволяет тестировать основные команды непосредственно в консоли javascript в инструментах разработчика Chrome. Обратите внимание, на момент написания этой статьи текущая версия Web3.js - 0.20.1. Чтобы получить документацию для версии 0.x.x перейдите по этой ссылке, не путайте ее с документацией для версии 1.0 (ссылка)

Выполним две команды. Сначала мы собираемся получить некоторые данные, например, баланс аккаунта. Во-вторых, внесем изменения. В качестве примера мы изменим строковое поле в смарт-контракте StringHolder, как описано в предыдущей статье. Прежде чем сделать это, убедитесь, что вы создали учетную запись в MetaMask, подключились к требуемой сети (в данном случае мы будем использовать Ropsten Test Network) и открыли консоль Developer Tools.

> web3.eth.getBalance(web3.eth.accounts[0], function(error, result) { console.log(web3.fromWei(result.toNumber())); } )

Обязательно передайте функцию обратного вызова в качестве последнего аргумента при вызове метода контракта. Сделать это можно так:

> web3.eth.getBalance(web3.eth.accounts[0], console.log)
null
e {s: 1, e: 18, c: Array(2)}
c:(2) [78950, 84540000000000]
e:18
s:1
__proto__:Object

Web3.js использует библиотеку BigNumber для числовых значений. В приведенном выше примере ответ дается без преобразования.

Команды чтения выполняются немедленно. Однако, если вы хотите вызвать функцию, которая изменяет данные в смарт-контракте (не помечена как constant), MetaMask откроет всплывающее окно для подписания транзакции. Давайте проверим это, открыв контракт StringHolder из предыдущей статьи и вызвав метод изменения строки в нем:

> var address = "0x65cA73D13a2cc1dB6B92fd04eb4EBE4cEB70c5eC";
> var abi = [ { "constant": false, "inputs": [ { "name": "newString", "type": "string" } ], "name": "setString", "outputs": [], "payable": false, "type": "function" }, { "constant": true, "inputs": [], "name": "getString", "outputs": [ { "name": "", "type": "string", "value": "Hello World!" } ], "payable": false, "type": "function" } ];
> var contract = web3.eth.contract(abi);
> var stringHolder = contract.at(address)
> stringHolder.getString(console.log)
null "Hello Dolly!!!  22"
> stringHolder.setString("Hello from the other side!", console.log)

После вызова метода setString всплывает окно подтверждения. В этом окне вы можете увидеть примерное использование газа и количество эфира. Нажмите «Отправить», чтобы подписать транзакцию. Вы увидите хеш транзакции в консоли.

Мы проверим, изменилась ли строка через некоторое время:

> stringHolder.setString("Hello from the other side!", console.log)
"0x4252c00ff25b690846ec8ad3b4266047a75a1708153bcac24066da9cb01e6db5"
> stringHolder.getString(console.log)
null "Hello from the other side!"

Все работает как положено.

3. Ethereum.rb

Это библиотека для работы с блокчейном Ethereum от Ruby (ссылка на github). Сейчас он наиболее активно поддерживается.

Давайте попробуем открыть контракт StringHolder из консоли irb:

> require “ethereum.rb”
> abi = '[ { "constant": false, "inputs": [ { "name": "newString", "type": "string" } ], "name": "setString", "outputs": [], "payable": false, "type": "function" }, { "constant": true, "inputs": [], "name": "getString", "outputs": [ { "name": "", "type": "string", "value": "Hello World!" } ], "payable": false, "type": "function" } ]'
> address = "0x65cA73D13a2cc1dB6B92fd04eb4EBE4cEB70c5eC"
> contract = Ethereum::Contract.create(name: "StringHolder", address: address, abi: abi)
> contract.call.get_string()
“Hello from the other side!”

Обратите внимание, футляр для верблюда (getString) был автоматически преобразован в футляр для змеи (get_string).

Обратите внимание, что через call вызываются только геттеры (когда вам просто нужно получить данные). Для выполнения транзакций вам понадобится transact_and_wait в случае синхронного вызова или transact для асинхронного вызова.

Вызовем функцию set_string, для которой будет создана транзакция. Есть два способа: unlockAccount (не рекомендуется) и подписание транзакции (подробнее позже).

> Ethereum::IpcClient.new.personal_unlock_account( Ethereum::IpcClient.new.eth_coinbase["result"], "<password>" )

Требуется указать отправителя транзакции (тот, который только что был разблокирован):

> contract.sender = Ethereum::IpcClient.new.eth_coinbase["result"]

Затем вы можете вызвать сеттер, поместив либо transact_and_wait, либо transact вместо call:

> contract.transact_and_wait.set_string(“Hello darkness, my old friend”)

Дождитесь завершения, затем позвоните:

> contract.call.get_string()

Вы видите изменения - работает.

Ожидаемые возможности нашего DApp

Сформулируем задачу. Нам нужен контракт, который представляет благотворительную организацию с учетной записью. В этой организации зарегистрированы пользователи, которые могут выступать в качестве получателей пожертвований и голосовать за предложения. Нам нужен метод для создания предложения о переводе валюты (эфира) из учетной записи организации в одну из учетных записей пользователей. Чтобы не поддаваться соблазну передать сразу весь доступный эфир, введем ограничение - предлагать можно не более 1 эфира. Затем происходит голосование (вы можете проголосовать «за» или «против»), которое не может быть прекращено до определенного срока (5 минут после формирования предложения). По истечении установленного срока голоса продолжают поступать, но становится доступна возможность завершить голосование. И, если это заканчивается тем, что больше пользователей одобряют транзакцию, эфир переводится из учетной записи организации в учетную запись получателя. Если против будет больше избирателей, ничего не будет сделано.

В целом схема выглядит так:

За работу с блокчейном отвечают два js-модуля - Blockchain.js и BlockchainApi.js. Они выполняют ту же задачу, но первый работает с Web3.js и напрямую обращается к блокчейну через узел MetaMask, а второй создает запросы ajax к Rails API, где взаимодействие с блокчейном происходит через гем ethereum.rb. Само клиентское приложение построено на React и не зависит от того, какой модуль js используется.

Благотворительность по основному контракту

Контракт представляет собой единый файл, поэтому мы не будем использовать какие-либо специальные инструменты для его развертывания, вы можете сделать это любым способом, например, через Mist или с помощью командной строки Geth. Вы можете использовать наш код здесь. Его схема представлена ​​на схеме ниже:

Давайте посмотрим на код контракта Charity.sol по логическим блокам. Сначала вводятся все необходимые переменные:

    uint public debatingPeriodInMinutes;
    Proposal[] public proposals;
    uint public numProposals;
    uint public numMembers;
    mapping (address => uint) public memberId;
    address[] public members;

Отображение позволяет получить индекс пользователя в массиве на основе адреса пользователя. Если пользователя с введенным адресом нет, будет возвращено «0». В дальнейшем этот принцип будет использоваться в функции, определяющей, зарегистрирован пользователь с определенным адресом или нет. Он устанавливает требование к массиву: пользователи должны храниться в нем, начиная с индекса «1». Код, поддерживающий эту логику, будет представлен позже. Следующий элемент представляет собой структуру для хранения предложения.

    struct Proposal {
        address recipient;
        uint amount;
        string description;
        uint votingDeadline;
        bool executed; // flash showing if proposal is finished
        bool proposalPassed; // flag showing if proposal is accepted
        uint numberOfVotes;
        int currentResult; // the total of votes, “for“ = +1, “against” = -1
        Vote[] votes; // array containing how everyone voted
        mapping (address => bool) voted; // mapping for quick check whether someone has voted or not
    }

Структура голосов сохраняется в массиве каждого предложения.

    struct Vote {
        bool inSupport;
        address voter;
    }

Давайте рассмотрим модификатор, который позволит убедиться, что модифицированный метод может быть вызван только зарегистрированным пользователем. Код проверки основан на предположении, что несуществующие элементы сопоставления приводят к «0», а пользователи сохраняются, начиная с индекса «1», как упомянуто выше.

    modifier onlyMembers {
        require (memberId[msg.sender] != 0);
        _; // code of modified method will be in place of underscore
    }

msg - это структура, с помощью которой можно получить информацию о вызывающем абоненте. В данном случае msg.sender - это адрес учетной записи, которая вызвала метод с этим модификатором.

Опишем конструктор нашего контракта, который будет выполняться во время деплоя. Единственное, что необходимо указать, - это время, отведенное на голосование по каждому предложению. Более того, нам нужно увеличить длину массива «members», поскольку мы будем добавлять пользователей в соответствии с этой длиной, а нулевой элемент останется зарезервированным.

    function Charity( uint _minutesForDebate ) payable { // payable means that if we send some ether with this transaction, it will be add to the contract balance
        debatingPeriodInMinutes = _minutesForDebate;
        members.length++;
    }

Функция добавления нового пользователя:

    function addMember(address _targetMember) {
        if (memberId[_targetMember] == 0) { // 0 means no such user exists
            uint id;
            memberId[_targetMember] = members.length;
            id = members.length++;
            members[id] = _targetMember;
        }
    }

Обратите внимание на функцию require - она ​​заменила throw в старых версиях Solidity. true или false передается в require; если значение равно false, то активируется обработчик, подобный throw, и откат всей транзакции.

Чтобы проверить, есть ли адрес в списке пользователей, мы используем следующую функцию:

    function isMember( address _targetMember ) constant returns ( bool ) {
        return ( memberId[_targetMember] != 0 );
    }

Следующая функция используется для создания предложения. Он получает адрес получателя пожертвования, количество эфира в wei и строку с описанием. К этой функции применяется модификатор onlyMembers, что означает, что перед выполнением кода будет произведена проверка того, зарегистрирована ли вызывающая учетная запись. Здесь вы увидите такие преобразования, как 1 ether и 1 minutes. Вы можете найти полный список таких суффиксов здесь, они созданы для удобства и могут применяться только к значениям, а не к переменным. Чтобы использовать их с переменными, вам нужно добавить 1 к суффиксу, что было сделано в нашем случае для преобразования в секунды.

    function newProposal(
            address _beneficiary,
            uint _weiAmount,
            string _description
        )
            onlyMembers
            returns (uint proposalID)
    {
        require( _weiAmount <= (1 ether) );
        proposalID = proposals.length++;
        Proposal storage p = proposals[proposalID];
        p.recipient = _beneficiary;
        p.amount = _weiAmount;
        p.description = _description;
        p.votingDeadline = now + debatingPeriodInMinutes * 1 minutes;
        p.executed = false;
        p.proposalPassed = false;
        p.numberOfVotes = 0;
        numProposals = proposalID + 1;

        return proposalID;
    }

Обратите внимание на слово now key, которое является текущим временем в момент создания блока, а не в момент вызова транзакции. Благодаря этому дедлайн будет отсчитываться с момента, когда предложение уже создано на блокчейне.

Несмотря на то, что наши proposals равны public, мы можем получать только базовые поля в виде массива, следуя этому подходу. Например, если мы вызовем метод proposals(1) в контракте, мы получим предложение с индексом «1» в виде следующего массива { recipient, amount, description, votingDeadline, executed, proposalPassed, numberOfVotes, currentResult }, а массивы votes и voted внутри структуры не будут возвращены. Однако нам нужно знать, проголосовал ли пользователь за конкретное предложение, чтобы показать свой голос или позволить ему проголосовать. Желательно получить эту информацию за один звонок. Для этого мы используем специальную функцию getProposal, которая получает учетную запись, статус голосования которой требуется, и идентификатор предложения. Статус голосования возвращается в дополнение к существующим Proposal полям.

    function getProposal( address _member, uint _proposalNumber ) constant
        returns ( address,
                  uint,
                  string,
                  uint,
                  bool,
                  bool,
                  uint,
                  int,
                  int ) {
        Proposal memory proposal = proposals[ _proposalNumber ];
        int vote = getVoted( _member, _proposalNumber );
        return ( proposal.recipient,
                 proposal.amount,
                 proposal.description,
                 proposal.votingDeadline,
                 proposal.executed,
                 proposal.proposalPassed,
                 proposal.numberOfVotes,
                 proposal.currentResult,
                 vote );
    }

Ниже приводится вспомогательная функция, которая показывает, как пользователь проголосовал за одно конкретное предложение. Если пользователь не голосовал, функция вернет «0», если голосующий за предложение, функция вернет «1», если избиратель против - вернет «-1».

    function getVoted(address _member, uint _proposalNumber) constant returns(int)
    {
        Proposal storage p = proposals[_proposalNumber];
        int result = 0;
        int true_int = 1;
        int false_int = -1;
        for (uint i = 0; i < p.numberOfVotes; i++)
        {
            if (p.votes[i].voter == _member)
            {
                result = p.votes[i].inSupport ? true_int : false_int;
                break;
            }
        }
        return result;
    }

Голосование: для каждого предложения с уникальным номером мы голосуем «верно» (за) или «ложно» (против).

    function vote(
            uint _proposalNumber,
            bool _supportsProposal
    )
            onlyMembers
            returns (uint voteID)
    {
        Proposal storage p = proposals[_proposalNumber];
        require (p.voted[msg.sender] != true);
        p.voted[msg.sender] = true;
        p.numberOfVotes++;
        if (_supportsProposal) {
            p.currentResult++;
        } else {
            p.currentResult--;
        }
        voteID = p.votes.length++;
        p.votes[voteID] = Vote({inSupport: _supportsProposal, voter: msg.sender});
        return p.numberOfVotes;
    }

Наконец, последняя функция executeProposal используется для завершения голосования и передачи эфира бенефициару в случае положительного результата голосования.

function executeProposal(uint _proposalNumber) {
        Proposal storage p = proposals[_proposalNumber];

        require ( !(now < p.votingDeadline || p.executed) );
        p.executed = true;

        if (p.currentResult > 0) {
            require ( p.recipient.send(p.amount) );
            p.proposalPassed = true;
        } else {
            p.proposalPassed = false;
        }
    }

В итоге остается пустая функция с модификатором payable.

function () payable {}

Эта функция позволяет отправлять эфир на адрес контракта. Как правило, для приема и обработки всех сообщений, не вызывающих функции, требуется пустая функция. Все, что нам нужно сделать, это сделать его payable. Когда он станет payable, отправленный газ будет заключен в контракт без каких-либо дополнительных действий. Обратите внимание, что другие функции не имеют этого модификатора, а это значит, что в нашем случае, например, вы не можете отправить эфир, вызвав addMember.

Версия приложения с использованием Web3.js

Базовый сценарий приложения:

  1. Пользователь подключается к сети Ropsten через MetaMask.
  2. Если в аккаунте нет эфира, провести транзакцию будет невозможно. Мы добавили функцию приема эфира, которая становится доступной при балансе счета ниже 0,1 эфира. Реализуется через внешний сервис. К этому сервису делается ajax-запрос с адресом, на который должен быть переведен эфир.
  3. Основные действия со смарт-контрактом становятся доступными только после того, как пользователь становится членом организации. Для добавления пользователя в организацию вызывается метод addMember в смарт-контракте.
  4. Член организации может создать предложение о переводе средств или проголосовать за уже существующее.
  5. Когда время, отведенное для предложения, истекает (время создания + пять минут), становится доступной возможность завершить предложение. В зависимости от распределения голосов, передача эфира на указанный адрес будет выполнена или нет.

Демонстрация работы приложения доступна по этой ссылке - версия MetaMask.

Исходный код находится здесь.

Мы хотели бы еще раз обратить ваше внимание на то, что текущая версия Web3.js - 0.20.1. Скоро выйдет версия 1.0 со значительными изменениями. Как упоминалось выше, MetaMask внедряет web3 на страницу, и его можно сразу использовать. Однако, учитывая тот факт, что библиотека постоянно развивается, а мы должны обеспечить работоспособность приложения для пользователя, мы вынуждены использовать заблокированную версию и переопределить объект web3, который интегрируется MetaMask. Мы делаем это здесь следующим способом:

  initializeWeb3() {
    if (typeof web3 !== 'undefined') { // if some version of web3.js is injected by MetaMask
      const defaultAccount = web3.eth.defaultAccount; // save the default account
      window.web3 = new Web3(web3.currentProvider); // initialize a new library
      window.web3.eth.defaultAccount = defaultAccount; // reassign the default account
    }  }

Это нужно сделать после события window.onload.

Есть одна проблема, которая не очевидна и решена в этом коде. Если вы просто напишете window.web3 = new Web3(web3.currentProvider), как это описано в официальной документации, то учетная запись по умолчанию не будет подобрана.

Также, как было сказано выше, в MetaMask вы можете выбрать сеть из списка. Мы используем адреса договоров из сети Ropsten. Поэтому, если вы попытаетесь использовать эти адреса для подключения в других сетях, результат может быть непредсказуемым. В связи с этим перед предоставлением доступа к приложению необходимо провести проверку сети пользователя. Вы можете получить сетевой идентификатор, используя следующую команду:

web3.version.getNetwork(function (err, netId) {});

Мы делаем эту проверку здесь, а затем сравниваем результат с идентификатором сети Ropsten, который равен 3.

Вы можете найти список сетевых идентификаторов здесь в описании net_version.

Логику работы с блокчейном можно найти в файле blockchain.js.

Существует два основных типа функций - функции для получения данных из цепочки блоков и функции для изменения данных в цепочке блоков. Большинство методов из web3.js выполняются асинхронно и в качестве последнего параметра принимают «обратный вызов». Поскольку для получения данных часто требуется вызвать несколько методов, а вызов некоторых из них зависит от результата работы других, удобно использовать промисы. В версии 1.0 асинхронные методы web3.js по умолчанию возвращают обещания.

Давайте рассмотрим пример получения информации из блокчейна:

Функция getCurrentAccountInfo возвращает адрес текущего счета, его баланс и флаг того, является ли этот счет членом организации.

Blockchain.prototype.getCurrentAccountInfo = function() {
  const address = this.address;
  if (address == undefined) {
    return Promise.resolve({});
  }

  const balancePromise = new Promise(function(resolve, reject) {
    web3.eth.getBalance(address, function(err, res) {
      err ? reject(err) : resolve(web3.fromWei(res).toNumber());
    });
  });

  const authorizedPromise = new Promise(function(resolve, reject) {
    this.contractInstance.isMember(address, function(err, res) {
      err ? reject(err) : resolve(res);
    });
  }.bind(this));

  return new Promise(function(resolve, reject) {
    Promise.all([balancePromise, authorizedPromise]).then(function(data) {
      resolve({
        address: address,
        balance: data[0],
        isMember: data[1]
      });
    });
  });
};

Теперь давайте рассмотрим функцию изменения данных в цепочке блоков, например функцию добавления члена в организацию.

Blockchain.prototype.becomeMember = function() {
  return new Promise(function(resolve, reject) {
    this.contractInstance.addMember(this.address, function(err, res) {
      err ? reject(err) : resolve(res);
    });
  }.bind(this));
};

Как видим, синтаксис ничем не отличается от предыдущего примера. Однако выполнение этой функции приведет к созданию транзакции для изменения данных в цепочке блоков.

При вызове любой функции в смарт-контракте, которая приводит к созданию транзакции, MetaMask просит пользователя подтвердить эту транзакцию или отменить ее. Если пользователь подтверждает транзакцию, функция возвращает хеш транзакции. Одна проблема, которая не очевидна, заключается в том, как узнать, была ли транзакция завершена успешно или нет. Статус транзакции можно определить по количеству использованного газа. Если использовался общий доступный баланс, то либо во время выполнения произошла ошибка, либо количества газа было недостаточно для выполнения транзакции. Проверку статуса мы проводим следующим образом.

Blockchain.prototype.checkTransaction = function(transaction) {
  const txPromise = new Promise(function(resolve, reject) {
    web3.eth.getTransaction(transaction.transactionHash, function(err, res) {
      err ? reject(err) : resolve(res);
    });
  });

  const txReceiptPromise = new Promise(function(resolve, reject) {
    web3.eth.getTransactionReceipt(transaction.transactionHash, function(err, res) {
      err ? reject(err) : resolve(res);
    });
  });

  return new Promise(function(resolve, reject) {
    Promise.all([txPromise, txReceiptPromise]).then(function(res) {
      const tx = res[0];
      const txReceipt = res[1];
      const succeeded = txReceipt && txReceipt.blockNumber && txReceipt.gasUsed < tx.gas;
      const failed = txReceipt && txReceipt.blockNumber && txReceipt.gasUsed == tx.gas;

      let state = transactionStates.STATE_PENDING;
      if (succeeded) {
        state = transactionStates.STATE_SUCCEEDED;
      } else if (failed) {
        state = transactionStates.STATE_FAILED;
      }

      resolve(state);
    });
  });
};

При создании новой транзакции мы добавляем ее в localStotage и периодически запрашиваем ее статус, пока не узнаем, была ли она выполнена, успешно или нет. Логику мониторинга транзакций можно найти в этом файле - transaction-storage.js.

Версия приложения с использованием Ruby on Rails и Ethereum.rb Gem

Подлинно децентрализованное приложение выглядит так, как описано выше. Пользователь подписывает транзакции с помощью закрытого ключа, который хранится локально. Однако, помимо случаев, когда пользователь приложения напрямую взаимодействует с цепочкой блоков, доступ к цепочке блоков может потребоваться со стороны серверной части. Это может быть внутреннее приложение, и действия с блокчейном выполняются из контролируемых вами учетных записей, а ключи от этих учетных записей вы можете хранить на сервере. Возможно, логика вашего приложения на основе смарт-контрактов требует, чтобы ваш сервер реагировал на определенные события. В этом случае, помимо web3.js, который вы, естественно, также можете использовать на сервере, было бы удобно иметь инструмент для вашего обычного стека разработки. Обычно мы используем Ruby on Rails, поэтому мы решили протестировать библиотеку ethereum.rb, которая должна служить той же цели, что и web3.js.

Демонстрация приложения доступна по этой ссылке - версия Rails API.

Исходный код находится здесь.

Для демонстрации работы с ethereum.rb мы создали набор функций, аналогичный описанному в blockchain.js. Вы можете ознакомиться с кодом работы с блокчейном здесь, а мы выделим основные идеи и, возможно, некоторые отличия.

Во-первых, у нас больше нет доступа к учетной записи пользователя, поэтому нам нужно установить некоторый процесс авторизации и создать учетную запись для каждого пользователя в цепочке блоков. Процесс авторизации чрезвычайно упрощен, мы просто генерируем токен, который хранится в «LocalStorage» браузера и используется для авторизации запросов к API.

В целом схема работы со счетами следующая:

  1. Пользователь нажимает кнопку «Создать учетную запись»; в базе на сервере создается пользователь с уникальным токеном для авторизации; создается учетная запись для подключения к блокчейну, а закрытый ключ учетной записи сохраняется в базе данных; токен возвращается пользователю для дальнейшей авторизации.
  2. Пользователь делает запросы к API, используя «токен авторизации» для авторизации.
  3. Закрытый ключ пользователя из базы данных используется для подписи транзакций.

Как и в js-версии, есть два типа методов - те, которые получают данные из блокчейна, и те, которые изменяют данные. Первые довольно просты, и их действие видно из приведенного ниже примера.

def proposals(address=nil)
    count = @contract_instance.call.num_proposals
    to = count - 1
    from = count > PROPOSALS_LIMIT ? count - PROPOSALS_LIMIT : 0

    res = (from..to).map do |idx|
      proposal = if address
        @contract_instance.call.get_proposal(address, idx)
      else
        @contract_instance.call.proposals(idx)
      end
      Proposal.new(proposal, idx)
    end

    res.sort_by(&:index).reverse
  end

Методы, которые изменяют данные в цепочке блоков, создают транзакцию, которая должна быть подписана закрытым ключом. Для работы с приватными ключами используется гем Eth. Ниже вы можете увидеть процесс создания новой учетной записи для блокчейна и подписания транзакции с закрытым ключом.

def self.new_account
    new_key = Eth::Key.new
    return new_key.address.downcase, new_key.private_hex
  end

 def signed_transactions(private_key_hex)
    key = Eth::Key.new priv: private_key_hex #create key instance from binary form
    @contract_instance.key = key # use this key for all instance calls
    res = yield(@contract_instance) # execute required transactions
    @contract_instance.key = nil # reset the key afterwards
    res
  end

Давайте посмотрим на метод signed_transactions. Он принимает закрытый ключ и блок кода, который мы хотим выполнить, используя этот ключ. В базе данных ключ хранится в двоичной форме, из которой создается объект ключа с помощью гема Eth. Затем этот ключ присваивается полю key экземпляра контракта. Транзакция в ethereum.rb подписывается автоматически, если ключ назначен полю key контракта. Вызвав требуемый метод или несколько методов, мы обязательно очищаем ключ, чтобы случайно не отправить больше транзакций с этого аккаунта.

В качестве примера использования signed_transactions давайте рассмотрим метод завершения предложения, который вызывает метод executeProposal в смарт-контракте:

def finish_proposal(proposal_index, options={})
    tx = signed_transactions(options[:private_key]) do |contract_instance|
      contract_instance.transact.execute_proposal(proposal_index) 
    end
    tx.id
  end

Метод возвращает хеш транзакции. Логика проверки статуса транзакции аналогична той, которую мы использовали в версии js.

def transaction_status(tx_hash)
    tx = @client.eth_get_transaction_by_hash(tx_hash)['result']
    tx_receipt = @client.eth_get_transaction_receipt(tx_hash)['result']
    if tx_receipt
      block_number = tx_receipt['blockNumber']

      gas_used = tx_receipt['gasUsed'].to_i(16)
      gas = tx['gas'].to_i(16)

      {
        succeeded: block_number && gas_used < gas,
        failed: block_number && gas_used == gas
      }
    end
  end

Посмотрите, как это работает

Демонстрация работы приложения доступна по этой ссылке.

Мы перевели некоторое количество эфира на счет контракта, если он закончился, и у вас есть дополнительный тестовый эфир, вы можете пожертвовать здесь: 0xe79d8738f0769ec69f09cef4cd497c9cc477733e - сеть Ropsten.

Каков вывод?

Однако мы считаем, что оба подхода жизнеспособны в разных обстоятельствах. В данном случае версия Ruby несколько надумана, а версия с использованием MetaMask более логична. Но, как упоминалось выше, нашей целью было не представить примерный проект, а, скорее, использовать простую логику для демонстрации примеров взаимодействия с блокчейном с использованием javascript и ruby. Мы надеемся, что нам это удалось. В следующей статье мы расскажем о более продвинутой работе с развертыванием и тестированием смарт-контрактов. Скоро выйдет!