Архитектура приёма криптовалютных платежей USDT TRC20

CARA Pay

Архитектура приёма криптовалютных платежей USDT TRC20

Версия: 1.0
Статус: MVP Architecture Specification
Компонент: Crypto Payment Processing
Поддерживаемая сеть: TRON Mainnet
Поддерживаемый актив: USDT TRC20
Срок действия котировки и платёжной ссылки: 15 минут
Модель адресов MVP: уникальный депозитный адрес для каждой криптоплатёжной попытки
Фиксированный сбор CARA Pay: 2.00 USDT
Точность клиентских сумм: 2 знака после запятой


1. Назначение системы

CARA Pay предоставляет мерчанту возможность создать платёжную ссылку в валюте заказа, например IDR, после чего клиент выбирает способ оплаты:

  1. банковский перевод в RUB;

  2. криптовалютный перевод USDT в сети TRON.

Если клиент выбирает оплату USDT TRC20, система:

  1. рассчитывает сумму заказа в USDT;

  2. округляет сумму до двух знаков;

  3. добавляет фиксированный сбор 2.00 USDT;

  4. выделяет уникальный депозитный TRON-адрес;

  5. активирует адрес в сети TRON;

  6. показывает клиенту адрес, QR-код и точную итоговую сумму;

  7. отслеживает входящий перевод USDT;

  8. подтверждает платёж после окончательного подтверждения транзакции;

  9. переводит полученные USDT с депозитного адреса на основной Treasury Wallet;

  10. сохраняет полную историю котировки, оплаты, сетевых расходов и sweep-транзакции.

Основной принцип MVP:

Один Payment Order
→ максимум один Crypto Payment Attempt
→ один уникальный TRON Deposit Address
→ одна ожидаемая итоговая сумма USDT
→ один принятый клиентский платёж
→ один основной sweep в Treasury Wallet

Уникальный адрес, однажды показанный клиенту, больше никогда не используется для другой платёжной операции.


2. Основные решения MVP

2.1. Поддерживается только одна сеть

В MVP поддерживается исключительно:

Network: TRON Mainnet
Asset: USDT
Token standard: TRC20

Не поддерживаются:

2.2. Разрешён только официальный контракт USDT

Разрешённый контракт:

TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t

Этот адрес опубликован Tether как текущий контракт USD₮ в сети TRON. Проверка платежа должна выполняться по адресу контракта, а не только по символу или названию токена.

Правило:

network = TRON_MAINNET
AND token_contract = TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t

Любой другой токен не считается оплатой, даже если:

2.3. Уникальный адрес для каждой криптопопытки

Каждая Crypto Payment Attempt получает отдельный адрес:

Payment Order A
└── Crypto Attempt A
    └── Deposit Address A

Payment Order B
└── Crypto Attempt B
    └── Deposit Address B

Даже если обе операции имеют одинаковую сумму:

Order A → 52.00 USDT → Address A
Order B → 52.00 USDT → Address B

Система однозначно определяет платёж по адресу назначения.

2.4. Фиксированный сбор 2.00 USDT

К рассчитанной стоимости заказа добавляется:

crypto_processing_fee = 2.00 USDT

Этот сбор предназначен для покрытия:

Внутри системы рекомендуется использовать название:

crypto_processing_fee

На странице оплаты более корректное клиентское название:

Комиссия сети и обработки

Важно: эти 2.00 USDT являются фиксированным сбором CARA Pay, а не точным фактическим размером комиссии TRON. Реальная стоимость активации и sweep может быть меньше или больше.

2.5. Комиссия кошелька клиента оплачивается отдельно

Если кошелёк или биржа клиента взимает комиссию за вывод, эта комиссия не должна вычитаться из суммы, поступающей CARA Pay.

Например:

Итоговая сумма CARA Pay: 52.00 USDT
Комиссия биржи клиента:    1.00 USDT
Со счёта клиента списано: 53.00 USDT
На CARA Pay поступило:    52.00 USDT

Если биржа отправила только:

51.00 USDT

платёж считается недоплаченным.


3. Термины

Payment Order

Основная операция, созданная мерчантом.

Содержит:

Пример:

Order amount: 5 000 000 IDR
External Order ID: ORDER-8711

Payment Option

Конкретный доступный способ оплаты Payment Order.

Примеры:

RUB_BANK
USDT_TRC20

Каждый Payment Option содержит собственную котировку и сумму.

Crypto Payment Attempt

Конкретная попытка оплатить Payment Order через USDT TRC20.

В MVP действует ограничение:

Один Payment Order
→ не более одной Crypto Payment Attempt

Если ссылка истекла, создаётся новый Payment Order и новый Crypto Payment Attempt.

Deposit Address

Уникальный TRON-адрес, выделенный одной Crypto Payment Attempt.

На него клиент отправляет USDT.

Treasury Wallet

Основной адрес CARA Pay, на который собираются USDT с депозитных адресов.

Sweep

Внутренний перевод:

Deposit Address
→ Treasury Wallet

Activation Wallet

Служебный TRON-кошелёк, который активирует новые депозитные адреса.

Resource Wallet

Служебный кошелёк, который:

Crypto Processing Fee

Фиксированный сбор CARA Pay:

2.00 USDT

Blockchain Transfer

Зафиксированный в сети TRON перевод токена между адресами.


4. Финансовая модель

4.1. Исходная сумма заказа

Пример:

order_amount = 5 000 000 IDR
order_currency = IDR

Эта сумма принадлежит исходному Payment Order и после создания не меняется.

4.2. Курс IDR/USDT

Система получает рыночный курс:

client_rate = 16 600.00 IDR за 1 USDT

Курс фиксируется в Rate Snapshot и действует до expires_at.

4.3. Расчёт основной суммы USDT

Формула:

crypto_principal_amount =
round(
    order_amount_idr / client_rate_idr_per_usdt,
    2,
    ROUND_HALF_UP
)

Пример:

5 000 000 / 16 600
= 301.204819...

crypto_principal_amount
= 301.20 USDT

4.4. Добавление сбора

crypto_processing_fee = 2.00 USDT

Итог:

crypto_total_amount =
crypto_principal_amount
+ crypto_processing_fee

