Бот-витрина магазина с оплатой в TON
Эта страница переведена сообществом на русский язык, но нуждается в улучшениях. Если вы хотите принять участие в переводе свяжитесь с @alexgton.
В этой статье мы расскажем о том, как принимать платежи в боте Telegram.
📖 Чему вы научитесь
В этой статье вы узнаете, как:
- создать Telegram-бота с помощью Python + Aiogram
- работать с публичным API TON (TON Center)
- работать с базой данных SQlite
И наконец: как принимать платежи в Telegram-боте, используя знания из предыдущих шагов.
📚 Прежде чем мы начнем
Убедитесь, что у вас установлена последняя версия Python и установлены следующие пакеты:
- aiogram
- requests
- sqlite3
🚀 Давайте начнем!
Мы будем действовать по нижеприведенному порядку:
- Работа с базой данных SQlite
- Работа с публичным API TON (TON Center)
- Создание Telegram-бота с помощью Python + Aiogram
- Получаем прибыль!
Давайте создадим следующие четыре файла в директории нашего проекта:
telegram-bot
├── config.json
├── main.py
├── api.py
└── db.py
Конфигурация
В config.json
мы сохраним токен нашего бота и наш публичный ключ TON API.
{
"BOT_TOKEN": "Your bot token",
"MAINNET_API_TOKEN": "Your mainnet api token",
"TESTNET_API_TOKEN": "Your testnet api token",
"MAINNET_WALLET": "Your mainnet wallet",
"TESTNET_WALLET": "Your testnet wallet",
"WORK_MODE": "testnet"
}
В config.json
мы решаем, какую сеть мы будем использовать: testnet
или mainnet
.
База данных
Создаем базу данных
В этом примере используется локальная база данных Sqlite.
Создайте db.py
.
Чтобы начать работу с базой данных, нам нужно импортировать модуль sqlite3 и несколько модулей для работы со временем.
import sqlite3
import datetime
import pytz
Sqlite3
-модуль для работы с базой данных sqlitedatetime
- модуль для работы со временемpytz
- модуль для работы с часовыми поясами
Далее нам нужно создать соединение с базой данных и курсор для работы с ней:
locCon = sqlite3.connect('local.db', check_same_thread=False)
cur = locCon.cursor()
Если база данных не существует, она будет создана автоматически.
Теперь мы можем создать таблицы. У нас их две.
Транзакции:
CREATE TABLE transactions (
source VARCHAR (48) NOT NULL,
hash VARCHAR (50) UNIQUE
NOT NULL,
value INTEGER NOT NULL,
comment VARCHAR (50)
);
source
- адрес кошелька плательщикаhash
- хэш транзакцииvalue
- значение транзакцииcomment
- комментарий к транзакции
Пользователи:
CREATE TABLE users (
id INTEGER UNIQUE
NOT NULL,
username VARCHAR (33),
first_name VARCHAR (300),
wallet VARCHAR (50) DEFAULT none
);
id
- ID пользователя Telegramusername
- имя пользователя Telegramfirst_name
- имя пользователя Telegramwallet
- адрес кошелька пользователя
В таблице users
мы храним пользователей :) их Telegram ID, @логин,
имя и кошелек. Кошелек добавляется в базу данных при первом
успешном платеже.
В таблице transactions
хранятся проверенные транзакции.
Чтобы проверить транзакцию, нам нужны хеш, источник, значение и комментарий.
Чтобы создать эти таблицы, нам нужно выполнить следующую функцию:
cur.execute('''CREATE TABLE IF NOT EXISTS transactions (
source VARCHAR (48) NOT NULL,
hash VARCHAR (50) UNIQUE
NOT NULL,
value INTEGER NOT NULL,
comment VARCHAR (50)
)''')
locCon.commit()
cur.execute('''CREATE TABLE IF NOT EXISTS users (
id INTEGER UNIQUE
NOT NULL,
username VARCHAR (33),
first_name VARCHAR (300),
wallet VARCHAR (50) DEFAULT none
)''')
locCon.commit()
Этот код создаст таблицы, если они еще не созданы.
Работа с базой данных
Давайте проанализируем ситуацию. Пользователь совершил транзакцию. Как ее подтвердить? Как сделать так, чтобы одна и та же транзакция не была подтверждена дважды?
В транзакциях есть body_hash, с помощью котор ого мы можем легко понять, есть ли транзакция в базе данных или нет.
Мы добавляем транзакции в базу данных, в которых мы уверены. Функция check_transaction
проверяет, есть ли найденная транзакция в базе данных или нет.
add_v_transaction
добавляет транзакцию в таблицу транзакций.
def add_v_transaction(source, hash, value, comment):
cur.execute("INSERT INTO transactions (source, hash, value, comment) VALUES (?, ?, ?, ?)",
(source, hash, value, comment))
locCon.commit()
def check_transaction(hash):
cur.execute(f"SELECT hash FROM transactions WHERE hash = '{hash}'")
result = cur.fetchone()
if result:
return True
return False
check_user
проверяет, есть ли пользователь в базе данных, и добавл яет его, если нет.
def check_user(user_id, username, first_name):
cur.execute(f"SELECT id FROM users WHERE id = '{user_id}'")
result = cur.fetchone()
if not result:
cur.execute("INSERT INTO users (id, username, first_name) VALUES (?, ?, ?)",
(user_id, username, first_name))
locCon.commit()
return False
return True
Пользователь может сохранить кошелек в таблице. Он добавляется при первой успешной покупке. Функция v_wallet
проверяет, есть ли у пользователя связанный с ним кошелек. Если есть, то возвращает его. Если нет, то добавляет.
def v_wallet(user_id, wallet):
cur.execute(f"SELECT wallet FROM users WHERE id = '{user_id}'")
result = cur.fetchone()
if result[0] == "none":
cur.execute(
f"UPDATE users SET wallet = '{wallet}' WHERE id = '{user_id}'")
locCon.commit()
return True
else:
return result[0]
get_user_wallet
просто возвращает кошелек пользователя.
def get_user_wallet(user_id):
cur.execute(f"SELECT wallet FROM users WHERE id = '{user_id}'")
result = cur.fetchone()
return result[0]
get_user_payments
возвращает список платежей пользователя.
Эта функция проверяет, есть ли у пользователя кошелек. Если есть, то она возвращает список платежей.
def get_user_payments(user_id):
wallet = get_user_wallet(user_id)
if wallet == "none":
return "You have no wallet"
else:
cur.execute(f"SELECT * FROM transactions WHERE source = '{wallet}'")
result = cur.fetchall()
tdict = {}
tlist = []
try:
for transaction in result:
tdict = {
"value": transaction[2],
"comment": transaction[3],
}
tlist.append(tdict)
return tlist
except:
return False
API
У нас есть возможность взаимодействовать с блокчейном, используя сторонние API, предоставляемые некоторыми участниками сети. С помощью этих сервисов разработчики могут пропустить этап запуска собственного узла и настройки API.
Необходимые запросы
Фактически, что нам нужно, чтобы подтвердить, что пользователь перевел нам требуемую сумму?
Нам просто нужно просмотреть последние входящие переводы на наш кошелек и найти среди них транзакцию с нужного адреса с нужной суммой (и, возможно, уникальным комментарием).
Для всего этого в TON Center есть метод getTransactions
.
getTransactions
По умолчанию, если мы применим эту функцию, мы получим 10 последних транзакций. Однако мы также можем указать, что нам нужно больше, но это несколько увеличит время ответа. И, скорее всего, вам не нужно так много.
Если вам нужно больше, то у каждой транзакции есть lt
и hash
. Вы можете просмотреть, например, 30 транзакций, и если среди них не найдется нужной, то взять lt
и hash
из последней и добавить их в запрос.
Таким образом, вы получаете следующие 30 транзакций и так далее.
Например, в тестовой сети есть кошелек EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5
, в нем есть несколько транзакций:
Используя запрос, мы получим ответ, содержащий две транзакции (часть информации, которая сейчас не нужна, была скрыта, полный ответ вы можете увидеть по ссылке выше).
{
"ok": true,
"result": [
{
"transaction_id": {
"lt": "1944556000003",
"hash": "swpaG6pTBXwYI2024NAisIFp59Fw3k1DRQ5fa5SuKAE="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "kBfGYBTkBaooeZ+NTVR0EiVGSybxQdb/ifXCRX5O7e0=",
"message": "Sea breeze 🌊"
},
"out_msgs": []
},
{
"transaction_id": {
"lt": "1943166000003",
"hash": "hxIQqn7lYD/c/fNS7W/iVsg2kx0p/kNIGF6Ld0QEIxk="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "7iirXn1RtliLnBUGC5umIQ6KTw1qmPk+wwJ5ibh9Pf0=",
"message": "Spring forest 🌲"
},
"out_msgs": []
}
]
}
Мы получили две последние транзакции с этого адреса. Добавив в запрос lt
и hash
, мы снова получим две транзакции. Однако вторая станет следующей в ряду. То есть, мы получим вторую и третью транзакции для этого адреса.
{
"ok": true,
"result": [
{
"transaction_id": {
"lt": "1943166000003",
"hash": "hxIQqn7lYD/c/fNS7W/iVsg2kx0p/kNIGF6Ld0QEIxk="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "7iirXn1RtliLnBUGC5umIQ6KTw1qmPk+wwJ5ibh9Pf0=",
"message": "Spring forest 🌲"
},
"out_msgs": []
},
{
"transaction_id": {
"lt": "1845458000003",
"hash": "k5U9AwIRNGhC10hHJ3MBOPT//bxAgW5d9flFiwr1Sao="
},
"in_msg": {
"source": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"destination": "EQAVKMzqtrvNB2SkcBONOijadqFZ1gMdjmzh1Y3HB1p_zai5",
"value": "1000000000",
"body_hash": "XpTXquHXP64qN6ihHe7Tokkpy88tiL+5DeqIrvrNCyo=",
"message": "Second"
},
"out_msgs": []
}
]
}
Запрос будет выглядеть вот так
Нам также понадобится метод detectAddress
.
Вот пример адреса кошелька Tonkeeper в тестовой сети: kQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aCTb
. Если мы поищем транзакцию в проводнике, то вместо указанного выше адреса будет: EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R
.
Этот метод возвращает нам "правильный" адрес.
{
"ok": true,
"result": {
"raw_form": "0:b3409241010f85ac415cbf13b9b0dc6157d09a39d2bd0827eadb20819f067868",
"bounceable": {
"b64": "EQCzQJJBAQ+FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R",
"b64url": "EQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aJ9R"
},
"non_bounceable": {
"b64": "UQCzQJJBAQ+FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aMKU",
"b64url": "UQCzQJJBAQ-FrEFcvxO5sNxhV9CaOdK9CCfq2yCBnwZ4aMKU"
}
}
}
Нам нужен b64url
.
Этот метод позволяет нам подтвердить адрес пользователя.
По большей части, это все, что нам нужно.
Запросы API и что с ними делать
Давайте вернемся в IDE. Создайте файл api.py
.
Импортируйте необходимые библиотеки.
import requests
import json
# We import our db module, as it will be convenient to add from here
# transactions to the database
import db
requests
- для выполнения запросов APIjson
для работы с jsondb
- для работы с нашей базой данных sqlite
Давайте создадим две переменные для хранения начала запросов.
# This is the beginning of our requests
MAINNET_API_BASE = "https://toncenter.com/api/v2/"
TESTNET_API_BASE = "https://testnet.toncenter.com/api/v2/"
Получите все API-токены и кошельки из файла config.json.
# Find out which network we are working on
with open('config.json', 'r') as f:
config_json = json.load(f)
MAINNET_API_TOKEN = config_json['MAINNET_API_TOKEN']
TESTNET_API_TOKEN = config_json['TESTNET_API_TOKEN']
MAINNET_WALLET = config_json['MAINNET_WALLET']
TESTNET_WALLET = config_json['TESTNET_WALLET']
WORK_MODE = config_json['WORK_MODE']
В зависимости от сети, мы берем необходимые данные.
if WORK_MODE == "mainnet":
API_BASE = MAINNET_API_BASE
API_TOKEN = MAINNET_API_TOKEN
WALLET = MAINNET_WALLET
else:
API_BASE = TESTNET_API_BASE
API_TOKEN = TESTNET_API_TOKEN
WALLET = TESTNET_WALLET
Наша первая функция запроса detectAddress
.
def detect_address(address):
url = f"{API_BASE}detectAddress?address={address}&api_key={API_TOKEN}"
r = requests.get(url)
response = json.loads(r.text)
try:
return response['result']['bounceable']['b64url']
except:
return False
На входе мы имеем предполагаемый адрес, а на выходе - либо "correct" адрес, необходимый нам для дальнейшей работы, либо False.
Вы можете заметить, что в конце запроса появился API-ключ. Он нужен для того, чтобы снять ограничение на количество запросов к API. Без него мы ограничены одним запросом в секунду.
Вот следующая функция для getTransactions
:
def get_address_transactions():
url = f"{API_BASE}getTransactions?address={WALLET}&limit=30&archival=true&api_key={API_TOKEN}"
r = requests.get(url)
response = json.loads(r.text)
return response['result']
Эта функция возвращает последние 30 транзакций в наш WALLET
.
Здесь вы можете увидеть archival=true
. Это необходимо для того, чтобы мы принимали транзакции только от узла с полной историей блокчейна.
На выходе мы получим список транзакций -[0,1,...,29]. Другими словами, список словарей.
И, наконец, последняя функция:
def find_transaction(user_wallet, value, comment):
# Get the last 30 transactions
transactions = get_address_transactions()
for transaction in transactions:
# Select the incoming "message" - transaction
msg = transaction['in_msg']
if msg['source'] == user_wallet and msg['value'] == value and msg['message'] == comment:
# If all the data match, we check that this transaction
# we have not verified before
t = db.check_transaction(msg['body_hash'])
if t == False:
# If not, we write in the table to the verified
# and return True
db.add_v_transaction(
msg['source'], msg['body_hash'], msg['value'], msg['message'])
print("find transaction")
print(
f"transaction from: {msg['source']} \nValue: {msg['value']} \nComment: {msg['message']}")
return True
# If this transaction is already verified, we check the rest, we can find the right one
else:
pass
# If the last 30 transactions do not contain the required one, return False
# Here you can add code to see the next 29 transactions
# However, within the scope of the Example, this would be redundant.
return False
На вход подаются "correct" адрес кошелька, сумма и комментарий. Если предполагаемая входящая транзакция найдена, результатом будет True; в противном случае - False.
Telegram-бот
Во-первых, давайте создадим основу для бота.
Импорт
В этой части мы импортируем необходимые библиотеки.
Из aiogram
нам нужны Bot
, Dispatcher
, types
и executor
.
from aiogram import Bot, Dispatcher, executor, types
MemoryStorage
необходима для временного хранения информации.
FSMContext
, State
и StatesGroup
необходимы для работы с машиной состояний.
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import State, StatesGroup
json
необходим для работы с json-файлами. logging
необходим для регистрации ошибок.
import json
import logging
api
и db
- это наши собственные файлы, которые мы заполним позже.
import db
import api
Настройка конфигурации
Для удобства рекомендуется хранить такие данные, как BOT_TOKEN
и ваши кошельки для получения платежей, в отдельном файле под названием config.json
.
{
"BOT_TOKEN": "Your bot token",
"MAINNET_API_TOKEN": "Your mainnet api token",
"TESTNET_API_TOKEN": "Your testnet api token",
"MAINNET_WALLET": "Your mainnet wallet",
"TESTNET_WALLET": "Your testnet wallet",
"WORK_MODE": "testnet"
}
Токен бота
BOT_TOKEN
- это токен вашего Telegram-бота от @BotFather
Режим работы
В ключе WORK_MODE
мы определим режим работы бота - тестовая или основная сеть; testnet
или mainnet
соответственно.
API-токены
API-токены для *_API_TOKEN
можно получить в ботах TON Center:
- для mainnet - @tonapibot
- для testnet - @tontestnetapibot
Подключите конфигурацию к боту
Далее мы закончим настройку бота.
Получите токен для работы бота из config.json
:
with open('config.json', 'r') as f:
config_json = json.load(f)
BOT_TOKEN = config_json['BOT_TOKEN']
# put wallets here to receive payments
MAINNET_WALLET = config_json['MAINNET_WALLET']
TESTNET_WALLET = config_json['TESTNET_WALLET']
WORK_MODE = config_json['WORK_MODE']
if WORK_MODE == "mainnet":
WALLET = MAINNET_WALLET
else:
# By default, the bot will run on the testnet
WALLET = TESTNET_WALLET
Ведение журнала и настройка бота
logging.basicConfig(level=logging.INFO)
bot = Bot(token=BOT_TOKEN, parse_mode=types.ParseMode.HTML)
dp = Dispatcher(bot, storage=MemoryStorage())