Самописная платежная система на TON и ее API

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0

Предисловие​

В данной статье будет реализирована собственная платежная система на TON с использованием собственного API, а также тестирование этого API. Для реализации платежной системы за основу будет взята библиотека tonutils в связке с tonconsole. Эта библиотека была выбрана, поскольку она показалась более простой в освоении по сравнению с другими библиотеками, список которых можно посмотреть по этой ссылке: https://docs.ton.org/develop/dapps/apis/sdk#adnl-based-sdks

Создание кошелька​

Для начала будет рассмотрен код для создания TON-кошелька. Первое, что нужно сделать, — это зарегистрироваться на tonconsole и получить API-ключ: https://tonconsole.com/. Регистрация там простая, нужно лишь авторизоваться через аккаунт Telegram. Ограничения у данного сервиса достаточно незаметные: возможен только один запрос в секунду, что, по моему мнению, абсолютно не критично. После получения ключа можно начинать писать код. В новом проекте будет создан файл create_wallets.py. В этом файле сразу же нужно указать необходимые импорты.
Python: Скопировать в буфер обмена
Код:
from tonutils.client import TonapiClient
from tonutils.wallet import WalletV3R1
Модуль WalletV3R1 означает, что он предназначен для работы с кошельками третьей версии (V3) первого релиза (R1). Версии кошельков могут меняться и устаревать. В новых версиях могут, например, исправлять уязвимости, улучшать безопасность и многое другое.

Далее нужно создать переменную с API-ключом, полученным ранее, и переменную, которая будет использоваться как флаг для указания, в какой сети будет работать софт — в тестовой или основной (в статье будет использоваться только тестовая сеть, так как в ней можно получить TON бесплатно).
Python: Скопировать в буфер обмена
Код:
api_key = ""

is_testnet = True
Затем следует создание самого кошелька с использованием API и флага.
Python: Скопировать в буфер обмена
Код:
client = TonapiClient(api_key=api_key, is_testnet=is_testnet)
# Создание кошелька
wallet, public_key, private_key, mnemonic = WalletV3R1.create(client)

# Конвертация ключей в hex формат
public_key_hex = public_key.hex()
private_key_hex = private_key.hex()

Для отображения результата были добавлены принты.
Python: Скопировать в буфер обмена
Код:
print("Кошелек успешно создан!")
print(f"Адрес: {wallet.address.to_str()}")
print(f"Публичный ключ: {public_key_hex}")
print(f"Приватный ключ: {private_key_hex}")
print(f"Мнемоническая фраза: {mnemonic}")

Результат:
1729964164078.png


Проверка баланса​

Теперь будет показано, как реализовать проверку баланса с использованием tonconsole.com. Документацию по их API можно посмотреть по этой ссылке: https://docs.tonconsole.com/. Проверка баланса будет выполняться через обычные запросы, и, кроме ограничения в один запрос в секунду, других ограничений нет.

Был создан новый файл с названием check_balance.py. В нем сразу нужно указать API-ключ, кошелек и ссылку для отправки запроса (если убрать из ссылки "testnet.", то работа будет с основной сетью), а также саму отправку запроса.
Python: Скопировать в буфер обмена
Код:
api_key = ''

wallet_address = ''

balance_url = f'https://testnet.tonapi.io/v2/accounts/{wallet_address}'

headers = {
   'X-API-KEY': api_key
}


response = requests.get(balance_url, headers=headers)

Далее из ответа нужно извлечь ключ balance и разделить его значение на 1 000 000 000. Если не выполнять деление, то результат будет не в TON, а в nanoTON
Python: Скопировать в буфер обмена
Код:
# Если ответ положительный
if response.status_code == 200:
   # Запись ответа в переменную data
   data = response.json()
   # Извлечение баланса
   balance = data.get('balance')
   print(f"Баланс: {balance / 1000000000} TON")  # Преобразуем в TON
else:
   print(f"Ошибка при получении баланса: {response.status_code} - {response.text}")

Результат:
1729966010315.png



Также можно проверять баланс, не отправляя запросы через requests, а используя tonutils.
Python: Скопировать в буфер обмена
Код:
from tonutils.client import TonapiClient
from tonutils.utils import to_amount
from tonutils.wallet import WalletV3R1

api_key = ""

is_testnet = True

mnemonic: list[str] = []