Пример:

Основная сумма:            301.20 USDT
Комиссия сети и обработки:   2.00 USDT
Итого к оплате:            303.20 USDT

4.5. Точность

Все клиентские суммы USDT имеют только два знака:

301.20 USDT
52.00 USDT
147.35 USDT

Запрещено отображать клиенту:

301.204819 USDT
52.001278 USDT

Типы данных:

crypto_principal_amount DECIMAL(24,2)
crypto_processing_fee  DECIMAL(24,2)
crypto_total_amount    DECIMAL(24,2)

Для сравнения с блокчейном дополнительно хранится raw-сумма.

USDT TRC20 использует шесть десятичных знаков, поэтому:

303.20 USDT
=
303200000 raw units

Формула:

expected_amount_raw =
crypto_total_amount × 1 000 000

Пример:

303.20 × 1 000 000
= 303 200 000

Последние четыре знака raw-суммы для клиентских платежей CARA Pay всегда равны нулю.

4.6. Разделение основной суммы и сбора

После подтверждения платежа бухгалтерский учёт должен разделять:

303.20 USDT получено

├── 301.20 USDT — основная сумма операции
└──   2.00 USDT — crypto processing fee

Фиксированный сбор не увеличивает сумму заказа мерчанта.

4.7. Риск превышения фактических расходов

Фиксированный сбор 2.00 USDT не гарантирует, что реальная стоимость активации и sweep всегда составит не более 2.00 USDT.

Правило MVP:

Если фактические сетевые расходы > 2.00 USDT,
разницу временно несёт CARA Pay.

По каждой операции обязательно рассчитывается:

fee_collected_usdt
actual_activation_cost_usdt
actual_sweep_cost_usdt
actual_total_cost_usdt
fee_margin_usdt

Формула:

fee_margin_usdt =
fee_collected_usdt
- actual_total_cost_usdt

5. Связь с существующим Payment Order

Исходный Payment Order остаётся общей сущностью для RUB и USDT.

Payment Order
├── RUB Payment Option
└── USDT TRC20 Payment Option

Рекомендуемая структура:

Payment Order
├── merchant_id
├── external_order_id
├── order_amount
├── order_currency
├── created_at
├── expires_at
├── accepted_payment_method
└── accepted_payment_attempt_id

Конкретные суммы оплаты выносятся из payment_orders в дочерние сущности:

RUB option:
├── payment_amount_rub
├── RUB rate snapshot
└── bank QR

Crypto option:
├── crypto_principal_amount
├── crypto_processing_fee
├── crypto_total_amount
├── USDT rate snapshot
└── deposit address

6. Момент создания криптопопытки

Уникальный адрес не нужно создавать и активировать сразу при создании каждого Payment Order.

Рекомендуемый процесс:

1. Мерчант создаёт Payment Order.
2. Клиент открывает ссылку.
3. Клиент выбирает «Оплатить USDT TRC20».
4. Только после выбора создаётся Crypto Payment Attempt.
5. Система выделяет и активирует адрес.
6. После успешной активации адрес показывается клиенту.

Это позволяет не расходовать TRX на адреса для клиентов, которые выбрали RUB или вообще не стали платить.

6.1. Идемпотентность выбора

Повторное нажатие кнопки USDT не должно создавать новый адрес.

Ограничение:

UNIQUE(payment_order_id)

для активной Crypto Payment Attempt в MVP.

Алгоритм:

if crypto_attempt already exists:
    return existing crypto_attempt
else:
    create crypto_attempt

6.2. Запрет создания после expiry

Если:

now >= payment_order.expires_at

новую Crypto Payment Attempt создавать нельзя.

Ответ:

{
  "error": "PAYMENT_ORDER_EXPIRED",
  "message": "Срок действия платёжной ссылки истёк"
}

7. Неизменяемость криптопопытки

После публикации адреса запрещается изменять:

Разрешается изменять:


8. Архитектура TRON-адресов

8.1. Что является дочерним адресом

CARA Pay использует отдельную master seed, из которой детерминированно создаются TRON-адреса.

CARA Pay Deposit Master Key
├── index 1 → Deposit Address 1
├── index 2 → Deposit Address 2
├── index 3 → Deposit Address 3
└── ...

На уровне сети TRON каждый такой адрес является самостоятельным аккаунтом:

TRON использует ключи на кривой secp256k1, а адрес может храниться в Hex- и Base58Check-формате; пользовательские Base58Check-адреса начинаются с T.

8.2. Derivation path

Точный derivation path должен быть зафиксирован до production-запуска и никогда не меняться для существующей версии ключа.

Пример:

m/44'/195'/0'/0/{index}

В базе сохраняются:

wallet_key_version
derivation_index
derivation_path
address_base58
address_hex

Пример:

wallet_key_version = tron-deposit-v1
derivation_index = 1842
derivation_path = m/44'/195'/0'/0/1842
address_base58 = TY...
address_hex = 41...

8.3. Версии ключей

Нельзя привязывать всю систему навсегда к одной seed.

Используется понятие:

wallet_key_version

Примеры:

tron-deposit-v1
tron-deposit-v2

При ротации:

8.4. Запрет переиспользования

После того как адрес был показан клиенту:

address_reuse_allowed = false

Даже если:

Причина: клиент может сохранить адрес и выполнить поздний перевод.


9. Key Management

9.1. Приватные ключи не хранятся в основной базе

Запрещено хранить:

private_key
master_seed
mnemonic

в:

9.2. Компоненты

Main Application
├── знает адрес;
├── знает derivation index;
├── знает Payment Attempt;
└── не может подписывать транзакции.

Address Allocator
├── получает новый адрес;
├── резервирует index;
└── не выдаёт приватный ключ приложению.

Signing Service
├── имеет доступ к master key;
├── производит дочерний ключ;
├── подписывает разрешённый sweep;
└── не отдаёт приватный ключ наружу.

9.3. Политика Signing Service

Signing Service должен подписывать только транзакции, соответствующие всем условиям:

network = TRON_MAINNET

contract =
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t

method = transfer(address,uint256)

destination =
approved_treasury_address

