Приймання криптоплатежів на Python
Мінімальна інтеграція Moonvex на Python із Flask і requests: створюємо рахунок, надсилаємо клієнта на сторінку оплати, приймаємо webhook і перевіряємо підпис HMAC-SHA256 стандартною бібліотекою. Підходить для будь-якого WSGI-застосунку.
Покроково
1.Отримайте ключ API і секрет webhook
У кабінеті Moonvex відкрийте «Інтеграція» → «Створити ключ» → тип «Секретний» (sk_live_…). Ключ показується один раз — збережіть його на сервері в MOONVEX_SECRET_KEY. Там само додайте webhook на https-адресу вашого сервера і збережіть секрет підпису в MOONVEX_WEBHOOK_SECRET.
pip install flask requests # змінні оточення на сервері export MOONVEX_SECRET_KEY=sk_live_... export MOONVEX_WEBHOOK_SECRET=whsec_...2.Створіть платіж
POST /v1/payments через requests: у заголовках Authorization: Bearer та Idempotency-Key, у тілі orderId, amount рядком, currency і network. Відповідь 201 — об’єкт платежу з id і paymentUrl. Збережіть id у себе поруч із замовленням.
# app.py import os import sqlite3 from contextlib import closing import requests from flask import Flask, abort, redirect app = Flask(__name__) API = "https://api.moonvex.org" def db() -> sqlite3.Connection: conn = sqlite3.connect("shop.db") conn.row_factory = sqlite3.Row return conn def create_payment(order_id: int, amount: str, attempt: int = 1) -> dict: r = requests.post( API + "/v1/payments", headers={ "Authorization": "Bearer " + os.environ["MOONVEX_SECRET_KEY"], # повтор із тим самим ключем не створить другий рахунок "Idempotency-Key": f"order-{order_id}-{attempt}", }, json={"orderId": str(order_id), "amount": amount, "currency": "USDT", "network": "TRON"}, timeout=15, ) payment = r.json() if r.status_code >= 400: raise RuntimeError(payment["error"]["code"] + ": " + payment["error"]["message"]) return payment @app.post("/orders/<int:order_id>/pay") def pay(order_id: int): with closing(db()) as conn: order = conn.execute("SELECT id, total FROM orders WHERE id = ?", (order_id,)).fetchone() if order is None: abort(404) # total зберігається як TEXT "25.00" — сума йде рядком payment = create_payment(order["id"], str(order["total"])) return redirect(payment["paymentUrl"], code=303)3.Надішліть клієнта на paymentUrl
paymentUrl — готова сторінка оплати: точна сума з комісією, адреса, QR-код, офіційний контракт токена й таймер, статус оновлюється сам. Відкривайте її редиректом або в новій вкладці — вбудувати у фрейм не можна. Повернення клієнта на ваш сайт ще не оплата: замовлення позначається оплаченим лише за webhook або статусом із GET /v1/payments/:id. У Flask — redirect(payment["paymentUrl"], code=303).
4.Прийміть webhook і перевірте підпис
Moonvex надсилає POST із заголовком Moonvex-Signature: t=<unix-час>,v1=<підпис>. Підпис — HMAC-SHA256 секретом webhook від рядка «t.сире_тіло» у hex. Рахуйте його за байтами тіла до розбору JSON, порівнюйте за сталий час, відхиляйте події, старші за 5 хвилин. Після зміни секрету 24 години в заголовку два v1 — підходить будь-який. У Flask сире тіло — request.get_data(), а hmac.compare_digest порівнює підписи за сталий час.
# moonvex_signature.py import hashlib import hmac import time def verify_moonvex(raw: bytes, header: str, secret: str, tolerance: int = 300) -> bool: """Перевіряє Moonvex-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + "." + raw)>""" parts = [p.split("=", 1) for p in (header or "").split(",") if "=" in p] t = next((v for k, v in parts if k == "t"), None) if t is None or not t.isdigit() or abs(time.time() - int(t)) > tolerance: return False expected = hmac.new(secret.encode(), t.encode() + b"." + raw, hashlib.sha256).hexdigest() # compare_digest — порівняння за сталий час; під час зміни секрету v1 два return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1") # app.py (продовження) import json from flask import request from moonvex_signature import verify_moonvex @app.post("/moonvex/webhook") def moonvex_webhook(): raw = request.get_data() # сирі байти, до розбору JSON if not verify_moonvex(raw, request.headers.get("Moonvex-Signature", ""), os.environ["MOONVEX_WEBHOOK_SECRET"]): return "bad signature", 400 event = json.loads(raw) if event["type"] in ("payment.confirmed", "payment.overpaid"): mark_paid_once(event["id"], event["data"]["orderId"]) return "", 2005.Позначте замовлення оплаченим рівно один раз
Під час повторної доставки id події той самий, тому запишіть його в таблицю з унікальним ключем в одній транзакції з оновленням замовлення: дубль нічого не змінить. Видавайте товар за payment.confirmed і payment.overpaid, а не за payment.detected. У SQLite це INSERT OR IGNORE і перевірка cursor.rowcount в одній транзакції.
# app.py (продовження) # CREATE TABLE processed_events (event_id TEXT PRIMARY KEY, created_at TEXT DEFAULT CURRENT_TIMESTAMP); def mark_paid_once(event_id: str, order_id: str) -> None: # другий "conn" — транзакція: commit у разі успіху, rollback у разі помилки with closing(db()) as conn, conn: cur = conn.execute("INSERT OR IGNORE INTO processed_events (event_id) VALUES (?)", (event_id,)) if cur.rowcount == 1: # перша доставка події conn.execute("UPDATE orders SET status = 'paid' WHERE id = ? AND status <> 'paid'", (int(order_id),))6.Перевірте інтеграцію
Тестової мережі в Moonvex немає — усе працює в основних мережах. Кнопка «Надіслати тест» у розділі «Інтеграція» надішле подію webhook.test — так перевіряється підпис. Потім оплатіть невеликий рахунок, наприклад 1 USDT у TRON: це звичайний платіж, гроші надійдуть на баланс. Журнал доставок із кодами відповідей і кнопкою «Повторити» — там само.
Підводні камені
- Використовуйте request.get_data(), а не request.json: підпис рахується за початковими байтами, повторна серіалізація їх змінює.
- Суми тримайте в Decimal або рядках. 0.1 + 0.2 у float — не 0.3, а в платежах це помилка в центах.
- Завжди задавайте timeout у requests.post — без нього завислий запит тримає воркер нескінченно.
- Функція verify_moonvex приймає всі значення v1 із заголовка — так ротація секрету пройде без простою.