javascript

Платёжная система в 50 строк кода, реально?

  • среда, 14 февраля 2018 г. в 03:15:49
https://habrahabr.ru/post/348876/
  • Программирование
  • Платежные системы
  • Solidity
  • JavaScript


В последнее время технологические решения на блокчейне всё больше проникают в нашу повседневную жизнь. Технология новая, поэтому не все понимают, как и где её применять. Я попробовал создать платежную систему на базе смарт-контракта Ethereum и результат меня удивил. Смарт-контракт выполняющий функции полноценной платёжной системы получился всего в 50 строк кода. Всех заинтересовавшихся как он работает прошу под кат.

image

На Хабре уже были хорошие публикации(один, два), в которых подробно рассматривалось, как создаётся и заливается в блокчейн сматр-контракт, поэтому сразу перейдём к коду.

Все действия проводятся в тестовой сети Rinkeby.

Смарт-контракт


Ядро нашей платёжной системы смарт-контракт, с него мы и начнём.

Функции контракта:

  1. приём платежей от пользователей
  2. вывод денег администратором
  3. возврат платежа администратором
  4. контроль пользовательских разрешений
  5. смена администратора
  6. хранение списка платежей
  7. блокирование повторной оплаты счета
  8. блокирование повторного возврата счета
  9. автоматический возврат средств отправленных на адрес контракта
  10. создание уведомлений об оплате, возврате денег, смене администратора

Код смарт-контракта с комментариями
pragma solidity ^0.4.18;
//version:4

/**
 * @title Ownable
 * @dev The Ownable contract has an owner address, and provides basic authorization control
 * functions, this simplifies the implementation of "user permissions".
 */
contract Ownable {
  address public owner;


  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);


  /**
   * @dev The Ownable constructor sets the original `owner` of the contract to the sender
   * account.
   */
  function Ownable() public {
    owner = msg.sender;
  }


  /**
   * @dev Throws if called by any account other than the owner.
   */
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }


  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) onlyOwner public {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }

}

contract PaymentSystem is Ownable {

    struct order {
        address payer;
        uint256 value;
        bool revert;
    }

    //база ордеров
    mapping(uint256 => order) public orders;

    //возврат денег при попытке отправить деньги на контракт
    function () public payable {
        revert();
    }

    event PaymentOrder(uint256 indexed id, address payer, uint256 value);

    //оплата ордера
    function paymentOrder(uint256 _id) public payable returns(bool) {
        require(orders[_id].value==0 && msg.value>0);

        orders[_id].payer=msg.sender;
        orders[_id].value=msg.value;
        orders[_id].revert=false;

        //создать евент
        PaymentOrder(_id, msg.sender, msg.value);

        return true;
    }

    event RevertOrder(uint256 indexed id, address payer, uint256 value);

    //возврат платежа администратором
    function revertOrder(uint256 _id) public onlyOwner returns(bool)  {
        require(orders[_id].value>0 && orders[_id].revert==false);

        orders[_id].revert=true;
        orders[_id].payer.transfer(orders[_id].value);

        RevertOrder(_id, orders[_id].payer, orders[_id].value);

        return true;
    }

    //вывод денег администратором
    function outputMoney(address _from, uint256 _value) public onlyOwner returns(bool) {
        require(this.balance>=_value);

        _from.transfer(_value);

        return true;
    }

}


Исходный код верифицирован на rinkeby.etherscan.io и как видно на вкладке “Contract Source” занимает всего 50 строк.

Пользовательский интерфейс


Конечно, можно попросить пользователей совершать платежи через Myetherwallet или Mist но, это неудобно поэтому лучше сделать на сайте форму оплаты. Для работы формы оплаты пользователь должен установить Metamask. Metamask автоматически подключает пользователя к своим RPC серверам.

Код платёжной формы
  <body>

    <div class="main_section">
      <h3 class="section_title">Pay</h3>

      <div class="edit"><input type="text" class="myedit" id="edit_id" placeholder="id"></div>
      <div class="edit"><input type="text" class="myedit" id="edit_value" placeholder="value (ETH)"></div>

      <div id="button_pay" class="mybutton">pay</div>
      <div id="message_pay" class="message"></div>

      <script>

        //оплата
        //-----------------------------------------------------------------

        var button_pay = document.querySelector('#button_pay');

        button_pay.addEventListener('click', function() {
          var pay_id = document.getElementById("edit_id").value;
          var pay_value = web3.toWei(parseFloat(document.getElementById("edit_value").value), 'ether')
          var user_adress = web3.eth.accounts[0];

          if (!web3.isAddress(user_adress)) {
            write_wessage("#message_pay", "error: MetaMask not open");
            return;
          }

          if (pay_id.length==0) {
            write_wessage("#message_pay", "error: not id");
            return;
          }

          if (pay_value==0) {
            write_wessage("#message_pay", "error: volume 0");
            return;
          }

          contract.paymentOrder(
            pay_id,
            {from: user_adress, value: pay_value, gasPrice: 41000000000},
            function (err, transaction_hash) {
              if (err) {
                write_wessage("#message_pay", "error");
                console.log(err);
              } else {
                write_wessage("#message_pay", "transaction hash: "+transaction_hash);
              }
          });

        });

      </script>

    </div>

  </body>

