Ця інструкція проведе вас через створення робочого 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. Послідовність для кожної команди:
- Користувач надсилає текстову команду у Telegram.
- Бот парсить аргументи (адреса гаманця, сума).
- Бот формує RPC-запит через об'єкт Connection з @solana/web3.js.
- Devnet повертає результат або помилку.
- Бот формує текстову відповідь і надсилає її у чат.
Для команди відправки SOL додатково створюється, підписується та відправляється транзакція. Підпис відбувається локально за допомогою KeyPair бота — приватний ключ ніколи не покидає процес.
Налаштування бота
Створення бота через BotFather
- Відкрийте Telegram і знайдіть @BotFather.
- Надішліть команду /newbot.
- Вкажіть ім'я бота (відображається в списку контактів) та username (має закінчуватися на bot).
- 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 — адреса скопійована з помилкою.