async def main() -> None:
   client = TonapiClient(api_key=api_key, is_testnet=is_testnet )
   wallet, public_key, private_key, mnemonic = WalletV3R1.from_mnemonic(client, mnemonic)


   balance = await wallet.balance()
   print(f"Баланс: {to_amount(balance)}")


if __name__ == "__main__":
   import asyncio


   asyncio.run(main())

Как получить TON в тестовой сети​

Для того чтобы получить монеты, можно отправить команду /get этому боту в Telegram: @testgiver_ton_bot. После этого нужно пройти капчу и ввести кошелек, и всё. В течение 10 секунд на кошелек придет 2 TON в тестовой сети.

Важно замечание! Если вы генерируете 2 кошелька и на каждый из них выдали TON в тестовой сети, то по умолчанию кошельки не будут активными. Чтобы сделать их активными, вам потребуется совершить с них любую транзакцию на любой кошелек, даже в тестовой сети. Можете просто отправить любое количество TON с каждого кошелька друг другу.

Чтобы узнать, активен ли кошелек, можете перейти по этой ссылке: https://testnet.tonapi.io/v2/accounts/кошелек. На открывшейся странице найдите ключ "status".

Отправка транзакций​

Раз речь пошла об отправке транзакций с одного кошелька на другой, то как раз это сейчас и будет реализовано в коде. Для этого был создан файл transfer_ton.py. В данном файле также нужно указать API, сеть, мнемоническую фразу, а также адрес, куда отправлять, комментарий к транзакции (необязателен) и сумму к отправке.
Python: Скопировать в буфер обмена
Код:
from tonutils.client import TonapiClient
from tonutils.wallet import WalletV3R1

api_key = ""

is_testnet = True

MNEMONIC: list[str] = []

DESTINATION_ADDRESS = ""

COMMENT = "hui1234"

Вот код для отправки TON:
Python: Скопировать в буфер обмена
Код:
async def transfer():
   client = TonapiClient(api_key=api_key, is_testnet=is_testnet)
   # Создание кошелька на основе мнемонической фразы и запись полученных данных в переменные
   wallet, public_key, private_key, mnemonic = WalletV3R1.from_mnemonic(client, seed)
   # wallet.transfer отправляет транзакцию. В tx_hash записывается хэш транзакции
   tx_hash = await wallet.transfer(destination=destination_address, amount=amount, body=comment,)


   print(f"Успешно переведено {amount} TON!")
   print(f"Хэш транзакции: {tx_hash}")


asyncio.run(transfer())
За отправку отвечает метод transfer, в который передаются параметры, такие как сумма и адрес, куда отправлять. После выполнения метода transfer возвращается хэш транзакции. transfer работает асинхронно, поэтому сделать функцию отправки синхронной не получится.

Результат:
1729966155662.png


Проверка транзакций​

Теперь рассмотрим код для проверки транзакций и поиска нужной. Так же, как и при проверке баланса, код будет работать через запросы.
Python: Скопировать в буфер обмена
Код:
api_key = ''

wallet_address = ''

transactions_url = f'https://testnet.tonapi.io/v2/blockchain/accounts/{wallet_address}/transactions'

Я не придумал ничего лучше, чем искать нужную транзакцию по сумме перевода. Учитывая, что суммы могут повторяться, в дальнейшем при переводе с кошелька на кошелек будет добавляться рандомное небольшое значение к сумме, чтобы каждая из транзакций была уникальной. Так как проверка будет происходить по сумме, нужно создать переменную, в которую будет записана сумма для поиска.
Python: Скопировать в буфер обмена
amount = 0.1

Далее следует сама отправка запроса и передача ключа.
Python: Скопировать в буфер обмена
Код:
headers = {
   'X-API-KEY': api_key
}

response = requests.get(transactions_url, headers=headers)

Затем в полученном ответе будет искаться ключ value из in_msg. Именно в value хранится сумма перевода в NanoTON.
Python: Скопировать в буфер обмена
Код:
# Если ответ положительный
if response.status_code == 200:
   # Запись ответа в переменную data
   data = response.json()


   # Проходим по всем транзакциям и извлекаем значение
   if 'transactions' in data:
       transaction_found = False  # Флаг для проверки, найдена ли транзакция
       # Проходит по всем строкам из ключа 'transactions'
       for transaction in data['transactions']:
           # Если строка это 'in_msg', то если в 'in_msg' есть ключ 'value'
           if 'in_msg' in transaction and 'value' in transaction['in_msg']:
               value = transaction['in_msg']['value']  # Получаем value
               value_in_ton = value / 1000000000  # Делим на 1,000,000,000 для перевода в TON


               # Сравнение каждого значения с заданной суммой
               if value_in_ton == amount:
                   print(f"Транзакция найдена: {value_in_ton} TON")
                   transaction_found = True  # Установка ключа означающего, что транзакция найдена
                   break  # Выход из цикла, если транзакция найдена


       # Если ключ transaction_found false то значит транзакция не найдена
       if not transaction_found:
           print("Транзакция не найдена.")
   else:
       print("Нет доступных транзакций.")
