Перейти к содержанию

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