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

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

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

Во всех перечисленных сервисах нет проверки 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:

 '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.

 

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

Исходники кода: https://www.bitvanga.com/tgbot.zip
Рабочий пример телеграм-бота: @ApironeWalletBot