Бот с внутренним балансом
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
В этой статье мы создадим простого Telegram-бота для приема платежей в TON.
🦄 Как это выглядит
Бот будет выглядеть следующим образом:
Исходный код
Исходники доступны на GitHub:
📖 Чему Вы научитесь
Вы узнаете, как:
- Создать Telegram бота в Python3 с помощью Aiogram
- Работать с базами данных SQLITE
- Работать с открытым API TON
✍️ Что нужно для начала работы
Установите Python, если вы этого еще не сделали.
Также вам понадобятся эти библиотеки PyPi:
- aiogram
- requests
Вы можете установить их одной командой в терминале.
pip install aiogram==2.21 requests
🚀 Давайте начнем!
Создайте директорию для нашего бота с четырьмя файлами в ней:
bot.py
- программа для запуска Telegram ботаconfig.py
- файл конфигурацииdb.py
- модуль для взаимодействия с базой данных sqlite3ton.py
- модуль для работы с платежами в TON
Директория должна выглядеть следующим образом:
my_bot
├── bot.py
├── config.py
├── db.py
└── ton.py
Теперь давайте начнем писать код!
Конфигурация
Давайте начнем с config.py
, потому что он самый маленький. Нам просто нужно задать в нем несколько параметров.
config.py
BOT_TOKEN = 'YOUR BOT TOKEN'
DEPOSIT_ADDRESS = 'YOUR DEPOSIT ADDRESS'
API_KEY = 'YOUR API KEY'
RUN_IN_MAINNET = True # Switch True/False to change mainnet to testnet
if RUN_IN_MAINNET:
API_BASE_URL = 'https://toncenter.com'
else:
API_BASE_URL = 'https://testnet.toncenter.com'
Здесь вам нужно заполнить значения в первых трех строках:
BOT_TOKEN
- это ваш токен Telegram бота, который вы можете получить после [создания бота] (https://t.me/BotFather).DEPOSIT_ADDRESS
- это адрес кошелька вашего проекта, который будет принимать все платежи. Вы можете просто создать новый кошелек TON и скопировать его адрес.API_KEY
- это ваш API-ключ от TON Center, который вы можете получить в этом боте.
Вы также можете выбрать, будет ли ваш бот работать в тестовой или в основной сети (4-я линия).
Это все, что касается файла конфигурации, поэтому мы можем двигаться дальше!
База данных
Теперь давайте отредактируем файл db.py
, который будет работать с базой данных нашего бота.
Импортируйте библиотеку sqlite3.
import sqlite3
Инициализируйте подключение к базе данных и курсор (вы можете указать любое имя файла вместо db.sqlite
).
con = sqlite3.connect('db.sqlite')
cur = con.cursor()
Чтобы хранить информацию о пользователях (в нашем случае их балансы), создайте таблицу "Users" со строками User ID и Balance.
cur.execute('''CREATE TABLE IF NOT EXISTS Users (
uid INTEGER,
balance INTEGER
)''')
con.commit()
Теперь нам нужно декларировать несколько функций для работы с базой данных.
Функция add_user
будет использоваться для вставки новых пользователей в базу данных.
def add_user(uid):
# new user always has balance = 0
cur.execute(f'INSERT INTO Users VALUES ({uid}, 0)')
con.commit()
Функция check_user
будет использоваться для проверки того, существует ли пользователь в базе данных или нет.
def check_user(uid):
cur.execute(f'SELECT * FROM Users WHERE uid = {uid}')
user = cur.fetchone()
if user:
return True
return False
Функция add_balance
будет использоваться для увеличения баланса пользователя.
def add_balance(uid, amount):
cur.execute(f'UPDATE Users SET balance = balance + {amount} WHERE uid = {uid}')
con.commit()
Функция get_balance
будет использоваться для получения баланса пользователя.
def get_balance(uid):
cur.execute(f'SELECT balance FROM Users WHERE uid = {uid}')
balance = cur.fetchone()[0]
return balance
Вот и все для файла db.py
!
Теперь мы можем использовать эти четыре функции в других компонентах бота для работы с базой данных.
TON Center API
В файле ton.py
мы декларируем функцию, которая будет обрабатывать все новые депозиты, увеличивать баланс пользователей и уведомлять их.
Метод getTransactions
Мы будем использовать TON Center API. Их документация доступна здесь: https://toncenter.com/api/v2/.
Нам нужен метод getTransactions, чтобы получить информацию о последних транзакциях по данному счету.
Давайте посмотрим, что этот метод принимает в качестве входных параметров и что он возвращает.
Здесь есть только одно обязательное поле ввода address
, но нам также нужно поле limit
, чтобы указать, сколько транзакций мы хотим получить в ответ.
Теперь давайте попробуем запустить этот метод на сайте TON Center с любым существующим адресом кошелька, чтобы понять, что мы должны получить на выходе.
{
"ok": true,
"result": [
{
...
},
{
...
}
]
}
Итак, когда все в порядке, в поле ok
устанавливается значение true
и мы получаем массив result
со списком последних транзакций limit
. Теперь давайте рассмотрим одну единственную транзакцию:
{
"@type": "raw.transaction",
"utime": 1666648337,
"data": "...",
"transaction_id": {
"@type": "internal.transactionId",
"lt": "32294193000003",
"hash": "ez3LKZq4KCNNLRU/G4YbUweM74D9xg/tWK0NyfuNcxA="
},
"fee": "105608",
"storage_fee": "5608",
"other_fee": "100000",
"in_msg": {
"@type": "raw.message",
"source": "EQBIhPuWmjT7fP-VomuTWseE8JNWv2q7QYfsVQ1IZwnMk8wL",
"destination": "EQBKgXCNLPexWhs2L79kiARR1phGH1LwXxRbNsCFF9doc2lN",
"value": "100000000",
"fwd_fee": "666672",
"ihr_fee": "0",
"created_lt": "32294193000002",
"body_hash": "tDJM2A4YFee5edKRfQWLML5XIJtb5FLq0jFvDXpv0xI=",
"msg_data": {
"@type": "msg.dataText",
"text": "SGVsbG8sIHdvcmxkIQ=="
},
"message": "Hello, world!"
},
"out_msgs": []
}
Мы видим, что информация, которая может помочь нам точно идентифицировать транзакцию, хранится в поле transaction_id
. Нам нужно поле lt
из него, чтобы понять, какая транзакция произошла раньше, а какая позже.
Информация о передаче монеты находится в поле in_msg
. Нам понадобятся value
и message
из него.
Теперь мы готовы создать обработчик платежей.
Отправка API-запросов из кода
Давайте начнем с импорта необходимых библиотек и двух наших предыдущих файлов: config.py
и db.py
.
import requests
import asyncio
# Aiogram
from aiogram import Bot
from aiogram.types import ParseMode
# We also need config and database here
import config
import db
Давайте подумаем, как может быть реализована обработка платежей.
Мы можем вызывать API каждые несколько секунд и проверять, есть ли новые транзакции по адресу нашего кошелька.
Для этого нам нужно знать, какой была последняя обработанная транзакция. Самым простым подходом было бы просто сохранить информацию об этой транзакции в каком-нибудь файле и обновлять ее каждый раз, когда мы обрабатываем новую транзакцию.
Какую информацию о транзакции мы будем хранить в файле? На самом деле, нам нужно хранить только значение lt
- logical time (логическое время).
С помощью этого значения мы сможем понять, какие транзакции нам нужно обработать.
Поэтому нам нужно определить новую асинхронную функцию; назовем ее start
. Почему эта функция должна быть асинхронной? Потому что библиотека Aiogram для ботов Telegram также является асинхронной, и в дальнейшем с асинхронными функциями будет проще работать.
Вот как должна выглядеть наша функция start
:
async def start():
try:
# Try to load last_lt from file
with open('last_lt.txt', 'r') as f:
last_lt = int(f.read())
except FileNotFoundError:
# If file not found, set last_lt to 0
last_lt = 0
# We need the Bot instance here to send deposit notifications to users
bot = Bot(token=config.BOT_TOKEN)
while True:
# Here we will call API every few seconds and fetch new transactions.
...
Теперь давайте напишем тело цикла while. Нам нужно вызывать TON Center API каждые несколько секунд.
while True:
# 2 Seconds delay between checks
await asyncio.sleep(2)
# API call to TON Center that returns last 100 transactions of our wallet
resp = requests.get(f'{config.API_BASE_URL}/api/v2/getTransactions?'
f'address={config.DEPOSIT_ADDRESS}&limit=100&'
f'archival=true&api_key={config.API_KEY}').json()
# If call was not successful, try again
if not resp['ok']:
continue
...
После вызова requests.get
у нас есть переменная resp
, которая содержит ответ от API. resp
- это объект, а resp['result']
- список с последними 100 транзакциями для нашего адреса.
Теперь давайте просто пройдемся итерацией по этим транзакциям и найдем новые.
while True:
...
# Iterating over transactions
for tx in resp['result']:
# LT is Logical Time and Hash is hash of our transaction
lt, hash = int(tx['transaction_id']['lt']), tx['transaction_id']['hash']
# If this transaction's logical time is lower than our last_lt,
# we already processed it, so skip it
if lt <= last_lt:
continue
# at this moment, `tx` is some new transaction that we haven't processed yet
...
Как нам обработать новую транзакцию? Нам необходимо:
- понять, какой пользователь отправил его
- увеличить баланс этого пользователя
- уведомить пользователя о его депозите
Вот код, который все это сделает:
while True:
...
for tx in resp['result']:
...
# at this moment, `tx` is some new transaction that we haven't processed yet
value = int(tx['in_msg']['value'])
if value > 0:
uid = tx['in_msg']['message']
if not uid.isdigit():
continue
uid = int(uid)
if not db.check_user(uid):
continue
db.add_balance(uid, value)
await bot.send_message(uid, 'Deposit confirmed!\n'
f'*+{value / 1e9:.2f} TON*',
parse_mode=ParseMode.MARKDOWN)
Давайте посмотрим на него и разберемся, что он делает.
Вся информация о передаче монеты находится в tx['in_msg']
. Нам нужны только поля 'value' и 'message'.
Прежде всего, мы проверяем, больше ли значение нуля, и продолжаем только в том случае, если это так.
Затем мы ожидаем, что при передаче комментарий ( tx['in_msg']['message']
) будет иметь идентификатор пользователя от нашего бота, поэтому мы проверяем, является ли он действительным номером и существует ли этот UID в нашей базе данных.
После этих простых проверок у нас есть переменная value
с суммой депозита и переменная uid
с идентификатором пользователя, который сделал этот депозит. Таким образом, мы можем просто добавить средства на его счет и отправить уведомление.
Также обратите внимание, что по умолчанию значение указано в нанотонах, поэтому нам нужно разделить его на 1 миллиард. Мы делаем это в соответствии с уведомлением:
{value / 1e9:.2f}
Здесь мы делим значение на 1e9
(1 миллиард) и оставляем только две цифры после запятой, чтобы показать его пользователю в удобном формате.
Отлично! Теперь программа может обрабатывать новые транзакции и уведомлять пользователей о депозитах. Но мы не должны забывать о сохранении lt
, которое мы использовали ранее. Мы должны обновить последнее lt
, потому что была обработана более новая транзакция.
Все просто:
while True:
...
for tx in resp['result']:
...
# we have processed this tx
# lt variable here contains LT of the last processed transaction
last_lt = lt
with open('last_lt.txt', 'w') as f:
f.write(str(last_lt))
И это все для файла ton.py
!
Теперь наш бот готов на 3/4. Нам осталось только создать пользовательский интерфейс с несколькими кнопками в самом боте.
Telegram бот
Инициализация
Откройте файл bot.py
и импортируйте все необходимые модули.
# Logging module
import logging
# Aiogram imports
from aiogram import Bot, Dispatcher, types
from aiogram.dispatcher.filters import Text
from aiogram.types import ParseMode, ReplyKeyboardMarkup, KeyboardButton, \
InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.utils import executor
# Local modules to work with the Database and TON Network
import config
import ton
import db
Давайте настроим ведение журнала в нашей программе, чтобы мы могли видеть, что происходит позже, для отладки.
logging.basicConfig(level=logging.INFO)
Теперь нам нужно инициализировать bot object и dispatcher с помощью Aiogram.
bot = Bot(token=config.BOT_TOKEN)
dp = Dispatcher(bot)
Здесь мы используем BOT_TOKEN
из нашего файла конфигурации, который мы сделали в начале урока.
Мы инициализировали бота, но он все еще пуст. Мы должны добавить несколько функций для взаимодействия с пользователем.
Обработчики сообщений
Команда /start
Начнем с обработчика команд /start
и /help
. Эта функция будет вызываться, когда пользователь запускает бота в первый раз, перезапускает его или использует команду /help
.
@dp.message_handler(commands=['start', 'help'])
async def welcome_handler(message: types.Message):
uid = message.from_user.id # Not neccessary, just to make code shorter
# If user doesn't exist in database, insert it
if not db.check_user(uid):
db.add_user(uid)
# Keyboard with two main buttons: Deposit and Balance
keyboard = ReplyKeyboardMarkup(resize_keyboard=True)
keyboard.row(KeyboardButton('Deposit'))
keyboard.row(KeyboardButton('Balance'))
# Send welcome text and include the keyboard
await message.answer('Hi!\nI am example bot '
'made for [this article](docs.ton.org/v3/guidelines/dapps/tutorials/telegram-bot-examples/accept-payments-in-a-telegram-bot-2).\n'
'My goal is to show how simple it is to receive '
'payments in Toncoin with Python.\n\n'
'Use keyboard to test my functionality.',
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN)
Приветственное сообщение может быть любым, каким вы захотите. Кнопки клавиатуры могут быть любыми, но в данном примере они обозначены наиболее понятным для нашего бота образом: Deposit
и Balance
.
Кнопка баланса
Теперь пользователь может запустить бота и увидеть клавиатуру с двумя кнопками. Но после вызова одной из них пользователь не получит никакого ответа, потому что мы не создали для нее никакой функции.
Поэтому давайте добавим функцию запроса баланса.
@dp.message_handler(commands='balance')
@dp.message_handler(Text(equals='balance', ignore_case=True))
async def balance_handler(message: types.Message):
uid = message.from_user.id
# Get user balance from database
# Also don't forget that 1 TON = 1e9 (billion) Nanoton
user_balance = db.get_balance(uid) / 1e9
# Format balance and send to user
await message.answer(f'Your balance: *{user_balance:.2f} TON*',
parse_mode=ParseMode.MARKDOWN)
Все довольно просто. Мы просто получаем баланс из базы данных и отправляем сообщение пользователю.
Кнопка Deposit
А как насчет второй кнопки Deposit
? Вот функция для нее:
@dp.message_handler(commands='deposit')
@dp.message_handler(Text(equals='deposit', ignore_case=True))
async def deposit_handler(message: types.Message):
uid = message.from_user.id
# Keyboard with deposit URL
keyboard = InlineKeyboardMarkup()
button = InlineKeyboardButton('Deposit',
url=f'ton://transfer/{config.DEPOSIT_ADDRESS}&text={uid}')
keyboard.add(button)
# Send text that explains how to make a deposit into bot to user
await message.answer('It is very easy to top up your balance here.\n'
'Simply send any amount of TON to this address:\n\n'
f'`{config.DEPOSIT_ADDRESS}`\n\n'
f'And include the following comment: `{uid}`\n\n'
'You can also deposit by clicking the button below.',
reply_markup=keyboard,
parse_mode=ParseMode.MARKDOWN)
То, что мы делаем здесь, также легко понять.
Помните, как в файле ton.py
мы определяли, какой пользователь сделал депозит, по комментарию с его UID? Теперь здесь, в боте, нам нужно попросить пользователя отправить транзакцию с комментарием, содержащим его UID.
Запуск бота
Единственное, что нам теперь нужно сделать в bot.py
, это запустить самого бота, а также выполнить функцию start
из ton.py
.
if __name__ == '__main__':
# Create Aiogram executor for our bot
ex = executor.Executor(dp)
# Launch the deposit waiter with our executor
ex.loop.create_task(ton.start())
# Launch the bot
ex.start_polling()
На данный момент мы написали весь необходимый код для нашего бота. Если вы все сделали правильно, он должен работать, когда вы запустите его с помощью команды python my-bot/bot.py
в терминале.
Если ваш бот работает некорректно, сравните свой код с кодом из этого репозитория.
Ссылки
- Сделано для TON как часть ton-footsteps/8
- By Gusarich (Telegram @Gusarich, Gusarich on GitHub)