Запрещается принимать произвольный адрес получателя от frontend или пользователя.

9.4. Treasury Wallet

Treasury Wallet должен использовать другой ключевой контур.

Deposit Master Wallet
→ создаёт клиентские депозитные адреса

Treasury Wallet
→ принимает собранные USDT

Не рекомендуется использовать seed текущего личного Trust Wallet как production master seed.

9.5. Резервные копии

Для master seed обязательны:


10. Генерация и резервирование адреса

10.1. Address Pool

Система может заранее генерировать пул неактивированных адресов:

available
available
available

Активация выполняется только после назначения Crypto Payment Attempt.

10.2. Алгоритм Allocation

1. Заблокировать счётчик derivation index.
2. Получить следующий индекс.
3. Сформировать derivation path.
4. Получить публичный адрес через Key Service.
5. Проверить валидность Base58Check.
6. Сохранить Base58 и Hex.
7. Убедиться в уникальности.
8. Связать адрес с Crypto Payment Attempt.
9. Установить address_status = assigned.
10. Записать событие address.assigned.

Ограничения:

UNIQUE(network, address_base58)
UNIQUE(wallet_key_version, derivation_index)
UNIQUE(crypto_payment_attempt_id)

10.3. Конкурентность

Индекс получается атомарно:

SELECT ...
FOR UPDATE

или через database sequence.

Пропуски индексов допустимы.

Повторное использование индекса запрещено.


11. Активация адреса

Новый локально созданный TRON-адрес ещё не существует как активный аккаунт в сети. TRON предусматривает активацию через перевод TRX/TRC10 или через createaccount. В актуальной документации также указана комиссия активации 1 TRX и возможный дополнительный расход Bandwidth; эти параметры должны считаться внешними и изменяемыми, а не навсегда захардкоженными.

11.1. Правило MVP

Адрес активируется до показа клиенту.

generate
→ assign
→ activate
→ verify activation
→ publish

Запрещено показывать адрес, пока:

activation_status != confirmed

11.2. Activation Wallet

Отдельный кошелёк:

TRON Activation Wallet

Он:

11.3. Статусы активации

not_started
queued
broadcast
confirming
confirmed
failed

11.4. Алгоритм

1. Получить назначенный депозитный адрес.
2. Проверить его существование в сети.
3. Если адрес уже активен:
   activation_status = confirmed.
4. Если не активен:
   сформировать activation transaction.
5. Подписать её Activation Wallet.
6. Отправить в TRON.
7. Сохранить activation_tx_hash.
8. Дождаться confirmed state.
9. Повторно проверить существование аккаунта.
10. Установить activation_status = confirmed.
11. Разрешить публикацию адреса.

11.5. Ошибка активации

Если активация не завершилась до expiry:

crypto_creation_status = failed

Адрес клиенту не показывается.

API возвращает:

{
  "error": "TRON_ADDRESS_ACTIVATION_FAILED",
  "message": "Криптовалютный способ оплаты временно недоступен"
}

12. Создание Crypto Payment Attempt

12.1. Входные данные

Из Payment Order берутся:

payment_order_id
merchant_id
order_amount
order_currency
created_at
expires_at

Клиент не передаёт сумму самостоятельно.

12.2. Алгоритм

1. Найти Payment Order по public token.
2. Проверить link_status = active.
3. Проверить now < expires_at.
4. Проверить payment_status = pending.
5. Проверить отсутствие принятого платежа.
6. Проверить, существует ли Crypto Payment Attempt.
7. Если существует — вернуть её.
8. Получить актуальный IDR/USDT Rate Snapshot.
9. Проверить свежесть курса.
10. Рассчитать principal USDT.
11. Округлить principal до 2 знаков.
12. Добавить fixed fee 2.00 USDT.
13. Рассчитать total USDT.
14. Рассчитать expected_amount_raw.
15. Создать Crypto Payment Attempt.
16. Выделить уникальный Deposit Address.
17. Активировать Deposit Address.
18. Проверить активацию.
19. Сформировать QR payload.
20. Сохранить QR image и SHA-256.
21. Установить creation_status = ready.
22. Записать payment_address.ready.
23. Вернуть данные клиенту.

12.3. Атомарность

Клиенту разрешено увидеть адрес только после успешного создания:

При частичном сбое операция остаётся внутренней и не публикуется.


13. Ответ клиенту

Пример:

{
  "payment_order_id": "0198...",
  "crypto_payment_attempt_id": "0198...",
  "network": "TRON",
  "asset": "USDT",
  "token_standard": "TRC20",
  "token_contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",

  "principal_amount": "301.20",
  "processing_fee": "2.00",
  "total_amount": "303.20",
  "currency": "USDT",

  "deposit_address": "TY...",
  "created_at": "2026-07-12T12:00:00Z",
  "expires_at": "2026-07-12T12:15:00Z",

  "creation_status": "ready",
  "payment_status": "pending"
}

14. Публичная страница оплаты

Страница должна показывать:

Сеть:
TRON (TRC20)

Актив:
USDT

Стоимость заказа:
301.20 USDT

Комиссия сети и обработки:
2.00 USDT

Итого к оплате:
303.20 USDT

Адрес:
TY...

Ссылка действует до:
20:15:00

14.1. Обязательные предупреждения

Отправляйте только USDT в сети TRON (TRC20).

Отправьте ровно 303.20 USDT.

Комиссия вашего кошелька или биржи должна
оплачиваться отдельно и не должна вычитаться
из суммы 303.20 USDT.

Не отправляйте TRX или другие токены.

Платёж в другой сети может быть потерян.

14.2. Кнопки

Скопировать адрес
Скопировать сумму
Показать QR
Я оплатил
Вернуться к выбору способа

Кнопка «Я оплатил» не подтверждает платёж. Она может только:

Источником истины остаётся блокчейн.

14.3. QR-код

Для MVP рекомендуется кодировать в QR только адрес:

TY...

Сумма отображается и копируется отдельно.

Причина: разные кошельки неодинаково обрабатывают URI с сетью, токеном и суммой.

Сохраняются:

qr_payload
qr_image_storage_key
qr_image_sha256
qr_created_at