else:
   # Выводим код состояния и текст ответа для отладки
   print(f"Ошибка при получении списка транзакций: {response.status_code} - {response.text}")

Результат:
1729966258776.png


Пользовательский интерфейс​

На этом разбор основных функций завершён, и можно приступать к реализации пользовательского интерфейса для управления платежной системой. Интерфейс, как и всегда, я буду реализовывать на Flask.

Регистрация​

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

Для начала просто сделаю инициализацию Flask и базы данных.
Python: Скопировать в буфер обмена
Код:
from flask import Flask, request, jsonify, session, render_template, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from uuid import uuid4
from sqlalchemy.ext.mutable import MutableList
from create_wallets import create_wallet


app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False


db = SQLAlchemy(app)
bcrypt = Bcrypt(app)


class Users(db.Model):
   id = db.Column(db.Integer, primary_key=True)
   # Значение по умолчанию это генерация UUID
   uuid = db.Column(db.String(36), unique=True, nullable=False, default=str(uuid4()))
   username = db.Column(db.String(50), unique=True, nullable=False)
   password = db.Column(db.String(128), nullable=False)


   # Столбцы для данных кошелька
   wallet_address = db.Column(db.String(50), nullable=True)
   public_key = db.Column(db.String(128), nullable=True)
   private_key = db.Column(db.String(128), nullable=True)
   mnemonic = db.Column(MutableList.as_mutable(db.JSON), nullable=True)  # Используется тип данных JSON


if __name__ == '__main__':
   with app.app_context():
       db.create_all()
   app.run(debug=True)
Отдельно лишь хочу подметить bcrypt и для чего он нужен. А нужен он для того, чтобы записывать пароль при регистрации в базу данных не в чистом виде, а в виде хэша с солью. В дальнейшем, при авторизации будет браться пароль из поля ввода и конвертироваться в хэш с такими же параметрами, как при регистрации, затем пароль из БД будет сравниваться с паролем из поля ввода. Также хочется отметить, что UUID создается сразу при создании столбца в базе данных в этой строке:
Python: Скопировать в буфер обмена
uuid = db.Column(db.String(36), unique=True, nullable=False, default=str(uuid4()))
Можно было бы генерировать UUID внутри будущей функции регистрации, но такой вариант мне показался более удобным.

Теперь рассмотрим саму функцию регистрации.
Python: Скопировать в буфер обмена
Код:
@app.route('/register', methods=['GET', 'POST'])
def register():
   # POST запрос означает что на адрес был отправлен запрос с html страницы в котором должны передаваться данные из полей ввода
   if request.method == 'POST':
       # Извлекает данные из полей с сайта
       username = request.form.get('username')
       password = request.form.get('password')

       # Если одно из полей пустое или оба пустые
       if not username or not password:
           return jsonify({"error": "Требуется имя пользователя и пароль"}), 400

       # Сравнивает данные из переменной с данными из бд
       existing_user = Users.query.filter_by(username=username).first()

       # Если данные сходятся, то регистрации не происходит и выводится ошибка
       if existing_user:
           return jsonify({"error": "Имя пользователя занято"}), 400

       # Берет пароль из переменной и хеширует его с помощью generate_password_hash и записывает хэш в переменную
       hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')

       # Назначает столбцам из бд данные из переменных (пароль в виде хэша)
       new_user = Users(username=username, password=hashed_password)

       # Добавляет новые данные в сессию
       db.session.add(new_user)
       # Сохраняет изменения в базе данных
       db.session.commit()

       # Создаем кошелек и получаем его данные
       wallet_data = create_wallet()  # Вызов функции для создания кошелька

       # Сохраняем данные кошелька в базу данных
       new_user.wallet_address = wallet_data["address"]
       new_user.public_key = wallet_data["public_key"]
       new_user.private_key = wallet_data["private_key"]
       new_user.mnemonic = wallet_data["mnemonic"]

       db.session.commit()  # Сохраняем изменения

       # Если все действия выше были выполнены удачно и пользователь зарегистрирован, то переадресация на страницу авторизации
       return redirect(url_for('login'))
   # Это если GET запрос, то есть если просто переход по ссылке в браузере
   return render_template('register.html')
