Ledger Service
Назначение
Ledger — сервис учёта и хранения балансов платформы Univex. Отвечает за учёт всех денежных операций: балансовые операции, фиатные операции, внутренние переводы, заморозку активов, выводы и funded codes (предоплаченные коды пополнения). Контроль целостности данных обеспечивается через хеширование операций.
Транспорт: gRPC. Количество RPC: 26. Асинхронные операции: Temporal.
Архитектура
┌──────────────────────────────────────────────────┐
│ gRPC Server │
│ 26 RPC методов │
└────────────────────┬─────────────────────────────┘
│
┌────────────────────▼─────────────────────────────┐
│ Domain Layer │
│ Бизнес-логика финансовых операций │
└────────┬───────────────────────┬─────────────────┘
│ │
┌────────▼────────┐ ┌───────────▼──────────┐
│ Storage Layer │ │ Temporal Workflows │
│ PostgreSQL │ │ (async processing) │
└─────────────────┘ └──────────────────────┘
База данных
Миграции (11)
| # |
Таблица / Изменение |
Назначение |
| 1 |
operations |
Базовая таблица финансовых операций |
| 2 |
balance_operations |
Балансовые операции (пополнение/вывод) |
| 3 |
fiat_operations |
Фиатные операции |
| 4 |
frozen_assets |
Замороженные активы |
| 5 |
funded_codes |
Funded codes (предоплаченные коды) |
| 6 |
Индексы |
Производительность запросов |
| 7 |
intra_exchange_transfers |
Внутренние переводы между пользователями |
| 8 |
withdrawals |
Записи о выводах |
| 9 |
withdrawal_status |
Статусы выводов |
| 10 |
withdrawal_events |
История событий выводов |
| 11 |
Дополнительные индексы |
Производительность запросов |
Схема ключевых таблиц
erDiagram
balance_operations {
UUID id PK
UUID account_id
TEXT asset
NUMERIC amount
TEXT type
TEXT status
TIMESTAMP created_at
}
fiat_operations {
UUID id PK
UUID account_id
NUMERIC amount
TEXT currency
TEXT type
TEXT status
TEXT bid_code
TIMESTAMP created_at
}
frozen_assets {
UUID id PK
UUID account_id
TEXT asset
NUMERIC amount
TEXT reason
TIMESTAMP frozen_at
TIMESTAMP unfrozen_at
}
funded_codes {
UUID id PK
TEXT code
UUID created_by
TEXT asset
NUMERIC amount
TEXT status
TIMESTAMP created_at
TIMESTAMP redeemed_at
}
intra_exchange_transfers {
UUID id PK
UUID from_account_id
UUID to_account_id
TEXT asset
NUMERIC amount
TEXT status
TIMESTAMP created_at
}
withdrawals {
UUID id PK
UUID account_id
TEXT asset
NUMERIC amount
TEXT address
TEXT network
TEXT status
TIMESTAMP created_at
}
gRPC-контракт
Прото-файл: proto/ledger.proto
Полный список RPC (26)
service Ledger {
// Балансовые операции (3)
rpc CreateBalanceOperation(CreateBalanceOperationRequest) returns (CreateBalanceOperationResponse);
rpc GetBalanceOperation(GetBalanceOperationRequest) returns (GetBalanceOperationResponse);
rpc ListBalanceOperations(ListBalanceOperationsRequest) returns (ListBalanceOperationsResponse);
// Фиат (7)
rpc CreateFiatOperation(CreateFiatOperationRequest) returns (CreateFiatOperationResponse);
rpc ListFiatOperations(ListFiatOperationsRequest) returns (ListFiatOperationsResponse);
rpc ListFiatBids(ListFiatBidsRequest) returns (ListFiatBidsResponse);
rpc ProcessFiatBid(ProcessFiatBidRequest) returns (ProcessFiatBidResponse);
rpc CompleteFiatBid(CompleteFiatBidRequest) returns (CompleteFiatBidResponse);
rpc FundsIssuedFiatBid(FundsIssuedFiatBidRequest) returns (FundsIssuedFiatBidResponse);
rpc GetFiatBidByCode(GetFiatBidByCodeRequest) returns (GetFiatBidByCodeResponse);
// Баланс USDT (1)
rpc GetBalanceUSDT(GetBalanceUSDTRequest) returns (GetBalanceUSDTResponse);
// Переводы (3)
rpc StartTransfer(StartTransferRequest) returns (StartTransferResponse);
rpc GetTransferStatus(GetTransferStatusRequest) returns (GetTransferStatusResponse);
rpc ListTransfers(ListTransfersRequest) returns (ListTransfersResponse);
// История (1)
rpc ListHistory(ListHistoryRequest) returns (ListHistoryResponse);
// Замороженные активы (4)
rpc FreezeMoney(FreezeMoneyRequest) returns (FreezeMoneyResponse);
rpc UnfreezeMoney(UnfreezeMoneyRequest) returns (UnfreezeMoneyResponse);
rpc SpendFrozenAssets(SpendFrozenAssetsRequest) returns (SpendFrozenAssetsResponse);
rpc ListFrozenAssets(ListFrozenAssetsRequest) returns (ListFrozenAssetsResponse);
// Funded Codes (4)
rpc CreateFundedCode(CreateFundedCodeRequest) returns (CreateFundedCodeResponse);
rpc CancelFundedCode(CancelFundedCodeRequest) returns (CancelFundedCodeResponse);
rpc RedeemFundedCode(RedeemFundedCodeRequest) returns (RedeemFundedCodeResponse);
rpc ListFundedCodes(ListFundedCodesRequest) returns (ListFundedCodesResponse);
// Выводы (2)
rpc ListWithdrawals(ListWithdrawalsRequest) returns (ListWithdrawalsResponse);
rpc GetWithdrawal(GetWithdrawalRequest) returns (GetWithdrawalResponse);
}
Группы RPC
Балансовые операции
| RPC |
Описание |
CreateBalanceOperation |
Создаёт балансовую операцию (пополнение или вывод с торгового баланса). |
GetBalanceOperation |
Возвращает данные балансовой операции по ID. |
ListBalanceOperations |
Постраничный список балансовых операций с фильтрацией. |
Фиатные операции
| RPC |
Описание |
CreateFiatOperation |
Создаёт заявку на фиатный депозит или вывод. |
ListFiatOperations |
История фиатных операций аккаунта. |
ListFiatBids |
Список фиатных заявок для кассира (все аккаунты). |
ProcessFiatBid |
Кассир принимает фиатную заявку в обработку. |
CompleteFiatBid |
Кассир завершает фиатную заявку (деньги приняты/выданы). |
FundsIssuedFiatBid |
Подтверждение факта выдачи средств. |
GetFiatBidByCode |
Поиск фиатной заявки по уникальному коду. |
Баланс USDT
| RPC |
Описание |
GetBalanceUSDT |
Возвращает суммарный баланс аккаунта в USDT (по всем активам с конвертацией). |
Внутренние переводы
| RPC |
Описание |
StartTransfer |
Инициирует внутренний перевод между пользователями. Запускает Temporal workflow. |
GetTransferStatus |
Возвращает текущий статус перевода. |
ListTransfers |
История переводов аккаунта. |
История операций
| RPC |
Описание |
ListHistory |
Сводная история всех операций аккаунта (балансовые, фиатные, переводы, выводы) с пагинацией и фильтрацией по типу и периоду. |
Замороженные активы
| RPC |
Описание |
FreezeMoney |
Замораживает указанную сумму актива аккаунта. |
UnfreezeMoney |
Размораживает замороженные активы. |
SpendFrozenAssets |
Списывает замороженные активы (используется при завершении P2P-ордера). |
ListFrozenAssets |
Список замороженных активов аккаунта. |
Funded Codes
| RPC |
Описание |
CreateFundedCode |
Создаёт предоплаченный код пополнения для аккаунта. |
CancelFundedCode |
Аннулирует funded code. |
RedeemFundedCode |
Применяет funded code: зачисляет средства на баланс. |
ListFundedCodes |
Список funded codes с фильтрацией по статусу и аккаунту. |
Выводы
| RPC |
Описание |
ListWithdrawals |
Список всех выводов с фильтрацией. |
GetWithdrawal |
Данные конкретного вывода. |
Перечисления
BalanceOperationType
enum BalanceOperationType {
BALANCE_OPERATION_TYPE_UNSPECIFIED = 0;
TOPUP = 1; // Пополнение
WITHDRAW = 2; // Вывод
}
BalanceOperationStatus
enum BalanceOperationStatus {
BALANCE_OPERATION_STATUS_UNSPECIFIED = 0;
PENDING = 1; // В обработке
SUCCESS = 2; // Успешно
FAILED = 3; // Ошибка
}
FiatOperationType
enum FiatOperationType {
FIAT_OPERATION_TYPE_UNSPECIFIED = 0;
FIAT_TOPUP = 1; // Фиатное пополнение
FIAT_WITHDRAWAL = 2; // Фиатный вывод
}
FiatOperationStatus
enum FiatOperationStatus {
FIAT_OPERATION_STATUS_UNSPECIFIED = 0;
CREATED = 1; // Создана
PENDING_CASHIER = 2; // Ожидает кассира
PROCESSING = 3; // В обработке
SUCCESS = 4; // Завершена
REJECTED = 5; // Отклонена
}
TransferStatus
enum TransferStatus {
TRANSFER_STATUS_UNSPECIFIED = 0;
TRANSFER_PENDING = 1; // В обработке
TRANSFER_COMPLETED = 2; // Завершён
TRANSFER_FAILED = 3; // Ошибка
}
FundedCodeStatus
enum FundedCodeStatus {
FUNDED_CODE_STATUS_UNSPECIFIED = 0;
ACTIVE = 1; // Активен, не использован
REDEEMED = 2; // Применён
CANCELLED = 3; // Аннулирован
}
Confirmation — тип подтверждения операции
enum Confirmation {
CONFIRMATION_UNSPECIFIED = 0;
EMAIL = 1;
PHONE = 2;
GOOGLE = 3;
SET_PASSWORD = 4;
SET_PHONE = 5;
SET_EMAIL = 6;
ADMIN = 7;
}
Temporal Workflows
Ledger использует Temporal для надёжного выполнения многошаговых асинхронных операций.
| Workflow |
Описание |
TransferWorkflow |
Надёжный внутренний перевод: списание у отправителя, зачисление у получателя, с компенсацией при сбое. |
FiatDepositWorkflow |
Асинхронная обработка фиатного депозита через кассира. |
FiatWithdrawWorkflow |
Асинхронная обработка фиатного вывода через кассира. |
Конфигурация
| Параметр |
Описание |
DATABASE_URL |
DSN для подключения к PostgreSQL |
GRPC_PORT |
Порт gRPC-сервера |
TEMPORAL_HOST |
Адрес Temporal-сервера |
TEMPORAL_NAMESPACE |
Namespace в Temporal |
EXCHANGE_INTEGRATION_GRPC_ADDR |
Адрес Exchange Integration gRPC (для получения курсов активов) |
HASHER_SECRET |
Секретный ключ для хеширования финансовых записей |
Контроль целостности данных
Ledger реализует механизм защиты от несанкционированных изменений в БД через хеширование всех финансовых записей. Каждая запись при сохранении получает криптографический хеш, который пересчитывается и проверяется при каждом чтении.
Алгоритм
- Алгоритм: SHA-256
- Формат вывода: 64 hex-символа
- Секретный ключ: настраивается на уровне экземпляра
Hasher через конфигурацию сервиса
Хешируемые сущности (6 типов)
| Сущность |
Формат хеша |
Поля |
| Operations |
{userID}_{type}_{timestamp}_{secret} |
ID пользователя, тип операции, время создания |
| Balance Operations |
{userID}_{amount}_{type}_{timestamp}_{secret} |
+ нормализованная сумма (8 знаков после запятой) |
| Fiat Operations |
{accountID}_{amount}_{type}_{timestamp}_{secret} |
+ сумма (2 знака, DECIMAL(20,2)) |
| Frozen Assets |
{accountID}_{asset}_{amount}_{accountType}_{operationID}_{timestamp}_{secret} |
+ актив, тип аккаунта, ID операции |
| Funded Codes |
{userID}_{code}_{asset}_{amount}_{freezeOpID}_{timestamp}_{secret} |
+ код, ID заморозки |
| Intra-Exchange Transfers |
{fromID}_{toID}_{amount}_{asset}_{externalID}_{timestamp}_{secret} |
+ отправитель, получатель, внешний ID |
Когда создаётся хеш
При INSERT — хеш вычисляется непосредственно перед записью в БД и сохраняется в колонку hash VARCHAR(64) NOT NULL. Приложение не доверяет БД вычисление хеша: он формируется на стороне сервиса до передачи данных в PostgreSQL.
Когда проверяется хеш
При каждом SELECT — после чтения записи из БД хеш пересчитывается по тем же полям и сравнивается с хранимым значением.
- При совпадении: запись возвращается приложению в штатном режиме.
- При несовпадении: возвращается ошибка
ErrInvalidHash: "data integrity check failed".
Для списков: валидация выполняется для каждой записи. Если хотя бы одна запись не прошла проверку — вся операция чтения отклоняется.
Обратная совместимость
Реализована dual validation: при проверке сначала применяется новый формат (с нормализованными decimals), затем legacy-формат (до нормализации). Это обеспечивает корректную работу с историческими данными, созданными до внедрения нормализации, без необходимости миграции существующих хешей.
Что происходит при несовпадении
- Запись не возвращается приложению
- Возвращается ошибка вида:
[operation_type] data integrity check failed: invalid hash - data integrity compromised
- Любые операции с этой записью (обновление, вывод, перевод) невозможны до устранения причины несовпадения
- Инцидент должен быть расследован: несовпадение хеша означает, что данные были изменены в обход приложения
Назначение
Механизм предназначен для обнаружения несанкционированных изменений в БД. Секретный ключ хранится исключительно в конфигурации сервиса и недоступен на уровне БД, поэтому прямое редактирование записей через PostgreSQL (psql, pgAdmin, SQL-скрипты) будет обнаружено при следующем чтении из приложения.
Запись в БД:
amount = 1000.00
hash = "a3f8c2..." ← вычислен с секретным ключом
Злоумышленник меняет через psql:
UPDATE balance_operations SET amount = 9999.99 WHERE id = '...';
При следующем чтении:
пересчитанный хеш = "d94e71..." ≠ "a3f8c2..."
→ ErrInvalidHash: data integrity compromised