14.4. После обнаружения транзакции

Страница меняется на:

Платёж обнаружен.

Ожидаем подтверждение сети TRON.

Не отправляйте платёж повторно
и не оплачивайте другим способом.

После обнаружения скрываются активные кнопки оплаты через RUB.


15. Истечение ссылки

15.1. Срок действия

expires_at = payment_order.created_at + 15 минут

Crypto Payment Attempt наследует expires_at Payment Order.

Она не получает собственные дополнительные 15 минут после выбора USDT.

15.2. Что истекает

Истекают:

Не истекают:

Нельзя технически отключить TRON-адрес после expiry.

15.3. Поведение страницы

После expiry:

Срок действия платёжной ссылки истёк.

Не переводите средства на ранее показанный адрес.
Запросите новую ссылку у продавца.

Запрещены:


16. Blockchain Monitor

16.1. Назначение

Blockchain Monitor отслеживает:

TRON рекомендует интеграциям кошельков и бирж использовать TronGrid либо собственный FullNode; входящие операции можно получать через историю аккаунта или путём обработки блоков. В документации также указаны лимиты публичного TronGrid, поэтому высокая нагрузка должна учитываться отдельно.

16.2. Архитектура MVP

Primary source:
TRON API / TronGrid

Secondary control:
periodic reconciliation scan

Blockchain Monitor должен иметь:

block cursor
last processed block
last confirmed block
retry cursor
reconciliation cursor

16.3. Не полагаться только на webhook

Даже если используется event subscription, система обязана периодически повторно сканировать блоки.

Причины:

16.4. Backfill

После перезапуска:

start_block =
last_confirmed_processed_block - safety_window

Повторная обработка безопасна благодаря уникальному ключу события.


17. Определение входящего USDT-перевода

TRC20-переводы определяются по событию Transfer. Сам стандарт содержит адрес отправителя, адрес получателя и значение перевода.

Для каждого события сохраняются:

network
token_contract
transaction_hash
event_index
block_number
block_hash
block_timestamp
from_address
to_address
amount_raw
amount
observed_at
confirmed_at

17.1. Уникальность события

UNIQUE(network, transaction_hash, event_index)

Нельзя использовать только transaction_hash, потому что одна транзакция может содержать несколько событий.

17.2. Проверка контракта

if token_contract != official_usdt_contract:
    do not count as payment

17.3. Поиск Payment Attempt

to_address
↓
tron_deposit_addresses
↓
crypto_payment_attempt_id
↓
payment_order_id
↓
merchant_id

Поскольку адрес уникален для попытки, сумма не используется как основной идентификатор операции.


18. Подтверждение транзакции

Первичное обнаружение события ещё не означает окончательное подтверждение оплаты.

TRON различает операции, присутствующие в текущем состоянии FullNode, и подтверждённые операции из solidified state. Для smart-contract-транзакции необходимо проверить confirmed API и успешный результат исполнения контракта.

18.1. Стадии

observed
→ confirming
→ confirmed

18.2. Условия confirmed

Транзакция считается подтверждённой, если:

  1. доступна через confirmed/solidified API;

  2. блок подтверждён сетью;

  3. smart contract execution имеет результат SUCCESS;

  4. присутствует корректное событие Transfer;

  5. контракт равен официальному USDT;

  6. получатель равен депозитному адресу;

  7. amount больше нуля.

У Tether USD₮ TRC20 используется более старая реализация, которая может явно не возвращать Boolean из transfer. Поэтому нельзя полагаться только на возвращаемое значение метода: требуется проверять receipt, confirmed state и событие Transfer.

18.3. Время оплаты

Для определения своевременности используется:

block_timestamp

Не используются:

Правило:

block_timestamp <= expires_at
→ timely

block_timestamp > expires_at
→ paid_late

19. Сопоставление суммы

19.1. Точная оплата

received_amount_raw = expected_amount_raw

Результат:

payment_status = paid
review_status = not_required
execution_status = ready

19.2. Недоплата

received_amount_raw < expected_amount_raw

Результат:

payment_status = underpaid
review_status = required
execution_status = not_started

Пример:

Ожидалось: 52.00
Получено:  51.00

19.3. Переплата

received_amount_raw > expected_amount_raw

Результат:

payment_status = overpaid
review_status = required

19.4. Частичные платежи

В MVP несколько переводов автоматически не суммируются.

Например:

ожидалось 52.00

получено:
30.00
22.00

Результат:

payment_status = partial_payment
review_status = required

Даже если сумма переводов равна 52.00.

19.5. Повторный платёж

Если точный платёж уже принят, а на тот же адрес приходит ещё один перевод:

payment_status основного Order не изменяется

новый transfer:
duplicate_payment = true
review_status = required

19.6. Неправильный токен

Если на адрес пришёл другой TRC20-токен:

transfer_status = wrong_asset
review_status = required

Payment Order остаётся неоплаченным.

19.7. TRX вместо USDT

Поступление TRX:


20. Поздний платёж

Если:

block_timestamp > expires_at

то:

payment_status = paid_late
review_status = required
execution_status = not_started

Даже если сумма точная.

Адрес мониторится бессрочно или в соответствии с утверждённой политикой хранения и on-chain reconciliation.


21. Конкуренция RUB и USDT

Клиент потенциально может оплатить:

В Payment Order хранится:

accepted_payment_attempt_id
accepted_payment_method

21.1. Первый окончательно подтверждённый платёж

Первый платёж, который прошёл окончательную проверку, блокирует Payment Order:

SELECT payment_order
FOR UPDATE

После этого:

accepted_payment_attempt_id = crypto_attempt_id
accepted_payment_method = USDT_TRC20

21.2. Второй платёж

Если позже обнаруживается банковская оплата:

second_payment_status = duplicate_payment
review_status = required

Автоматическое повторное исполнение запрещено.

21.3. Обнаруженный, но не подтверждённый USDT

Как только USDT-транзакция обнаружена:


22. Статусы

22.1. Crypto Creation Status

creating
address_allocated
activating
ready
failed

22.2. Crypto Payment Status

pending
detected
confirming
paid
paid_late
underpaid
overpaid
partial_payment
wrong_asset
duplicate_payment
refunded