<script>

function write_wessage(element, message) {
  document.querySelector(element).innerHTML = message;
}


if (typeof web3 === 'undefined') {
  document.getElementsByTagName("body")[0].innerHTML = 'You need to install MetaMask';
} else {
  //инициализация контракта
  var contract_adress='0x3b4a22858093B9942514eE42eD1B4BF177632ba3';

  var abi=[ { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "", "type": "uint256" } ], "name": "orders", "outputs": [ { "name": "payer", "type": "address" }, { "name": "value", "type": "uint256" }, { "name": "revert", "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "previousOwner", "type": "address" }, { "indexed": true, "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "id", "type": "uint256" }, { "indexed": false, "name": "payer", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "PaymentOrder", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "id", "type": "uint256" }, { "indexed": false, "name": "payer", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "RevertOrder", "type": "event" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "outputMoney", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_id", "type": "uint256" } ], "name": "paymentOrder", "outputs": [ { "name": "", "type": "bool" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_id", "type": "uint256" } ], "name": "revertOrder", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "payable": true, "stateMutability": "payable", "type": "fallback" } ];

  var contract = web3.eth.contract(abi).at(contract_adress);
}

</script>


image
Также создадим страницу для взаимодействия администратора со смарт-контрактом.

Код страницы администратора
  <body>

    <div class="main_section">
      <h3 class="section_title">Info</h3>

      <div id="message_balance" class="message"></div>
      <div id="message_owner" class="message"></div>

    </div>

    <div class="main_section">
      <h3 class="section_title">Output money</h3>

      <div class="edit"><input type="text" class="myedit" id="edit_adress" placeholder="adress"></div>
      <div class="edit"><input type="text" class="myedit" id="edit_value" placeholder="value (ETH)"></div>

      <div id="button_output" class="mybutton">output</div>
      <div id="message_output" class="message"></div>

      <script>

        //вывод денег
        //-----------------------------------------------------------------

        var button_output = document.querySelector('#button_output');

        button_output.addEventListener('click', function() {
          var pay_value = web3.toWei(parseFloat(document.getElementById("edit_value").value), 'ether')
          var to_adress = document.getElementById("edit_adress").value;
          var user_adress = web3.eth.accounts[0];

          if (!web3.isAddress(user_adress)) {
            write_wessage("#message_output", "error: MetaMask not open");
            return;
          }

          if (!web3.isAddress(to_adress)) {
            write_wessage("#message_output", "error: adress not valid");
            return;
          }

          if (pay_value==0) {
            write_wessage("#message_output", "error: volume 0");
            return;
          }

          contract.outputMoney(
            to_adress,
            pay_value,
            {from: user_adress, gasPrice: 41000000000},
            function (err, transaction_hash) {
              if (err) {
                write_wessage("#message_output", "error");
                console.log(err);
              } else {
                write_wessage("#message_output", "transaction hash: "+transaction_hash);
              }
          });

        });

      </script>

    </div>

    <div class="main_section">
      <h3 class="section_title">Revert order</h3>

      <div class="edit"><input type="text" class="myedit" id="edit_id" placeholder="id"></div>

      <div id="button_revert" class="mybutton">revert</div>
      <div id="message_revert" class="message"></div>

      <script>

        //возврат денег
        //-----------------------------------------------------------------

        var button_revert = document.querySelector('#button_revert');

        button_revert.addEventListener('click', function() {
          var pay_id = document.getElementById("edit_id").value;
          var user_adress = web3.eth.accounts[0];

          if (!web3.isAddress(user_adress)) {
            write_wessage("#message_revert", "error: MetaMask not open");
            return;
          }

          if (pay_id.length==0) {
            write_wessage("#message_revert", "error: not id");
            return;
          }

          contract.revertOrder(
            pay_id,
            {from: user_adress, gasPrice: 41000000000},
            function (err, transaction_hash) {
              if (err) {
                write_wessage("#message_revert", "error");
                console.log(err);
              } else {
                write_wessage("#message_revert", "transaction hash: "+transaction_hash);
              }
          });

        });

      </script>

    </div>

  </body>

<script>

function write_wessage(element, message) {
  document.querySelector(element).innerHTML = message;
}


