From 663812fb98c579307acfa357fb846b90264fa08d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Nov 2026 02:38:54 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=D0=91=D0=B8=D0=BB=D0=BB=D0=B8=D0=BD=D0=B3:?= =?UTF-8?q?=20=D0=BE=D1=82=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=20Crypto=20Pay=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20testnet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переключатель testnet выбирает CRYPTO_PAY_TESTNET_TOKEN и testnet-хост. Без этого токена счет не создается, боевой токен на testnet не уходит. Вебхук другого режима отвечает 200 и не продлевает подписку. Co-authored-by: ANDY BUM --- CHANGELOG.en.md | 1 + CHANGELOG.md | 1 + ops/billing/README.md | 10 +- ops/billing/admin/src/lib/text.ts | 1 + ops/billing/admin/src/panels/sections.tsx | 6 +- .../{index-BBBXsVw3.js => index-Cif9tUAp.js} | 4 +- ops/billing/public/index.html | 2 +- ops/billing/src/cryptoPay.mjs | 36 +++- ops/billing/src/envSecrets.mjs | 1 + ops/billing/src/server.mjs | 27 ++- ops/billing/src/services.mjs | 2 + ops/billing/src/state.mjs | 4 +- ops/billing/test/cryptoPayMode.test.ts | 155 ++++++++++++++ ops/billing/test/envSecrets.test.ts | 9 +- ops/billing/test/payMethods.test.ts | 199 +++++++++++++++++- ops/vps/.env.example | 8 +- ops/vps/README.md | 2 +- ops/vps/docker-compose.yml | 4 +- 18 files changed, 447 insertions(+), 25 deletions(-) rename ops/billing/public/assets/{index-BBBXsVw3.js => index-Cif9tUAp.js} (93%) create mode 100644 ops/billing/test/cryptoPayMode.test.ts diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 0ad3fe23..79e5dbff 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -6,6 +6,7 @@ The English translation covers the latest versions only; the full history (in Ru ## Unreleased +- [tech] Billing: a separate Crypto Pay testnet token (`CRYPTO_PAY_TESTNET_TOKEN`). The testnet switch picks the token and the host together. Invoice creation and the `/v1/crypto-pay` signature use only the active mode. With testnet on and no testnet token, crypto stays disabled and the live token is not sent there. A webhook signed by the other mode returns 200, is logged as ignored, and does not extend a subscription. The billing image was not deployed - [phone] Catalog: one, two or three cards a row, list and compact. The poster keeps quality on the left and the season line at the bottom, plus watching and a progress bar. The list has a status line and no size - [phone] Search: plus opens magnet paste and a .torrent file, and a pasted magnet on any tab opens add. An http(s) .torrent link from another app is added the same way. A file error is visible at once. The feed line drops the tracker phrase, and 0-1 seeds are marked. The sources hint is one line with a close button - [phone] Calendar lists days, Earlier stays folded, and the aired/today plates are gone. Month view marks today with a yellow circle, the selected day in gray, up to three dots plus a count, and a yellow dot for a subscription. A subscription that is any quality and best-only reads "best" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d765ae2..3a6f6723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## Не выпущено +- [tech] Биллинг: отдельный токен Crypto Pay для testnet (`CRYPTO_PAY_TESTNET_TOKEN`). Переключатель testnet выбирает токен и хост вместе. Счёт и подпись вебхука `/v1/crypto-pay` идут только в активный режим. Без testnet-токена крипта в testnet выключена, боевой токен туда не уходит. Вебхук другого режима отвечает 200 и пишется как игнорированный, подписку не продлевает. Образ биллинга не выкатывался - [phone] Каталог: одна, две или три карточки в ряд, список и компактный вид. На постере качество слева и строка сезонов снизу, «смотрите» с полоской. В списке строка статуса, без размера - [phone] Поиск: «+» для magnet и файла .torrent, вставленная magnet-ссылка на любой вкладке открывает добавление. Ссылка http(s) на .torrent из другого приложения добавляется так же. Ошибка файла видна сразу. В ленте короткая строка размера и сидов без «по данным трекера», 0-1 сид выделен. Подсказка источников - одна строка с крестиком - [phone] Календарь: список по дням, «Ранее» свернуто, без плашек «сегодня» и «вышла». В месяце сегодня - желтый круг, выбранный день серый, до трех точек и счетчик, желтая точка у подписки. У подписки «любое качество» и «только лучшее» пишется «лучшее» diff --git a/ops/billing/README.md b/ops/billing/README.md index 7919f16c..f8f7a415 100644 --- a/ops/billing/README.md +++ b/ops/billing/README.md @@ -64,6 +64,7 @@ HSTS (`max-age=15552000; includeSubDomains`, без preload), `X-Content-Type-Op | Токен бота | `BILLING_BOT_TOKEN` | | Секрет ЮMoney | `BILLING_YOOMONEY_SECRET` | | Токен Crypto Pay | `CRYPTO_PAY_TOKEN` | +| Токен Crypto Pay (testnet) | `CRYPTO_PAY_TESTNET_TOKEN` | | SMTP-пароль | `BILLING_SMTP_PASS` | | Ключ API Jackett | `BILLING_JACKETT_API_KEY` | | Ключ API JacRed | `BILLING_JACRED_API_KEY` | @@ -126,14 +127,15 @@ sudo $DC up -d --no-deps billing # | --- | --- | | Включить звёзды, цена подписки и доп. устройства в звёздах | **Настройки**, группа **Звёзды** | | Включить крипту, цены USDT/TON/BTC, курс ₽ за 1, testnet, только не из РФ | **Настройки**, группа **Крипта** | -| Токен Crypto Pay | `CRYPTO_PAY_TOKEN` в `.env`. В админке только «задан» и маска. В лог токен не пишется | -| Вебхук Crypto Pay | `POST /v1/crypto-pay`. Подпись `crypto-pay-api-signature` - HMAC-SHA256 сырого тела, ключ SHA256 токена | -| Testnet | Переключатель в админке. Хост `testnet-pay.crypt.bot`, токен тот же `CRYPTO_PAY_TOKEN` (тестовый токен кладётся в эту переменную) | +| Токен Crypto Pay | `CRYPTO_PAY_TOKEN` в `.env`, хост `pay.crypt.bot`. В админке только «задан» и маска. В лог токен не пишется | +| Токен Crypto Pay (testnet) | `CRYPTO_PAY_TESTNET_TOKEN` в `.env`. В админке только «задан» и маска. В лог токен не пишется | +| Вебхук Crypto Pay | `POST /v1/crypto-pay`. Подпись `crypto-pay-api-signature` - HMAC-SHA256 сырого тела, ключ SHA256 токена активного режима | +| Testnet | Переключатель в админке. Включен: хост `testnet-pay.crypt.bot` и `CRYPTO_PAY_TESTNET_TOKEN`. Выключен: `pay.crypt.bot` и `CRYPTO_PAY_TOKEN`. Если testnet включен, а `CRYPTO_PAY_TESTNET_TOKEN` пуст, счёт не создаётся (`disabled`), боевой токен на testnet не отправляется. Вебхук принимает подпись только активного режима. Подпись другого режима отвечает `200`, пишется в журнал как игнорированная и подписку не продлевает | | Ссылка крипты | В боте (кнопка на сайт Crypto Pay и кнопка Crypto Bot) и в приложении, кнопка «Криптой». Сайт omp-player.online на это указывает | | Возврат звёзд | Карточка пользователя, «Вернуть звёзды» (`refundStarPayment`). Дни этой кнопкой не снимаются | | Выручка | Вкладка **Финансы**, блок «по способам». В таблице заказов есть колонка «Способ» | -`pre_checkout_query` только проверяет счёт и не включает подписку. `successful_payment` с валютой XTR включает её. Недоплата (сумма меньше заказа, другая валюта, чужой invoice id) записывается как ошибка и подписку не включает. Неверная подпись Crypto Pay - `400`. Повтор уже принятого вебхука - `200` и без второго продления. +`pre_checkout_query` только проверяет счёт и не включает подписку. `successful_payment` с валютой XTR включает её. Недоплата (сумма меньше заказа, другая валюта, чужой invoice id) записывается как ошибка и подписку не включает. Неверная подпись Crypto Pay - `400`. Подпись неактивного режима - `200` без продления. Повтор уже принятого вебхука - `200` и без второго продления. Флаг «только не из РФ» смотрит код страны Cloudflare (`CF-IPCountry`). Пустой код и `RU` ссылку не выдают. Бот узнаёт страну из последнего захода приложения. diff --git a/ops/billing/admin/src/lib/text.ts b/ops/billing/admin/src/lib/text.ts index 19d15ad5..f9c92883 100644 --- a/ops/billing/admin/src/lib/text.ts +++ b/ops/billing/admin/src/lib/text.ts @@ -95,6 +95,7 @@ const PROBLEM_KIND: Record = { duplicate: 'повтор уведомления', amount: 'неверная сумма', bad_signature: 'неверная подпись', + ignored: 'игнорирован', webhook: 'уведомление', no_account: 'нет аккаунта', no_subscription: 'нет подписки', diff --git a/ops/billing/admin/src/panels/sections.tsx b/ops/billing/admin/src/panels/sections.tsx index dc468614..cf6f64ad 100644 --- a/ops/billing/admin/src/panels/sections.tsx +++ b/ops/billing/admin/src/panels/sections.tsx @@ -584,6 +584,7 @@ const SETTING_GROUPS: { title: string; fields: [string, string][] }[] = [ ['yoomoneyWallet', 'Кошелёк ЮMoney'], ['yoomoneySecret', 'Секрет ЮMoney'], ['cryptoPayToken', 'Токен Crypto Pay'], + ['cryptoPayTestnetToken', 'Токен Crypto Pay (testnet)'], ], }, { @@ -641,7 +642,7 @@ const SETTING_GROUPS: { title: string; fields: [string, string][] }[] = [ ]; /** Secrets are read-only in the panel: they live in the server's .env. */ -const SECRET_KEYS = new Set(['botToken', 'yoomoneySecret', 'cryptoPayToken', 'smtpPass', 'jackettApiKey', 'jacredApiKey', 'indexerPass']); +const SECRET_KEYS = new Set(['botToken', 'yoomoneySecret', 'cryptoPayToken', 'cryptoPayTestnetToken', 'smtpPass', 'jackettApiKey', 'jacredApiKey', 'indexerPass']); /** One read-only secret: «задан ✅, ****abcd» or «не задан», and where it changes. */ function SecretField({ name, label, view, fallback }: { name: string; label: string; view?: SecretView; fallback?: string }) { @@ -827,7 +828,8 @@ export function SettingsCard({ setCryptoTest(e.target.checked); }} /> - Тестовая сеть Crypto Pay (testnet-pay.crypt.bot). Токен всё равно берётся из CRYPTO_PAY_TOKEN. + Тестовая сеть Crypto Pay (testnet-pay.crypt.bot). Токен этого режима:{' '} + {cryptoTest ? String(settings.cryptoPayTestnetToken || 'не задан') : String(settings.cryptoPayToken || 'не задан')}.