22.3. Address Status

generated
available
assigned
activation_pending
active
used
retired
compromised

После публикации адрес не возвращается в available.

22.4. Blockchain Transfer Status

observed
confirming
confirmed
failed
orphaned
ignored

22.5. Sweep Status

not_started
queued
estimating_resources
awaiting_resources
signing
broadcast
confirming
completed
failed
blocked

23. Переходы статусов

Crypto Creation

creating
→ address_allocated
→ activating
→ ready

Ошибка:

creating → failed
address_allocated → failed
activating → failed

Crypto Payment

pending → detected
detected → confirming

confirming → paid
confirming → paid_late
confirming → underpaid
confirming → overpaid
confirming → partial_payment

Sweep

not_started
→ queued
→ estimating_resources
→ awaiting_resources
→ signing
→ broadcast
→ confirming
→ completed

Ошибка:

estimating_resources → failed
signing → failed
broadcast → failed
confirming → failed

Повторный запуск разрешён только идемпотентно.


24. Sweep в Treasury Wallet

24.1. Когда выполняется sweep

Для точной своевременной оплаты:

payment_status = paid
transfer_status = confirmed

После этого:

sweep_status = queued

Для поздних, ошибочных, частичных и повторных платежей средства также могут быть переведены в Treasury, но это не означает автоматического принятия платежа.

Правильное разделение:

Sweep
= внутреннее безопасное перемещение активов

Payment acceptance
= бизнес-решение по исполнению заказа

24.2. Что переводится

Рекомендуется переводить весь подтверждённый баланс официального USDT на депозитном адресе:

sweep_amount =
confirmed_usdt_balance

Если во время sweep поступил новый платёж, остаток обрабатывается следующей sweep-операцией.

24.3. Ресурсы TRON

TRC20-перевод использует Energy и Bandwidth. Потребление Energy может изменяться из-за Dynamic Energy Model, поэтому нельзя навсегда захардкодить предполагаемую себестоимость. TRON предоставляет метод estimateEnergy, который возвращает ожидаемый расход перед созданием транзакции.

24.4. Resource Strategy

Система должна поддерживать абстракцию:

resource_strategy

Значения:

DELEGATED_ENERGY
RENTED_ENERGY
TRX_FEE_FUNDING

Предпочтительный порядок:

1. Собственная делегированная Energy.
2. Арендованная Energy.
3. Пополнение TRX и прямое покрытие расходов.

TRON позволяет делегировать Energy и Bandwidth другому активированному внешнему аккаунту.

24.5. Алгоритм sweep

1. Получить exclusive lock на Deposit Address.
2. Проверить отсутствие активного sweep.
3. Получить confirmed USDT balance.
4. Если balance = 0:
   завершить без транзакции.
5. Рассчитать sweep amount.
6. Вызвать estimateEnergy.
7. Получить energy_required.
8. Рассчитать безопасный fee_limit.
9. Выбрать resource strategy.
10. Предоставить ресурсы депозитному адресу.
11. Сформировать USDT transfer в Treasury.
12. Передать unsigned transaction в Signing Service.
13. Signing Service проверяет:
    - сеть;
    - контракт;
    - destination;
    - amount;
    - fee limit.
14. Подписать дочерним приватным ключом.
15. Broadcast transaction.
16. Сохранить sweep_tx_hash.
17. Дождаться confirmed state.
18. Проверить receipt.result = SUCCESS.
19. Повторно проверить Deposit Address balance.
20. Сохранить фактический расход ресурсов.
21. Установить sweep_status = completed.
22. Освободить или вернуть делегированные ресурсы.

24.6. Недостаточно ресурсов

Если фактическая оценка выше допустимого операционного лимита:

sweep_status = awaiting_resources

Payment Order при этом остаётся оплаченным.

Нельзя отменять клиентскую оплату из-за того, что CARA Pay временно не смог выполнить внутренний sweep.

24.7. Идемпотентность

Один и тот же sweep нельзя отправить дважды.

Перед созданием новой транзакции проверяются:


25. Учёт фактической комиссии

По каждой операции сохраняются:

activation_cost_trx
activation_cost_usdt
energy_required
energy_consumed
bandwidth_consumed
trx_burned
resource_rental_cost_trx
resource_rental_cost_usdt
sweep_cost_trx
sweep_cost_usdt
total_network_cost_usdt
fee_collected_usdt
fee_margin_usdt

Для пересчёта TRX в USDT сохраняется отдельный ценовой снимок:

trx_usdt_rate
rate_source
rate_fetched_at

Не допускается пересчитывать историческую себестоимость по текущему курсу.


26. Структура базы данных

26.1. payment_orders

Дополняется полями:

accepted_payment_method ENUM NULL
accepted_payment_attempt_id UUID NULL
payment_detected_at TIMESTAMP NULL

26.2. payment_options

id UUIDv7 PK
payment_order_id UUID FK

method ENUM
payment_currency VARCHAR

principal_amount DECIMAL(24,2)
fee_amount DECIMAL(24,2)
total_amount DECIMAL(24,2)

rate_snapshot_id UUID FK
created_at TIMESTAMP
expires_at TIMESTAMP

status ENUM

26.3. crypto_payment_attempts

id UUIDv7 PK
payment_order_id UUID UNIQUE FK
payment_option_id UUID FK

network VARCHAR
asset_code VARCHAR
token_standard VARCHAR
token_contract VARCHAR
token_decimals INT

principal_amount DECIMAL(24,2)
processing_fee DECIMAL(24,2)
expected_amount DECIMAL(24,2)
expected_amount_raw NUMERIC(78,0)

deposit_address_id UUID UNIQUE FK
rate_snapshot_id UUID FK
fee_snapshot_id UUID FK

creation_status ENUM
payment_status ENUM
review_status ENUM
sweep_status ENUM

created_at TIMESTAMP
expires_at TIMESTAMP

first_detected_at TIMESTAMP NULL
confirmed_at TIMESTAMP NULL
paid_at TIMESTAMP NULL
late_paid_at TIMESTAMP NULL

26.4. crypto_fee_snapshots

id UUIDv7 PK