if (typeof web3 === 'undefined') {
  document.getElementsByTagName("body")[0].innerHTML = 'You need to install MetaMask';
} else {
  //инициализация контракта
  var contract_adress='0x3b4a22858093B9942514eE42eD1B4BF177632ba3';

  var abi=[ { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [ { "name": "", "type": "uint256" } ], "name": "orders", "outputs": [ { "name": "payer", "type": "address" }, { "name": "value", "type": "uint256" }, { "name": "revert", "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "previousOwner", "type": "address" }, { "indexed": true, "name": "newOwner", "type": "address" } ], "name": "OwnershipTransferred", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "id", "type": "uint256" }, { "indexed": false, "name": "payer", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "PaymentOrder", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": true, "name": "id", "type": "uint256" }, { "indexed": false, "name": "payer", "type": "address" }, { "indexed": false, "name": "value", "type": "uint256" } ], "name": "RevertOrder", "type": "event" }, { "constant": false, "inputs": [ { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "name": "outputMoney", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_id", "type": "uint256" } ], "name": "paymentOrder", "outputs": [ { "name": "", "type": "bool" } ], "payable": true, "stateMutability": "payable", "type": "function" }, { "constant": false, "inputs": [ { "name": "_id", "type": "uint256" } ], "name": "revertOrder", "outputs": [ { "name": "", "type": "bool" } ], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "constant": false, "inputs": [ { "name": "newOwner", "type": "address" } ], "name": "transferOwnership", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { "payable": true, "stateMutability": "payable", "type": "fallback" } ];

  var contract = web3.eth.contract(abi).at(contract_adress);

  //получаем баланс
  web3.eth.getBalance(contract_adress.toString(), function (err, result) {
    write_wessage("#message_balance", "contract balance: "+web3.fromWei(result, 'ether')+" ETH");
  });

  //получаем owner
  contract.owner(function(err, data) {
    if (err) {
      write_wessage("#message_owner", "error");
    } else {
      write_wessage("#message_owner", "owner: "+data);
    }
  });

}


</script>


image

Уведомления


Для автоматической обработки ордеров большинство платежных систем предоставляют API для отслеживания статуса платежа или уведомления о поступивших платежах. Блокчейн не отправляет уведомления о совершении платежей, но мы можем прочитать блоки и получить список событий созданных контрактом.

Для доступа к блокам будем использовать Geth с включенным RPC-HTTP сервером

geth --rinkeby --datadir "D:/eth/blockchain_rinkeby" --rpc --rpcaddr "0.0.0.0" --rpcapi "admin,debug,miner,shh,txpool,personal,eth,net,web3" console

Подключение к Geth я реализовал на php, но подойдет любая платформа, из которой можно выполнить POST запрос.

Для ускорения разработки я использовал ethereum-php.

Код для получения списка событий
<?php

require 'ethereum-php-master/ethereum.php';

$rate=0.000000000000000001;

//создаём новое подключение
$ethereum = new Ethereum('192.168.56.1', 8545);

//создаём новый фильтр
$filter = new Ethereum_Filter('0x0', 'latest', '0x3b4a22858093B9942514eE42eD1B4BF177632ba3', []);

//отправляем фильтр в ноду
$result_filter=$ethereum->eth_newFilter($filter);

//получаем список events
$logs=$ethereum->eth_getFilterLogs($result_filter);

foreach ($logs as $key => $value) {

  /*
  сравниваем первый элемент масива topics, в нем хранится хэш имени события и списка типов переменных
  строка: PaymentOrder(uint256,address,uint256) тип хэштрования: Keccak-256 (для получения хэша я воспользовался онлайн сервисом)
  в остальнх элементах topics хранятся проиндексированные параметры события
  */

  if (strcasecmp($value->{'topics'}[0], "0x"."c84883193d3a69d991d82f61928c06e179b647e413da4c20be80d8c0314c2e1b") == 0) {
    echo "Payment order id:".hexdec($value->{'topics'}[1]);

    /*
    в элементе data хранятся остальные параметры события
    склеенные по 32 байта
    */

    $data=str_split(substr($value->{'data'}, 2),64);

    echo " volume:".hexdec($data[1])*$rate." ETH";

    echo "<br>";
  }

}

?>


image

В своём скрипте я использовал метод eth_getFilterLogs с нулевого до последнего блока, естественно это не самый быстрый и эффективный вариант. Лучше ограничить eth_getFilterLogs по количеству блоков или использовать метод eth_getFilterChanges, который вернёт события только из новых блоков.

Полное описание методов JSON-RPC можно посмотреть в документации.

Заключение


Вот так довольно просто можно получить независимую платёжную систему и доработав подключить её к сайту. Думаю, на этом примере для многих станет понятнее, как и где можно применить блокчейн.

Смарт-контракт на rinkeby.etherscan.io
Репозиторий на GitHub