В начале функции видно, что принимаются данные из полей с веб-части, но пока этой веб-части нет, ее реализацию я покажу после объяснения всех функций на стороне Python. Также, как видно из кода, вызывается функция создания кошелька, и записываются данные, возвращенные от этой функции, внутрь базы данных. Но пока что код для создания кошелька не адаптирован так, чтобы возвращать данные, да и самой функции пока что нет, ведь изначально код для создания кошелька был написан для запуска как отдельное приложение. Вот обновленная версия кода:
Python: Скопировать в буфер обмена
Код:
from tonutils.client import TonapiClient
from tonutils.wallet import WalletV3R1


# API ключ для доступа tonconsole
api_key = ""


# Функция создания кошелька с параметром означающим работу в тестовой сети
def create_wallet(is_testnet=True):
   client = TonapiClient(api_key=api_key, is_testnet=is_testnet)
   # Создание кошелька
   wallet, public_key, private_key, mnemonic = WalletV3R1.create(client)

   # Конвертация ключей в hex формат
   public_key_hex = public_key.hex()
   private_key_hex = private_key.hex()

   # Возвращает данные от кошелька в виде словаря json
   return {
       "address": wallet.address.to_str(),
       "public_key": public_key_hex,
       "private_key": private_key_hex,
       "mnemonic": mnemonic
   }
Из изменений: была перенесена вся логика внутрь функции с параметром для работы в тестовой сети, данные созданного кошелька больше не выводятся простыми принтами, а возвращаются в виде JSON-словаря.

Авторизация​

Теперь рассмотрим функцию авторизации.
Python: Скопировать в буфер обмена
Код:
@app.route('/login', methods=['GET', 'POST'])
def login():
   if request.method == 'POST':
       # Получение данных из полей ввода
       username = request.form.get('username')
       password = request.form.get('password')

       # Если одно или оба поля ввода пустые
       if not username or not password:
           return jsonify({"error": "Требуется имя пользователя и пароль"}), 400

       # Сравнивает введённое имя пользователя с данными в базе данных
       user = Users.query.filter_by(username=username).first()

       # Если пользователь найден, то сравнивает его пароль в виде хэша с паролем из поля ввода
       if user and bcrypt.check_password_hash(user.password, password):
           # Записывает в сессию, внутрь ключа user_id ид из столбца в базе данных
           session['user_id'] = user.id
           return redirect(url_for('profile'))

       return jsonify({"error": "Неправильное имя пользователя или пароль"}), 401

   return render_template('login.html')
Как видно, в начале функции берутся данные из полей ввода с веб-части, которой пока что также нет. check_password_hash берет пароль из поля ввода, переводит его в хэш и сравнивает с хэшем пароля из базы данных. Если пароли сходятся, то происходит редирект по маршруту страницы профиля.

Профиль​

Теперь рассмотрим маршрут страницы профиля.
Python: Скопировать в буфер обмена
Код:
@app.route('/profile')
def profile():
   # Если в сессии нет ключа с ид пользователя, то редирект на страницу авторизации
   if 'user_id' not in session:
       return redirect(url_for('login'))
   # Если ид пользователя из сессии найден в базе данных, то редирект на страницу профиля
   user = Users.query.get(session['user_id'])
   # Возвращает страницу профиля и передает параметры username, uuid и wallet_address из базы данных
   return render_template('profile.html', username=user.username, uuid=user.uuid, wallet_address=user.wallet_address)
В return возвращается не только страница, но и данные пользователя. Это нужно для того, чтобы в дальнейшем на веб-части отображать эти данные.

Веб часть​

Теперь можно разобрать код веб-части интерфейса.

Страница регистрации​

Первой на очереди будет страница для регистрации.
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Register</title>
</head>
<body>
   <h2>Регистрация</h2>
   <form method="POST" action="{{ url_for('register') }}">
       <label for="username">Имя пользователя:</label>
       <input type="text" id="username" name="username" required><br>
       <label for="password">Пароль:</label>
       <input type="password" id="password" name="password" required><br>
       <button type="submit">Зарегистрироваться</button>
   </form>
   <p>Есть аккаунт? <a href="{{ url_for('login') }}">Авторизация</a>.</p>
