Ця інструкція проведе вас через створення робочого Telegram-бота, який взаємодіє з блокчейном Solana на Devnet. Бот зможе перевіряти баланси, отримувати тестові SOL та відправляти транзакції. Це навчальний проєкт — він не призначений для production і працює виключно з Devnet-кластером.

Середовище: Node.js 18.x або 20.x, ОС без значення
Залежності: @solana/web3.js 1.95.x, node-telegram-bot-api 0.64.x
Кластер: Devnet (https://api.devnet.solana.com)
Передумови: встановлений Node.js, обліковий запис у Telegram, базове розуміння JavaScript

Архітектура бота

Telegram Bot API + Solana SDK (Node.js)

Бот — це єдиний Node.js-процес, який виконує дві ролі одночасно:

  • Telegram-клієнт — слухає повідомлення користувачів через бібліотеку node-telegram-bot-api (довге опитування, long polling). Це найпростіший варіант для навчання: не потрібен вебсервер, SSL-сертифікат чи домен.
  • Solana-клієнт — формує та відправляє запити до Devnet через @solana/web3.js. Кожна команда користувача перетворюється на один або кілька RPC-викликів.

Дані не зберігаються в базу — бот Stateless. Кожне повідомлення обробляється незалежно. Для навчального проєкту цього достатньо.

Як бот взаємодіє з Devnet

Бот підключається до публічного RPC-вузла Devnet за HTTP. Послідовність для кожної команди:

  1. Користувач надсилає текстову команду у Telegram.
  2. Бот парсить аргументи (адреса гаманця, сума).
  3. Бот формує RPC-запит через об'єкт Connection з @solana/web3.js.
  4. Devnet повертає результат або помилку.
  5. Бот формує текстову відповідь і надсилає її у чат.

Для команди відправки SOL додатково створюється, підписується та відправляється транзакція. Підпис відбувається локально за допомогою KeyPair бота — приватний ключ ніколи не покидає процес.

Налаштування бота

Створення бота через BotFather

  1. Відкрийте Telegram і знайдіть @BotFather.
  2. Надішліть команду /newbot.
  3. Вкажіть ім'я бота (відображається в списку контактів) та username (має закінчуватися на bot).
  4. BotFather поверне токен — рядок виду 1234567890:AAH.... Збережіть його.

Токен — єдиний ідентифікатор для доступу до вашого бота. Не публікуйте його у відкритих репозиторіях.

Залежності: node-telegram-bot-api, @solana/web3.js

Створіть каталог проєкту та ініціалізуйте його:

mkdir solana-telegram-bot
cd solana-telegram-bot
npm init -y
npm install [email protected] @solana/[email protected]
npm install -d [email protected]

Створіть файл .env у корені проєкту:

BOT_TOKEN=ваш_токен_від_BotFather
PRIVATE_KEY=ваш_приватний_ключ_у_форматі_base58
RPC_ENDPOINT=https://api.devnet.solana.com

Щоб отримати приватний ключ у форматі base58, створіть або імпортуйте гаманець через solana-cli або згенеруйте KeyPair програмно (тільки для Devnet, ніколи не використовуйте такий гаманець для реальних коштів).

Додайте .env до .gitignore одразу:

echo ".env" >> .gitignore

Команди бота

Бот підтримує три команди. Усі працюють виключно з Devnet.

/balance <адреса> — повертає баланс гаманця в SOL. Адреса — це Base58-encoded публічний ключ (32 байти, 43–44 символи). Бот запитує getBalance через RPC і конвертує лампортів у SOL (1 SOL = 1 000 000 000 lamports).

/airdrop <адреса> — запитує тестові SOL з Devnet-крану через requestAirdrop. Ліміт: 2 SOL за один запит, до 5 SOL на одну адресу загалом. Транзакція потребує підтвердження, тому бот чекає confirmTransaction перед відповіддю.

/send <адреса_отримувача> <сума> — відправляє вказану кількість SOL з гаманця бота на вказану адресу. Бот створює транзакцію з інструкцією SystemProgram.transfer, підписує її своїм KeyPair і відправляє у мережу. Сума вказується в SOL, всередині конвертується в лампортів.

Реалізація команд

Обробка повідомлень та виклики SDK

Створіть файл index.js у корені проєкту. Повний код навчального бота:

require('dotenv').config();
const TelegramBot = require('node-telegram-bot-api');
const {
  Connection,
  PublicKey,
  Keypair,
  SystemProgram,
  Transaction,
  LAMPORTS_PER_SOL
} = require('@solana/web3.js');

const token = process.env.BOT_TOKEN;
const privateKey = process.env.PRIVATE_KEY;
const rpcEndpoint = process.env.RPC_ENDPOINT;

if (!token || !privateKey || !rpcEndpoint) {
  throw new Error('Перевірте .env: BOT_TOKEN, PRIVATE_KEY, RPC_ENDPOINT мають бути заповнені');
}

const bot = new TelegramBot(token, { polling: true });
const connection = new Connection(rpcEndpoint, 'confirmed');

// Відновлення KeyPair з base58-рядка
const keypair = Keypair.fromSecretKey(
  Buffer.from(privateKey, 'base58')
);

// Допоміжна функція: валідація адреси Solana
function isValidSolanaAddress(address) {
  try {
    new PublicKey(address);
    return true;
  } catch {
    return false;
  }
}

// /balance <адреса>
bot.onText(/\/balance (.+)/, async (msg, match) => {
  const chatId = msg.chat.id;
  const address = match[1].trim();

  if (!isValidSolanaAddress(address)) {
    return bot.sendMessage(chatId, 'Невірний формат адреси. Перевірте та спробуйте знову.');
  }

  try {
    const publicKey = new PublicKey(address);
    const balance = await connection.getBalance(publicKey);
    const sol = balance / LAMPORTS_PER_SOL;
    bot.sendMessage(chatId, `Баланс ${address}: ${sol} SOL`);
  } catch (error) {
    bot.sendMessage(chatId, `Помилка запиту балансу: ${error.message}`);
  }
});

// /airdrop <адреса>
bot.onText(/\/airdrop (.+)/, async (msg, match) => {
  const chatId = msg.chat.id;
  const address = match[1].trim();

  if (!isValidSolanaAddress(address)) {
    return bot.sendMessage(chatId, 'Невірний формат адреси. Перевірте та спробуйте знову.');
  }

  try {
    const publicKey = new PublicKey(address);
    const signature = await connection.requestAirdrop(
      publicKey,
      2 * LAMPORTS_PER_SOL
    );
    await connection.confirmTransaction(signature, 'confirmed');
    bot.sendMessage(
      chatId,
      `Airdrop виконано. 2 SOL надіслано на ${address}.` +
      `\nТранзакція: ${signature}`
    );
  } catch (error) {
    bot.sendMessage(chatId, `Помилка airdrop: ${error.message}`);
  }
});

// /send <адреса> <сума>
bot.onText(/\/send (\S+) (\S+)/, async (msg, match) => {
  const chatId = msg.chat.id;
  const toAddress = match[1].trim();
  const amountStr = match[2].trim();

  if (!isValidSolanaAddress(toAddress)) {
    return bot.sendMessage(chatId, 'Невірна адреса отримувача.');
  }

  const amount = parseFloat(amountStr);
  if (isNaN(amount) || amount <= 0) {
    return bot.sendMessage(chatId, 'Сума має бути додатним числом.');
  }

  try {
    const toPublicKey = new PublicKey(toAddress);
    const lamports = amount * LAMPORTS_PER_SOL;

    const transaction = new Transaction().add(
      SystemProgram.transfer({
        fromPubkey: keypair.publicKey,
        toPubkey: toPublicKey,
        lamports: lamports,
      })
    );

    const signature = await connection.sendTransaction(transaction, [keypair]);
    await connection.confirmTransaction(signature, 'confirmed');

    bot.sendMessage(
      chatId,
      `Відправлено ${amount} SOL на ${toAddress}.` +
      `\nТранзакція: ${signature}`
    );
  } catch (error) {
    bot.sendMessage(chatId, `Помилка відправки: ${error.message}`);
  }
});

// Обробка невідомих повідомлень
bot.on('message', (msg) => {
  const chatId = msg.chat.id;
  if (!msg.text.startsWith('/')) {
    bot.sendMessage(
      chatId,
      'Доступні команди:\n/balance <адреса>\n/airdrop <адреса>\n/send <адреса> <сума>'
    );
  }
});

console.log('Бот запущено. Очікуювання повідомлень...');

Очікуваний результат запуску: у терміналі з'являється рядок «Бот запущено. Очікуювання повідомлень...». Бот реагує на команди у Telegram.

Перевірка: відкрийте бота у Telegram, надішліть /balance з будь-якою Devnet-адресою — має повернутися число (може бути 0, якщо адреса нова).

Безпечний відкат: щоб зупинити бота, натисніть Ctrl+C у терміналі. Ніякі зміни в блокчейні не залишаються активними без вашої явної команди.

Формування відповідей користувачу

У навчальному боті відповіді — прості текстові повідомлення. Кожна містить результат або опис помилки. Для production-рішень варто розглянути inline-клавіатури, форматування Markdown та асинхронну обробку з індикатором «бот друкує...», але це виходить за межі поточного проєкту.

Ключове правило: бот ніколи не розкриває приватний ключ, логи помилок містять лише message без стеку, а суми завжди показуються в SOL, не в лампортах.

Типові помилки

Бот не відповідає (помилка підключення до RPC)

Симптом: бот запускається, але на будь-яку команду відповідає помилкою з текстом на кшталт «fetch failed» або «connection refused».

Причина: публічний Devnet-вузол (api.devnet.solana.com) має обмеження за частотою запитів (rate limit). Якщо ви тестуєте часто або з кількох місць одночасно, RPC може тимчасово відхиляти запити.

Рішення:

  • Додайте затримку між запитами або кешуйте результати балансу на 10–30 секунд.
  • Використовуйте альтернативний Devnet-вузол від провайдера (Helius, QuickNode, Triton) — вони надають безкоштовні тарифи для Devnet.
  • Перевірте, що змінна RPC_ENDPOINT у .env не містить зайвих пробілів або лапок.

Діагностика: виконайте у терміналі окремий запит до RPC, щоб переконатися, що вузол доступний:

curl -X POST https://api.devnet.solana.com -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}'

Якщо відповідь містить «ok» — вузол працює, проблема на боці коду або мережі.

Невірна адреса гаманця у команді

Симптом: бот відповідає «Невірний формат адреси» навіть на правильну, на ваш погляд, адресу.

Причини:

  • Зайві символи: користувач скопіював адресу з пробілом на кінці, з розривом рядка або з маркером посилання (якщо адреса була у вигляді гіперпосилання у чаті). Регулярний вираз у боті захоплює перший «слово» після команди, тому пробіл обрізається, але внутрішні пробіли або невидимі символи (zero-width space) — ні.
  • Адреса з іншої мережі: Ethereum-адреса (0x...) або Bitcoin-адреса не є валідними для Solana. Конструктор PublicKey викидає помилку, яку бот перехоплює.
  • Скорочена адреса: деякі інтерфейси показують адреси у вигляді `Bp3k...7qRc`. Це не повна адреса, і PublicKey її не прийме.

Рішення: у навчальному боті користувач має надсилати повну адресу (43–44 символи Base58). Для production-рішення можна додати попередню очистку рядка від пробілів та перевірку довжини перед створенням PublicKey.

Додаткова перевірка: якщо бот видає «Невірний формат адреси» на адресу, яка виглядає правильно, виведіть її довжину в лог:

console.log('Отримано адресу:', address, 'довжина:', address.length);

Якщо довжина відрізняється від 43–44 — адреса скопійована з помилкою.

Джерела