fee_type VARCHAR
fee_amount DECIMAL(24,2)
fee_currency VARCHAR

policy_version VARCHAR
display_name VARCHAR

created_at TIMESTAMP

Пример:

fee_type = TRON_PROCESSING_FEE
fee_amount = 2.00
fee_currency = USDT
policy_version = tron-mvp-v1

26.5. wallet_key_versions

id UUIDv7 PK
key_version VARCHAR UNIQUE

network VARCHAR
purpose VARCHAR
derivation_path_template VARCHAR

status ENUM
activated_at TIMESTAMP
retired_at TIMESTAMP NULL

signing_provider_reference VARCHAR
created_at TIMESTAMP

Private key и seed отсутствуют.

26.6. tron_deposit_addresses

id UUIDv7 PK

wallet_key_version_id UUID FK
derivation_index BIGINT
derivation_path VARCHAR

address_base58 VARCHAR
address_hex VARCHAR

crypto_payment_attempt_id UUID UNIQUE NULL

address_status ENUM
activation_status ENUM

generated_at TIMESTAMP
assigned_at TIMESTAMP NULL
activated_at TIMESTAMP NULL
published_at TIMESTAMP NULL
used_at TIMESTAMP NULL
retired_at TIMESTAMP NULL

Ограничения:

UNIQUE(address_base58)
UNIQUE(address_hex)
UNIQUE(wallet_key_version_id, derivation_index)

26.7. tron_address_activations

id UUIDv7 PK
deposit_address_id UUID FK

activation_method ENUM
activation_wallet_id UUID FK

transaction_hash VARCHAR UNIQUE NULL
status ENUM

cost_trx DECIMAL(24,6) NULL
cost_usdt DECIMAL(24,8) NULL

broadcast_at TIMESTAMP NULL
confirmed_at TIMESTAMP NULL
failed_at TIMESTAMP NULL

raw_response JSONB
created_at TIMESTAMP

26.8. tron_token_transfers

id UUIDv7 PK

network VARCHAR
token_contract VARCHAR

transaction_hash VARCHAR
event_index BIGINT

block_number BIGINT
block_hash VARCHAR
block_timestamp TIMESTAMP

from_address_base58 VARCHAR
to_address_base58 VARCHAR

amount_raw NUMERIC(78,0)
amount DECIMAL(36,6)

transfer_status ENUM
is_confirmed BOOLEAN

observed_at TIMESTAMP
confirmed_at TIMESTAMP NULL

raw_event JSONB
raw_transaction JSONB

Ограничение:

UNIQUE(network, transaction_hash, event_index)

26.9. crypto_payment_matches

id UUIDv7 PK

crypto_payment_attempt_id UUID FK
tron_token_transfer_id UUID FK

contract_match BOOLEAN
address_match BOOLEAN
amount_match BOOLEAN
time_match BOOLEAN

match_result ENUM
matched_automatically BOOLEAN

created_at TIMESTAMP
reviewed_at TIMESTAMP NULL
reviewed_by_user_id UUID NULL

26.10. tron_sweeps

id UUIDv7 PK

deposit_address_id UUID FK
crypto_payment_attempt_id UUID FK

source_address VARCHAR
destination_address VARCHAR

token_contract VARCHAR
amount_raw NUMERIC(78,0)
amount DECIMAL(36,6)

resource_strategy ENUM
estimated_energy BIGINT NULL
actual_energy BIGINT NULL
actual_bandwidth BIGINT NULL

fee_limit_sun BIGINT NULL
trx_burned DECIMAL(24,6) NULL

transaction_hash VARCHAR UNIQUE NULL
status ENUM

created_at TIMESTAMP
broadcast_at TIMESTAMP NULL
confirmed_at TIMESTAMP NULL
failed_at TIMESTAMP NULL

error_code VARCHAR NULL
error_message TEXT NULL

raw_transaction JSONB NULL
raw_receipt JSONB NULL

26.11. blockchain_sync_state

id UUIDv7 PK
network VARCHAR
worker_name VARCHAR

last_seen_block BIGINT
last_confirmed_block BIGINT
last_reconciled_block BIGINT

updated_at TIMESTAMP

27. API

27.1. Выбор оплаты USDT

POST /v1/public/payment-links/{public_token}/crypto-attempt
Idempotency-Key: UUID

Тело может быть пустым, поскольку:

27.2. Получение статуса

GET /v1/public/payment-links/{public_token}/crypto-attempt

27.3. Статус для мерчанта

GET /v1/payment-orders/{payment_order_id}

Пример:

{
  "payment_status": "paid",
  "accepted_payment_method": "USDT_TRC20",
  "crypto_payment": {
    "principal_amount": "301.20",
    "processing_fee": "2.00",
    "total_amount": "303.20",
    "transaction_hash": "abc...",
    "block_timestamp": "2026-07-12T12:10:22Z",
    "confirmed_at": "2026-07-12T12:11:30Z",
    "sweep_status": "queued"
  }
}

27.4. Отмена

Отмена Payment Order разрешена, если:

payment_status = pending
AND crypto_payment_status = pending
AND no blockchain transfer detected

Адрес после отмены всё равно не переиспользуется и продолжает мониториться.


28. Webhooks

События:

crypto.payment_address_ready
crypto.payment_detected
crypto.payment_confirming
crypto.payment_paid
crypto.payment_paid_late
crypto.payment_underpaid
crypto.payment_overpaid
crypto.payment_partial
crypto.payment_duplicate
crypto.payment_wrong_asset

crypto.sweep_queued
crypto.sweep_completed
crypto.sweep_failed

Мерчанту обязательно отправляются:

crypto.payment_paid
crypto.payment_paid_late
crypto.payment_underpaid
crypto.payment_overpaid
crypto.payment_duplicate

Sweep-события могут оставаться внутренними.

Пример:

{
  "event_id": "0198...",
  "event_type": "crypto.payment_paid",
  "created_at": "2026-07-12T12:11:30Z",
  "data": {
    "payment_order_id": "0198...",
    "external_order_id": "ORDER-8711",
    "payment_method": "USDT_TRC20",
    "principal_amount": "301.20",
    "processing_fee": "2.00",
    "total_amount": "303.20",
    "transaction_hash": "abc...",
    "payment_status": "paid"
  }
}