</body>
</html>
Все элементы, которые должны обрабатываться на стороне Python, находятся в объекте формы с указанным методом отправки и адресом, куда эту форму отправлять. Объект button типа submit как раз отправляет эти данные указанным методом на указанный адрес. В Python-части в начале функции регистрации указано, из каких полей с каким id брать данные. Объект <a> нужен для создания гиперссылок, то есть кликабельная ссылка, которая прячется за обычным текстом.

Страница авторизации​

Далее рассмотрим страницу авторизации. Здесь абсолютно то же самое, что и на странице регистрации.
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Login</title>
</head>
<body>
   <h2>Авторизация</h2>
   <form method="POST" action="{{ url_for('login') }}">
       <label for="username">Имя пользователя:</label>
       <input type="text" id="username" name="username" required><br>
       <label for="password">Пароль:</label>
       <input type="password" id="password" name="password" required><br>
       <button type="submit">Авторизоваться</button>
   </form>
   <p>Нет аккаунта? <a href="{{ url_for('register') }}">Регистрация</a>.</p>
</body>
</html>

Страница профиля​

Теперь перейдем к странице профиля.
HTML: Скопировать в буфер обмена
Код:
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Profile</title>
</head>
<body>
   <p>Имя пользователя: {{username}}</p>
   <p>UUID: {{uuid}}</p>
   <p>Кошелек: {{wallet_address}}</p>
   <form action="{{ url_for('logout') }}" method="POST">
       <button type="submit">Выйти</button>
   </form>
</body>
</html>
Имя пользователя, UUID, кошелек. Эти данные можно отобразить благодаря тому, что в Python-коде эти данные передались на HTML-страницу из базы данных с помощью функции render_template в этой строке:
HTML: Скопировать в буфер обмена
return render_template('profile.html', username=user.username, uuid=user.uuid, wallet_address=user.wallet_address)

Форма с ссылкой на logout пока не работает, так как данный маршрут не был прописан на стороне Python. Сейчас это будет исправлено:
Python: Скопировать в буфер обмена
Код:
@app.route('/logout', methods=['POST'])
def logout():
   session.pop('user_id', None)
   return redirect(url_for('login'))
Данный код нужен для того, чтобы выходить из аккаунта методом очищения ключа user_id в сессии и установкой на его место None (в функции profile как раз есть проверка на этот ключ в сессии).

На этом с веб-частью и регистрацией в сервисе пока что закончено, и вот страницы, которые в данный момент готовы:
1729966598344.png


1729966616586.png


1729966629875.png


Написание API​

Теперь можно приступать к реализации собственного API, благодаря которому в дальнейшем можно будет настраивать систему оплаты в сторонних проектах.

Принцип работы​

Для начала рассмотрим, как будет устроена работа с API. Представим, что есть магазин с кнопкой "Купить". При нажатии на эту кнопку будет отправляться запрос к API на создание заявки об оплате. В запросе будет храниться сумма отправки и UUID человека, использующего API. Далее API создает заявку и отправляет обратно магазину ответ с ID заявки, суммой к оплате (будет добавляться рандомное небольшое значение, чтобы сумма была уникальной) и кошельком, на который оплачивать. Далее магазин получает этот ответ и выводит на своей странице данные к оплате, добавляя кнопку "Оплатил". При нажатии на эту кнопку на API снова отправляется запрос, но уже с номером заявки. Затем API проверяет этот номер заявки, берет кошелек, указанный в ней, и сумму к оплате, после чего проверяет, есть ли транзакция на кошельке на сумму, указанную в заявке. Если есть, API возвращает положительный ответ магазину, а дальше магазин делает то, что хочет.

Создание заявок на оплату​

Первое, что будет сделано, — это возможность создавать заявки. Первым делом нужно переделать базу данных, добавить в нее таблицу, в которой будет в дальнейшем записываться ID транзакции и сумма.
Python: Скопировать в буфер обмена
Код:
class Users(db.Model):
   __tablename__ = 'users'

   id = db.Column(db.Integer, primary_key=True)
   # Значение по умолчанию это генерация UUID
   uuid = db.Column(db.String(36), unique=True, nullable=False, default=str(uuid4()))
   username = db.Column(db.String(50), unique=True, nullable=False)
   password = db.Column(db.String(128), nullable=False)

   # Столбцы для данных кошелька
   wallet_address = db.Column(db.String(50), nullable=True)
   public_key = db.Column(db.String(128), nullable=True)
   private_key = db.Column(db.String(128), nullable=True)
   mnemonic = db.Column(MutableList.as_mutable(db.JSON), nullable=True)  # Используется тип данных JSON

   # Связь с таблицей Transactions
   transactions = relationship("Transactions", backref="user", lazy=True)


