В интернет-коммерции сейчас активно развивается автоматизация мессенджеров. Полезные боты растут, как грибы после дождя. Тут вам и магазины, и онлайн-помощники, инструменты для аналитики, и прочие важные и полезные решения. Современный бот в телеграмме может полноценно заменить целый сайт и вывести бизнес на новый уровень. Даже криптовалютный кошелек можно создать с помощью бота. Этим и займемся.

Для быстрого создания сервиса будем использовать PHP, возьмем одну из библиотек с core.telegram.org (https://core.telegram.org/bots/samples) php-telegram-bot  (https://github.com/php-telegram-bot/core/). Эта библиотека часто обновляется и подходит для дальнейшего масштабирования бота.

В свободном доступе есть несколько API-сервисов для работы с криптовалютами:

  1. https://www.coinpayments.net/apidoc

  2. https://confirmonetapi.docs.apiary.io

  3. https://www.blockchain.com/ru/api/blockchain_wallet_api

  4. https://www.alfacoins.com/developers/api-documentation

  5. https://apirone.com/docs/wallet

Во всех перечисленных сервисах нет проверки KYC и свободная регистрация. Blockchain.info требует получение токена для работы, он условно без проверки, но его получение может занять несколько дней или даже недель.

Выбор пал на сервис Apirone, потому что тут нет ограничений на создание кошельков и адресов.

Итак, наш бот будет уметь:

  • создавать криптовалютный кошелек в нужной валюте, 

  • получать баланс, 

  • генерировать криптовалютный адрес, 

  • обрабатывать обратную связь (колбэк) от криптовалютного процессинга, 

  • делать перевод имеющихся средств. 

Вся обработка колбэков от телеграма уже реализована в php-telegram-bot .

Создаем через @BotFather нового бота, получаем идентификатор бота и ключ API. Далее разворачиваем php-telegram-bot  с помощью Composer. Надо заметить, что для работы бота требуется SSL сертификат и действующее доменное имя. В конфиг composer.json добавляем дополнительно библиотеку monolog, которая требуется для отладки приложений. В секции autoload сразу укажем, что для классов будем использовать директорию Classes в папке проекта.

{
   "require": {
       "longman/telegram-bot": "^0.74.0",
       "monolog/monolog": "^2.2"
   },
   "autoload": {
       "classmap": ["Classes"]
   }
}

Запускаем composer update для создания базовой структуры. Создаем ее и добавляем туда папку  Apirone и в нее файл ApironeWallet.php, в котором реализуем класс взаимодействия с криптопроцессингом.

В нем реализованы следующие функции:

getWallets($user_id, $currencyId = null) //получить кошельки для telegram Пользователя
addWallet($user_id, $wallet, $address) //add generated wallet into DB
postWallet($user_id, $currency) //get wallet from Apirone
createWallets($user_id) //create all available wallets
getCurrencyId($currency,$units = null) //get currency id from DB
getAddress($chat_id, $user_id, $currency) //outputs wallet from DB
getAvailableCurrencies() //collect available currencies from DB
getCurrencyById($id, $units = null) // return currency by Id
getBalance($chat_id, $user_id, $wallet_id = null) // get wallet or wallets balance
checkAddress($currency, $address) // validate crypto address
estimate($user_id, $currencyName, $address, $amount) // pre-calculation of crypto transaction
transfer($user_id, $currencyName, $address, $amount) // transfer funds
explorerUrl($currency, $tx) // get explorer link

Подробно работу этих функций смотрим в примере. Создадим MySQL базу данных, она будет нужна для работы интерактивных диалогов. В нее требуется добавить три таблицы: apirone_currencies,  apirone_transactions и  apirone_users.

Настроим config.php:

<?php
 
return [
   // Add you bot's API key and name
   'api_key'      => 'key',
   'bot_username' => 'botName', // Without "@"
 
   // When using the getUpdates method, this can be commented out
   'webhook'      => [
       'url' => 'https://example.com/hook.php',
   ],
 
   'apirone_secret' => 'secret',
   'apirone_callback' => 'http://example.com/callback.php',
   // All command related configs go here
   'commands'     => [
       // Define all paths for your custom commands
       'paths'   => [
           __DIR__ . '/Commands',
       ],
 
   // Define all IDs of admin users
   'admins'       => [
       00000,
   ],
 
   // Enter your MySQL database credentials
   'mysql'        => [
       'host'     => 'localhost',
       'user'     => 'telegram_bot',
       'password' => 'password',
       'database' => 'telegram_bot',
   ],
 
   //Logging (Debug, Error and Raw Updates)
   'logging'  => [
       'debug'  => __DIR__ . '/php-telegram-bot-debug.log',
       'error'  => __DIR__ . '/php-telegram-bot-error.log',
       'update' => __DIR__ . '/php-telegram-bot-update.log',
   ],
 
   // Set custom Upload and Download paths
   'paths'        => [
       'download' => __DIR__ . '/Download',
       'upload'   => __DIR__ . '/Upload',
   ],
 
   // Requests Limiter (tries to prevent reaching Telegram API limits)
   'limiter'      => [
       'enabled' => true,
   ],
];

С телеграмом будем общаться с помощью вебхуков (Webhook). Для этого set.php и unset.php - включение и отключение колбэков. hook.php принимает сами колбэки из телеграма:

   $telegram->handle();

Установка и удаление вебхука выполняются в две функции:

   $result = $telegram->setWebhook($config['webhook']['url']);
   $result = $telegram->deleteWebhook();

В корне проекта нужна папка Commands, в нее запишем наши кастомные команды и изменим существующие. Из изменений в базовых классах нас интересует взаимодействие с меню не только через команды, начинающихся со слэша, но и просто текстовые команды. Для этого в папке Commands добавляем папку ServiceMessages и в ней создаем GenericmessageCommand.php , в который путем перечисления вносим нужные команды и реакции на них:

       // Fetch conversation command if it exists and execute it.
       if ($conversation->exists() && $command = $conversation->getCommand()) {
           return $this->telegram->executeCommand($command);
       }
      
       if($type === "text") {
           $text = $message->getText(true);
           if (stripos($text, 'Receive') === 0) {
               return $this->telegram->executeCommand('receive');
           }
           if (stripos($text, 'Menu') === 0) {
               return $this->telegram->executeCommand('menu');
           }
 
           if (stripos($text, 'Balance') === 0) {
               return $this->telegram->executeCommand('balance');
           }
           if (stripos($text, 'Send') === 0) {
               return $this->telegram->executeCommand('send');
           }
           return $this->replyToChat(
               'Command not found'
           );
       }

Также добавляем наши четыре кастомные команды BalanceCommand.php , MenuCommand.php , ReceiveCommand.php и SendCommand.php. 

Каждой команде делаем описание, использование по умолчанию, присваиваем версию.

   protected $name = 'balance';
   protected $description = 'Show balance of wallet';
   protected $usage = '/balance';
   protected $version = '1.2.0';

Сам код команды закладывается в функцию execute(). В ней происходит выполнение операций. Например, вот команда, которая смотрит баланс

       $message = $this->getMessage();
       $text    = $message->getText(true);
       $chat    = $message->getChat();
       $user    = $message->getFrom();
       $chat_id = $chat->getId();
       $user_id = $user->getId();
  	 $sql = '
       	SELECT *
       	FROM `apirone_currencies`
    	 ';
     $pdo = DB::getPdo();
     $sth = $pdo->prepare($sql);
     $sth->execute();
     $result = $sth->fetchAll(PDO::FETCH_ASSOC);
     foreach ($result as $currency) {
       if (stripos($text, $currency['name']) === 8) {
           $response = new ApironeWallet;
    		$response->getAddress($chat_id,$user_id,$currency['name']);
           return Request::emptyResponse();
       }
     }
     if ($text === 'Balance' || $text ==='') {
       $response = new ApironeWallet;
       $response->getBalance($chat_id, $user_id);
   }
 
   return Request::emptyResponse();

SendCommand.php будет интерактивным. В нем бот спросит криптовалюту, адрес для перевода средств и сумму платежа.

$this->conversation = new Conversation($user_id, $chat_id, $this->getName());
 
       // Load any existing notes from this conversation
       $notes = &$this->conversation->notes;
…
     // Every time a step is achieved the state is updated
 
       if($text === 'Cancel') {
           $state = 4;
           $notes['answer'] = 'Cancel';
       }
       if($text === 'Send') {
           $text = '';
       }
       switch ($state) {
           case 0:
               $currencies = $apirone->getAvailableCurrencies();
               if ($text === '' || !in_array(strtoupper($text), $currencies, true)) {
                  ...
                   $data['reply_markup'] = (new Keyboard(
                   [$currencies[0],$currencies[1]],
                   [$currencies[2],$currencies[3],$currencies[4]],
                   ['Cancel']))
                           ->setResizeKeyboard(true)
                           ->setOneTimeKeyboard(true)
                           ->setSelective(true);
              
                   $result = Request::sendMessage($data);
                   break;
               }
               $notes['currency'] = strtolower($text);
               $text          = '';
            case 1:
               if ($text === ''|| !$apirone->checkAddress($notes['currency'], $text)) {
                   $notes['state'] = 1;
                   $this->conversation->update();
 
                   $data['text'] = 'Type address for transfer:';
                   if ($text !== '') {
              ...
                   break;
               }
 
               $notes['address'] = $text;
               $text          = '';
 
           // No break!
           case 2:
                  …
           case 3:
               if ($text === '' || !in_array($text, ['Ok', 'Cancel'], true)) {
                   $notes['state'] = 3;
                   $this->conversation->update();
                   $currency = $apirone->getCurrencyId($notes['currency'], true);
                   $estimate = $apirone->estimate($user_id, $notes['currency'], $notes['address'], $notes['amount']);
                   if(isset($estimate['message'])) {
                       $data['text'] = 'Message from Apirone:'. PHP_EOL. '*'.$estimate['message'].'*' .PHP_EOL. 'Operation cancelled.';
                       $data['parse_mode'] = 'markdown';
                       $data['reply_markup'] = (new Keyboard(['Receive', 'Send'],
                       ['Balance']))
                           ->setResizeKeyboard(true)
                           ->setOneTimeKeyboard(true)
                           ->setSelective(true);
                       $this->conversation->stop();
                   } else {
                   $data['reply_markup'] = (new Keyboard(['Ok', 'Cancel']))
                       ->setResizeKeyboard(true)
                       ->setOneTimeKeyboard(true)
                       ->setSelective(true);
                   $data['text'] = 'Please double check that all data correct. Send "Ok" message in answer. If you want to stop sending type "Cancel"'. PHP_EOL;
...
                   if ($text !== '') {
                       $data['text'] = 'Simply type Ok or Cancel.';
                   }
                   }
                   $result = Request::sendMessage($data);
                   break;
               }
               $notes['answer'] = $text;
 
           // No break!
           case 4:
               $this->conversation->update();
               unset($notes['state']);
               if($notes['answer'] === 'Ok') {
                   $transfer = $apirone->transfer($user_id, $notes['currency'], $notes['address'], $notes['amount']);
 
                   if(isset($transfer['message'])) {
                       $data['text'] = 'Message from Apirone:'. PHP_EOL. '*'.$transfer['message'].'*' .PHP_EOL. 'Operation cancelled.';
                    ...
                       $this->conversation->stop();
                   } else {
                       $data['text'] = 'Transfer successfully complete.'.PHP_EOL.
                       'Transactions:'. PHP_EOL ;
                       foreach ($transfer['txs'] as $tx) {
                           $data['text'].= $tx .PHP_EOL. $apirone->explorerUrl($notes['currency'], $tx).PHP_EOL;
                       };
                   }
               } 
...
               $result = Request::sendMessage($data);
               $this->conversation->stop();
               break;
       }
       return $result;

Создаем StartCommand.php И в нем при первом обращении генерируем данные для нашей базы данных

       $message = $this->getMessage();
       $user_id = $message->getFrom()->getId();
       $apirone = new ApironeWallet;
       $apirone->createWallets($user_id);
       return $this->replyToChat(
           'Welcome to Apirone wallet bot! You are ready to use BTC,BCH,DOGE and LTC wallets right now' . PHP_EOL .
           'Type /menu to start using it now!'
       );

В корне проекта создаем callback.php . Здесь будет приниматься колбэк от процессинга:

require_once __DIR__ . '/vendor/autoload.php';
use Longman\TelegramBot\Request;
use Classes\Apirone\ApironeWallet;
 
$config = require __DIR__ . '/config.php';
 
$telegram = new Longman\TelegramBot\Telegram($config['api_key'], $config['bot_username']);
$apirone = new ApironeWallet;
$telegram->enableMySql($config['mysql']);;
 
$apironeData = file_get_contents('php://input');
 
if ($apironeData) {
   $params = json_decode($apironeData, true);
 
   // check your secret code
   if ($params["data"]["secret"] !== $config['apirone_secret']) die();
 
   $user_id = $params["data"]["user_id"];
   $input_address = $params["input_address"];
   $input_transaction_hash = $params["input_transaction_hash"];
   $value_in_satoshi = $params["value"];
 
   $wallets = $apirone->getWallets($user_id);
 
   foreach ($wallets as $wallet) {
      if($wallet['wallet_id'] === $params['wallet']) {
       $currency = $apirone->getCurrencyById($wallet['currency'], true);
      }
   }
 
   //Save unconfirmed transactions and data to your Database.
       $data['chat_id'] = $user_id;
       $data['parse_mode'] = 'markdown';
   if ($params["confirmations"] < 2 ) {
       $data['text'] = '*'.strtoupper($currency['name']) .' wallet*: '. PHP_EOL .
       "Transaction ". $input_transaction_hash . PHP_EOL.
       'Waiting *'. number_format($value_in_satoshi/pow(10, $currency['units-factor']), 8, '.', ''). strtoupper($currency['name']) .'*'. PHP_EOL .
       $params["confirmations"].' of 2 confirmations received'. PHP_EOL .
       $apirone->explorerUrl($currency['name'], $input_transaction_hash);  
   }
  
   if ($params["confirmations"] >= 2) {
       $balance = $apirone->getBalance($user_id, $user_id, $params['wallet']);
       $data['text'] = '*'.strtoupper($currency['name']) .' wallet*: '. PHP_EOL .
       "Transaction ". $input_transaction_hash . PHP_EOL.
       'Payment successfully received!'. PHP_EOL .'Amount: *'. number_format($value_in_satoshi/pow(10, $currency['units-factor']), 8, '.', ''). strtoupper($currency['name']) .'*'. PHP_EOL.
       'Current balance:'. PHP_EOL .
       $balance . PHP_EOL.
       $apirone->explorerUrl($currency['name'], $input_transaction_hash);
       echo "*ok*";
   }
   return Request::sendMessage($data);

В итоговом результате имеем простого бота, который в дальнейшем можно будет трансформировать и дорабатывать, т.к. заложена серьезная база в виде  php-telegram-bot.

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

Комментарии (10)


  1. derikn_mike
    12.08.2021 22:37
    -1

    а почему это бесплатно?

    ведь если создать тот же кошелек напрямую в блокчейне это стоит денег , когда вы пользуетесь стороним API , неужто сервис за вас платит?


    1. atd
      12.08.2021 23:55
      +2

      если создать тот же кошелек напрямую в блокчейне это стоит денег

      Дело в том, что это не стоит ничего. Более того, нельзя создать «кошелёк в блокчейне», кошелёк это просто адрес и приватный ключ от него, это можно сделать локально.

      А сервисы эти зарабатывают на тех кому 1. лень разбираться что к чему, или 2. нет возможности хостить свою ноду. Если внимательно посмотреть по ссылкам, то деньги многие берут либо за переводы, либо при большом количестве транзакции, так что они в накладе не останутся ))


      1. MBoyarov Автор
        13.08.2021 10:27

        Да, для небольших интернет-магазинов или ботов, нет даже смысла ставить ноду, обслуживать сервера и писать коннекторы. Используя стороннее API, можно мгновенно разворачивать свои проекты и приступать к работе.
        PS: У этих ребят, приём платежей бесплатный, есть только фиксированная комиссия за вывод.


        1. atd
          13.08.2021 15:13

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

          обслуживать сервера

          ноды, если они вам точно нужны, обычно необслуживаемые: развернули контейнер и вперёд

          писать коннекторы

          у большинства нод есть вполне удобный http-api, ничем не сложнее (а местами даже проще), чем тот, который используете вы.

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


          1. MBoyarov Автор
            13.08.2021 22:17
            -1

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

            Боюсь, чтобы собрать большое количество txout, подписать и забродкастить транзакцию, вам просто так не дадут. В какой то момент, у "бесплатных" сервисов начнётся платная подписка или вам ограничат лимиты.

            ноды, если они вам точно нужны, обычно необслуживаемые: развернули контейнер и вперёд

            да, но для небольших проектов покупать за 50-100 долларов хост и разворачивать ноду смысла нет. К тому же, периодически обновляются версии и за этим нужно следить. Так же, ноды иногда ре-синхронизируются, выпадая из работы от 5 до 30 минут, т.е. вам потребуется вторая для бесперебойной работы и балансир между ними.

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

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


            1. WaXe
              13.08.2021 23:15
              +2

              Поначалу я даже и не понял как от api Blockchain.info вы пришли к мало кому известному Apirone, без какого-либо сравнения

              … потому что тут нет ограничений на создание кошельков и адресов...
              Таких ограничений и быть не может, т.к. создать кошелек и адрес можно даже оффлайн. И в случае blockchain.info далее нужно будет скормить ему xPub (да, да, вместе с api ключом) для создания инвойса. А уж распоряжаться средставами сможет только владелец с приватным ключом от этого xPub и не важно с помощью какого кошелька.

              PS: У этих ребят, приём платежей бесплатный, есть только фиксированная комиссия за вывод.
              Но вот тут, судя по вашему сообщению, владеет всем кошельком только аpirone и тут совсем неясно кому доверяются средства.

              И наверное сразу стоило написать, не «У этих ребят», а «У нас»
              Maksim Boyarov — founder — Apirone.com | LinkedIn


              Я не спорю, сервис может быть достаточно продуман и хорошо реализован, но вот так сходу, ткнув пальцем в небо, без каких либо разъяснений и проверок я бы не побежал пользоваться незнакомым никому сервисом. Уж лучше все будет организовано средствами Electrum, извините


  1. MBoyarov Автор
    13.08.2021 11:15
    -1

    Пишите лично, если вам нужны исходники и действующий пример бота.


    1. Movmov
      19.08.2021 04:45

      Hi! Please is it possible to provide sources and a functional working bot?


      1. MBoyarov Автор
        19.08.2021 04:46

        Hi
        Yes, sure. Link to archive https://www.bitvanga.com/tgbot.zip


        1. MBoyarov Автор
          19.08.2021 04:46

          TG bot example: @ApironeWalletBot