29. Административная панель

29.1. Список операций

Столбцы:

Дата
Payment Order
Мерчант
External Order ID
Сумма заказа
Principal USDT
Processing Fee
Total USDT
Deposit Address
Transaction Hash
Статус оплаты
Статус проверки
Статус sweep
Фактический расход
Fee Margin

29.2. Карточка криптоплатежа

Payment

Payment Order ID
Crypto Attempt ID
Merchant
External Order ID
Created At
Expires At

Quote

Order amount
Order currency
Market rate
Client rate
Rate source
Principal USDT
Processing Fee
Total USDT

Address

Address Base58
Address Hex
Wallet Key Version
Derivation Index
Derivation Path
Activation Status
Activation TX
Activation Cost
Published At

Incoming Transfer

Transaction Hash
Event Index
Block Number
Block Timestamp
From Address
To Address
Token Contract
Amount
Confirmation Status
Confirmed At

Sweep

Treasury Address
Sweep Amount
Resource Strategy
Estimated Energy
Actual Energy
TRX Burned
Sweep TX Hash
Sweep Status
Confirmed At

Audit

Все события
Все статусы
Все ручные решения
Все API-запросы
Все webhook delivery attempts

29.3. Ручные действия

Подтвердить поздний платёж
Отклонить поздний платёж
Принять недоплату
Отклонить недоплату
Принять переплату
Зафиксировать возврат
Повторить sweep
Заблокировать sweep
Отметить адрес compromised
Запустить reconciliation

Каждое действие содержит:

user_id
role
old_status
new_status
comment
created_at
IP address
request_id

30. Возвраты

Автоматические возвраты в MVP не выполняются.

Причины:

Процесс:

1. Клиент обращается в поддержку.
2. Оператор проверяет Payment Order.
3. Клиент предоставляет TRON-адрес.
4. Адрес проходит валидацию.
5. Сотрудник подтверждает возврат.
6. Возврат выполняется из Treasury Wallet.
7. Сохраняется refund transaction hash.

Политика возврата фиксированного сбора 2.00 USDT должна быть отдельно закреплена в оферте.

Рекомендуемая техническая конфигурация:

fee_refund_policy:
NON_REFUNDABLE_AFTER_CONFIRMED_PAYMENT

Исключения допускаются вручную.


31. Безопасность

31.1. Allowlist

Жёстко фиксируются:

TRON Mainnet
Official USDT contract
Approved Treasury Address
Approved Activation Wallet
Approved Resource Wallet

Изменение Treasury Address требует:

31.2. Signing Policy

Signing Service отклоняет транзакцию, если:

31.3. Ограничение баланса

На депозитных адресах не должны долго храниться крупные суммы.

Алерты:

balance > configured_limit
paid but not swept > configured_period
unknown token received
second payment received
sweep failed repeatedly

31.4. Компрометация ключа

При подозрении:

wallet_key_version.status = compromised

После этого:


32. Reconciliation

Минимум один раз в сутки система выполняет полную сверку.

32.1. Deposit Address Reconciliation

Для каждого адреса:

database received amount
vs
on-chain confirmed transfers
vs
current USDT balance
vs
completed sweeps

Инвариант:

confirmed incoming
- confirmed outgoing
= current on-chain balance

С учётом возможных дополнительных переводов и возвратов.

32.2. Payment Reconciliation

Проверяется:

paid Payment Order
→ имеет confirmed incoming transfer

confirmed exact incoming transfer
→ имеет Payment Match

completed sweep
→ имеет confirmed outgoing transaction

32.3. Fee Reconciliation

fee collected
vs
activation cost
vs
resource cost
vs
sweep cost
vs
fee margin

33. Мониторинг и алерты

Метрики:

crypto_attempts_created
addresses_generated
addresses_activated
activation_failures
activation_cost_trx

payments_detected
payments_confirmed
payments_paid
payments_paid_late
payments_underpaid
payments_overpaid
payments_partial
duplicate_payments
wrong_asset_transfers

average_detection_delay
average_confirmation_delay

sweeps_queued
sweeps_completed
sweeps_failed
average_sweep_delay

energy_estimated
energy_consumed
trx_burned
fee_collected_usdt
actual_network_cost_usdt
fee_margin_usdt

Критические алерты:

TRON API unavailable
Blockchain cursor not moving
No payments detected for abnormal period
Activation Wallet low TRX
Resource Wallet low resources
Treasury sweep failure
Signing Service unavailable
Address generation collision
Negative fee margin spike
Deposit address balance stuck
Unknown outbound transaction

34. Сценарии

Сценарий 1. Успешная оплата

12:00 — Payment Order создан
12:02 — клиент выбрал USDT
12:02 — адрес выделен
12:03 — адрес активирован
12:03 — клиенту показано 52.00 USDT
12:10 — клиент отправил 52.00 USDT
12:10 — Transfer обнаружен
12:11 — Transfer подтверждён
12:11 — Payment Order paid
12:12 — sweep поставлен в очередь
12:15 — USDT переведены в Treasury

Результат:

payment_status = paid
execution_status = ready
sweep_status = completed

Сценарий 2. Биржа вычла комиссию

Ожидалось: 52.00
Поступило: 51.00

Результат:

payment_status = underpaid
review_status = required

Сценарий 3. Поздняя оплата

Ссылка истекла: 12:15
Block timestamp: 12:18

Результат:

payment_status = paid_late
review_status = required

Сценарий 4. Клиент отправил дважды

Первый перевод: 52.00
Второй перевод: 52.00

Результат:

первый → paid
второй → duplicate_payment
manual review

Сценарий 5. Клиент отправил TRX

Получено: 52 TRX

Результат:

Payment Order остаётся pending
wrong_asset incident создаётся

Сценарий 6. Клиент отправил поддельный USDT

Правильный адрес
Правильная сумма
Другой contract

Результат:

не является оплатой
wrong_asset

Сценарий 7. Sweep временно не выполнен

USDT payment confirmed
Resource Wallet недоступен

Результат:

payment_status = paid
execution_status = ready
sweep_status = awaiting_resources

Сценарий 8. Клиент оплатил RUB и USDT

USDT подтверждён первым
RUB обнаружен позже

Результат:

USDT → accepted payment
RUB → duplicate payment / manual review

35. Инварианты

Один Crypto Payment Attempt принадлежит одному Payment Order.

Один Crypto Payment Attempt имеет один Deposit Address.

Один опубликованный Deposit Address никогда не используется повторно.

Один Payment Order в MVP имеет не более одной Crypto Payment Attempt.

Сеть всегда TRON Mainnet.

Актив всегда официальный USDT TRC20.

Контракт проверяется по allowlist.

Все клиентские суммы имеют два знака.

Processing Fee всегда фиксируется снимком.

Изменение глобальной комиссии не меняет старые попытки.

Deposit Address показывается только после активации.

Payment определяется по адресу назначения и контракту.

Оплата подтверждается только по confirmed state.

Не подтверждённая транзакция не запускает исполнение.

Поздний платёж не исполняется автоматически.

Неправильный токен не является оплатой.

Один blockchain event не обрабатывается дважды.

Один принятый платёж не исполняется дважды.

Sweep failure не отменяет клиентский платёж.

Приватные ключи отсутствуют в основной базе.

Signing Service не может отправлять средства на произвольный адрес.

Все ручные действия сохраняются в audit log.

36. Критерии готовности MVP

Система считается готовой, если:

  1. мерчант создаёт Payment Order;

  2. клиент может выбрать USDT TRC20;

  3. сумма USDT рассчитывается по фиксированному курсу;

  4. основная сумма округляется до двух знаков;

  5. к ней добавляется 2.00 USDT;

  6. итоговая сумма имеет два знака;

  7. создаётся уникальный TRON-адрес;

  8. адрес активируется до публикации;

  9. адрес никогда не переиспользуется;

  10. отображаются адрес, сумма, сеть и QR;

  11. клиент предупреждён о комиссии своей биржи;

  12. система отслеживает официальный контракт USDT;

  13. поддельный токен не засчитывается;

  14. транзакция проходит confirmed-проверку;

  15. точная оплата определяется автоматически;

  16. недоплата определяется;

  17. переплата определяется;

  18. поздняя оплата определяется;

  19. повторная оплата определяется;

  20. Payment Order нельзя исполнить дважды;

  21. подтверждённые USDT собираются в Treasury;

  22. sweep подписывается изолированным Signing Service;

  23. фактический расход TRX и Energy сохраняется;

  24. рассчитывается маржа фиксированного сбора;

  25. выполняется ежедневная reconciliation;

  26. в админке видны адрес, tx hash и sweep;

  27. мерчант получает подписанные webhooks;

  28. все операции имеют audit trail;

  29. ключи не хранятся в основной БД;

  30. production-система протестирована на малых реальных суммах.


37. Этапы реализации

Этап 1. Payment Model

Реализовать:

payment_options
crypto_payment_attempts
crypto_fee_snapshots
новые статусы
формулу principal + 2.00

Этап 2. Key Infrastructure

Реализовать:

wallet_key_versions
Address Allocator
Signing Service
recovery procedure
address generation tests

Этап 3. Address Activation

Реализовать:

Activation Wallet
Activation Service
activation statuses
activation reconciliation

Этап 4. Blockchain Monitoring

Реализовать:

TRON API integration
block cursor
TRC20 Transfer parser
confirmed-state verification
backfill
deduplication

Этап 5. Payment Matching

Реализовать:

exact amount
underpayment
overpayment
late payment
duplicate payment
wrong asset

Этап 6. Sweep

Реализовать:

Resource Wallet
estimateEnergy
resource strategy
Signing Service
Treasury transfer
sweep reconciliation

Этап 7. Admin и Webhooks

Реализовать:

operation card
manual review
webhook delivery
audit history
financial fee report

Этап 8. Production Readiness

Перед запуском:

отдельные production keys
резервные копии
recovery test
лимиты кошельков
алерты
mainnet smoke tests
incident plan
daily reconciliation

38. Итоговая схема

Merchant
   │
   ▼
Payment Order в IDR
   │
   ▼
Публичная платёжная ссылка
   │
   ├── Оплата RUB
   │
   └── Оплата USDT TRC20
             │
             ▼
       Rate Snapshot
             │
             ├── Principal USDT
             ├── + 2.00 USDT
             └── Total USDT, 2 знака
             │
             ▼
      Crypto Payment Attempt
             │
             ▼
      Unique Deposit Address
             │
             ▼
         Activation
             │
             ▼
      Публикация адреса и QR
             │
             ▼
       Клиент отправляет USDT
             │
             ▼
      Blockchain Monitor
             │
             ├── официальный контракт
             ├── правильный адрес
             ├── точная сумма
             ├── block timestamp
             └── confirmed state
             │
             ▼
        Payment confirmed
             │
             ├── execution_status = ready
             └── sweep_status = queued
                         │
                         ▼
                 Estimate Energy
                         │
                         ▼
                 Resource Strategy
                         │
                         ▼
                  Signing Service
                         │
                         ▼
Deposit Address ─── USDT ───► Treasury Wallet

39. Финальный стандарт CARA Pay Crypto MVP

Сеть:
TRON Mainnet

Актив:
USDT TRC20

Контракт:
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t

Срок ссылки:
15 минут

Точность:
2 знака после запятой

Основная сумма:
рассчитывается по зафиксированному курсу

Фиксированный сбор:
2.00 USDT

Итого:
principal + 2.00 USDT

Адрес:
уникальный для каждой Crypto Payment Attempt

Активация:
до публикации клиенту

Переиспользование:
запрещено

Подтверждение:
confirmed / solidified TRON transaction

Поздняя оплата:
manual review

Недоплата:
manual review

Переплата:
manual review

Неверный токен:
не является оплатой

Sweep:
Deposit Address → Treasury Wallet

Private keys:
только в изолированном Signing Service

Основной принцип:
одна криптоплатёжная попытка —
один уникальный адрес —
одна зафиксированная сумма —
одна полная история on-chain.