class Transactions(db.Model):
   __tablename__ = 'transactions'

   id = db.Column(db.Integer, primary_key=True)
   user_id = db.Column(db.Integer, ForeignKey('users.id'), nullable=False)  # Внешний ключ для связи с Users
   amount = db.Column(db.Float, nullable=False)
   timestamp = db.Column(db.DateTime, default=db.func.current_timestamp())
Хочу обратить внимание на то, что обе таблицы связаны через ForeignKey, чтобы каждая транзакция была прикреплена к конкретному пользователю. Привязка идет в столбце user_id со столбцом id из таблицы users. Также обратите внимание на столбец timestamp: при создании заявки в него сразу будет записываться текущее время. Сейчас это ни к чему, но в дальнейшем это может понадобиться.

Далее можно приступать к написанию API-логики. Для этого понадобится создать функцию, которая будет принимать POST-запросы с данными для создания заявки на оплату, а именно: суммой и UUID. После этого полученная сумма будет записываться в базу данных, а также ID пользователя, чей UUID был принят.
Python: Скопировать в буфер обмена
Код:
@app.route('/create_invoice', methods=['POST'])
def create_invoice():
   # Извлекаем UUID из заголовков запроса
   uuid = request.headers.get('Authorization')
   if not uuid:
       return jsonify({"error": "UUID не предоставлен"}), 400

   # Ищем пользователя с этим UUID в базе данных
   user = Users.query.filter_by(uuid=uuid).first()
   if not user:
       return jsonify({"error": "Пользователь не найден"}), 404

   # Извлекаем данные из запроса и записываем в переменную
   data = request.json
   # Запись суммы из ключа amount
   amount = data.get('amount')
   # Добавляем небольшое случайное значение от 0.00001 до 0.00011
   random_increment = random.uniform(0.00001, 0.00999)
   amount = round(amount + random_increment, 5)
   if amount is None:
       return jsonify({"error": "Сумма не предоставлена"}), 400

   # Создаем новую заявку и добавляем в базу данных
   transaction = Transactions(user_id=user.id, amount=amount)
   db.session.add(transaction)
   db.session.commit()

   # Возвращаем ответ с суммой и ID транзакции
   return jsonify({"transaction_id": transaction.id, "amount": transaction.amount, "wallets": user.wallet_address}), 201
Как видно из кода, к полученной сумме добавляется рандомное значение, о чем я говорил ранее.

Теперь напишем тестовый код для отправки запроса на данный маршрут.
Python: Скопировать в буфер обмена
Код:
import requests

UUID = 'd1fced81-9cb5-4843-8b7b-0887dfc35827'
# URL API
URL = 'http://127.0.0.1:5000/create_invoice'

# Данные для создания инвойса
data = {
   'amount': 2.0,
}

# Заголовки для запроса
headers = {
   'Authorization': UUID,
   'Content-Type': 'application/json'
}

# Отправляем запрос
response = requests.post(URL, headers=headers, json=data)

print(response.json())
Важное замечание! По каким-то причинам отправка запроса не работала при включенном VPN, хотя если отправить такой же запрос через консоль Windows, то все в порядке.

В общем, после отправки запроса тестовый код получит сообщение, содержащее новую сумму с сгенерированным числом, ID транзакции, кошелек, и база данных также пополнится информацией о заявке.
1729966756849.png


Проверка заявок на оплату​

Заявка есть, теперь нужно как-то проверить ее статус. Для этого будет написана новая функция, также принимающая POST-запросы. Новая функция будет извлекать из базы данных сумму и кошелек.
Python: Скопировать в буфер обмена
Код:
@app.route('/check_transaction', methods=['POST'])
def check_transaction():
   # Извлекаем UUID из заголовков запроса
   uuid = request.headers.get('Authorization')
   if not uuid:
       return jsonify({"error": "UUID не предоставлен"}), 400

   # Ищем пользователя с этим UUID в базе данных
   user = Users.query.filter_by(uuid=uuid).first()
   if not user:
       return jsonify({"error": "Пользователь не найден"}), 404

   # Извлекаем данные из запроса и записываем в переменную
   data = request.json
   transaction_id = data.get('transaction_id')

   if transaction_id is None:
       return jsonify({"error": "ID транзакции не предоставлен"}), 400

   # Ищем транзакцию по ID и user_id
   transaction = Transactions.query.filter_by(id=transaction_id, user_id=user.id).first()
   if not transaction:
       return jsonify({"error": "Транзакция не найдена"}), 404

   # Получаем сумму и адрес кошелька из данных пользователя и транзакции
   amount = transaction.amount
   wallet_address = user.wallet_address

   # Вызываем функцию для проверки транзакции
   result = check_transactions(amount, wallet_address)

   # Возвращаем ответ на основе результата проверки
   if result['status'] == "success":
       return jsonify({"status": "success"}), 200
   elif result['status'] == "not_found":
       return jsonify({"status": "not_found"}), 404
   elif result['status'] == "no_transactions":
       return jsonify({"status": "message"}), 404
   else:
       return jsonify({"status": "error"}), 500
Как видно, данная функция работает аналогично функции для создания заявок, только здесь вызывается функция для проверки транзакций, и затем возвращается ответ от функции проверки транзакций. Дело в том, что такой функции пока что нет, так как код проверки транзакции разрабатывался как отдельный проект, поэтому его нужно обновить.
Python: Скопировать в буфер обмена
Код:
import requests

def check_transactions(amount, wallet_address):
   api_key = "AH35FW35LU7OF3AAAAAH5PZIYY6APMW6PQ7EBQJC5WA7H6OG4UT7PAZWYXJV7V6WQF42YJA"

   # URL для получения списка транзакций
   transactions_url = f'https://testnet.tonapi.io/v2/blockchain/accounts/{wallet_address}/transactions'

   # Заголовки для запроса, включая API ключ
   headers = {
       'X-API-KEY': api_key
   }

   # Отправляем GET запрос
   response = requests.get(transactions_url, headers=headers)

   # Если ответ положительный
   if response.status_code == 200:
       # Запись ответа в переменную data
       data = response.json()

       # Проходим по всем транзакциям и извлекаем значение
       if 'transactions' in data:
           for transaction in data['transactions']:
               if 'in_msg' in transaction and 'value' in transaction['in_msg']:
                   value = transaction['in_msg']['value']
                   value_in_ton = value / 1000000000  # Перевод в TON


                   # Сравнение каждого значения с заданной суммой
                   if value_in_ton == amount:
                       return {"status": "success"}
           return {"status": "not_found"}
       else:
           return {"status": "no_transactions"}
   else:
       return {"status": "error"}
Из изменений: логика была перенесена внутрь функции, вместо принтов теперь стоит return, чтобы возвращать статус обратно.

Теперь можно написать тестовый код для отправки запроса на проверку транзакции.
Python: Скопировать в буфер обмена
Код:
import requests

# Замените на фактический токен аутентификации пользователя
UUID = 'd1fced81-9cb5-4843-8b7b-0887dfc35827'
# URL вашего сервера
URL = 'http://127.0.0.1:5000/check_transaction'

# Данные для создания инвойса
data = {
   'transaction_id': 1,
}

# Заголовки для запроса
headers = {
   'Authorization': UUID,
   'Content-Type': 'application/json'
}

# Отправляем запрос
response = requests.post(URL, headers=headers, json=data)

print(response.json())

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


По сути, с API закончено. Теперь с помощью него можно создавать заявки на оплату и проверять заявки на оплату. Но выводить деньги из сервиса API пока нельзя, и именно это мы сейчас и будем исправлять.

Отображение баланса пользователя в платежной системе​

Первое, что будет сделано, — это отображение баланса кошелька. Чтобы это реализовать, я решил вставить логику проверки баланса прямо в маршрут /profile, чтобы при каждом заходе на страницу баланс обновлялся.
Python: Скопировать в буфер обмена
Код:
@app.route('/profile')
def profile():
   # Проверяем, что пользователь авторизован
   if 'user_id' not in session:
       return redirect(url_for('login'))

   # Получаем данные пользователя
   user = Users.query.get(session['user_id'])

   # API ключ и адрес кошелька
   api_key = 'AH35FW35LU7OF3AAAAAH5PZIYY6APMW6PQ7EBQJC5WA7H6OG4UT7PAZWYXJV7V6WQF42YJA'
   wallet_address = user.wallet_address
   balance_url = f'https://testnet.tonapi.io/v2/accounts/{wallet_address}'

   # Отправляем GET-запрос на API для получения баланса
   headers = {'X-API-KEY': api_key}
   response = requests.get(balance_url, headers=headers)

   if response.status_code == 200:
       # Извлекаем баланс и преобразуем его в TON
       data = response.json()
       balance = data.get('balance') / 1000000000  # Преобразуем в TON
   else:
       balance = "Ошибка при получении баланса"

   # Передаём баланс и данные пользователя на страницу
   return render_template('profile.html', username=user.username, uuid=user.uuid, wallet_address=user.wallet_address,
                          balance=balance)
Нужный кошелек определяется благодаря тому, что берется ID пользователя из сессии.

Теперь нужно дополнить веб-страницу profile. Для этого нужно лишь дописать одну строчку.
HTML: Скопировать в буфер обмена
<p>Баланс: {{ balance }} TON</p>

Вывод денег из платежной системы​

С отображением баланса закончено, теперь можно реализовать перевод денег с баланса пользователя API на другой кошелек.
Для начала нужно на странице создать форму с полями для ввода суммы и кошелька, куда отправлять TON.
HTML: Скопировать в буфер обмена
Код:
<form action="{{ url_for('transfer') }}" method="POST">
   <label for="amount">Сумма перевода (TON):</label>
   <input type="number" name="amount" step="0.0001" required>

   <label for="destination_address">Адрес получателя:</label>
   <input type="text" name="destination_address" required>

   <button type="submit">Отправить</button>
</form>
Как видно, форма будет отправлять запрос на /transfer, которого пока что нет. Так что сейчас мы его и будем писать.
Python: Скопировать в буфер обмена
Код:
@app.route('/transfer', methods=['POST'])
def transfer():
   # Получаем данные из формы
   amount = float(request.form.get('amount'))
   destination_address = request.form.get('destination_address')
   user_id = session.get('user_id')  # Получаем user_id из сессии

   # Проверка наличия user_id в сессии
   if not user_id:
       return jsonify({"error": "Пользователь не найден"}), 401

   # Извлекаем сид-фразу пользователя из базы данных
   user = Users.query.get(user_id)
   seed = user.mnemonic  # Сид-фраза из базы данных

   # Запуск функции перевода деняг
   tx_hash = asyncio.run(execute_transfer(seed, amount, destination_address))
   return jsonify({"message": "Перевод выполнен", "tx_hash": tx_hash})
В данном коде берутся данные из формы, ID берется из сессии, по этому ID также берется мнемоническая фраза. Далее вызывается функция отправки транзакции.
Python: Скопировать в буфер обмена
Код:
async def execute_transfer(seed, amount, destination_address):
   client = TonapiClient(api_key=api_key, is_testnet=True)
   wallet, public_key, private_key, mnemonic = WalletV3R1.from_mnemonic(client, seed)
   tx_hash = await wallet.transfer(destination=destination_address, amount=amount, body="Перевод с профиля")
   return tx_hash
Разбирать её не вижу смысла, так как это было сделано в начале статьи.

Результат:
1729967247946.png


1729967263200.png


Вывод​

На этом статья закончена. Проект получился несложным, но, как мне кажется, достаточно интересным, если вы не работали с криптовалютой. Код достаточно грязный, так как он был сделан лишь для проверки функционала. Я очень жду ваших замечаний по коду и надеюсь на помощь в исправлении багов и уязвимостей. В дальнейшем я хочу попробовать довести эту платежную систему до ума и опробовать на практике в своём продакшн-проекте. После тестов я обязательно напишу об этом в своём профиле на форуме.

P.S. В данной статье я постарался писать меньше банальных объяснений, но всё же решил их указывать как комментарии в коде. Таким образом, я надеюсь, что у людей не будет гореть с каждой оплаченной мне копейки за символ (привет, shrekushka), если статья будет оплачена, ведь код не будет учитываться при оплате. Но при этом статьи всё равно останутся максимально подробными, как я и хочу, ведь я всё же считаю, что не все люди могут с легкостью прочитать код, и именно для таких людей я и пишу комментарии к каждой строчке кода.


Статья в виде документа: https://docs.google.com/document/d/1YSec2B8xz-ubvpUzF_63Sngfxo35YVPl0Sk2DCXgtjU/edit?usp=sharing

Исходники на GitHub: https://github.com/overlordgamedev/Ton-Payment-System

Сделано OverlordGameDev специально для форума XSS.IS
 
Сверху Снизу