Перейти к содержанию
Версия: 3.3.51 Обновлено: 2026-01-23

Withdrawal Lifecycle Architecture

Обзор

Полная документация жизненного цикла вывода средств (withdrawal) в Saga DeFi Platform - от запроса пользователя до исполнения транзакции на blockchain.

Lifecycle States

State Machine Диаграмма (Упрощённый Workflow 2025-11)

flowchart TB
    A[User Request] --> B[PENDING]
    B -->|Admin Takes| C[PROCESSING]
    C -->|Complete + TxHash| D[COMPLETED]
    B -->|User Cancels| E[REJECTED]
    B -->|Admin Rejects| E
    C -->|TX Fails| F[FAILED]

    style D fill:#90EE90
    style E fill:#FFB6C1
    style F fill:#FF6B6B

Ключевое отличие: В упрощённом workflow статус approved не используется. Вместо этого:

  • POST /take переводит сразу в processing
  • POST /complete с txHash завершает вывод

Возможные статусы

pending - Создан запрос на вывод - Пользователь отправил withdrawal request - Запись создана в БД withdrawals таблице (через transaction_requests) - Ожидает взятия в обработку администратором - Средства заморожены на счету пользователя - Пользователь МОЖЕТ отменить запрос (статус → rejected)

processing - Взят в обработку администратором (НОВЫЙ в 2025-11) - Администратор нажал "Взять в обработку" (POST /api/admin/withdrawals/:id/take) - Запрос в работе, готовится к исполнению в Fordefi - Пользователь НЕ МОЖЕТ отменить запрос - Администратор может отклонить или завершить

approved - Одобрен администратором (опциональный шаг) - В упрощённом workflow обычно пропускается: pending → processing → completed

completed - Успешно выполнен - Администратор завершил через POST /api/admin/withdrawals/:id/complete с txHash - Blockchain транзакция выполнена в Fordefi - Transaction hash записан в БД - Финальное состояние (успех)

rejected - Отклонен - Пользователь отменил (только из pending) - Или администратор отклонил (из pending или processing) - Средства возвращены пользователю - Финальное состояние (отказ)

failed - Ошибка при исполнении - Blockchain транзакция не прошла - Технические проблемы в Fordefi - Требует ручного вмешательства - Финальное состояние (ошибка)

Detailed Flow

1. User Request Phase

Frontend (User App):

// POST /api/withdrawals
const requestWithdrawal = async (amount: string, address: string) => {
  const response = await fetch('/api/withdrawals', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${jwtToken}`
    },
    body: JSON.stringify({
      amount,
      wallet_address: address,
      currency: 'USDC'
    })
  });

  return response.json(); // { id, status: "pending", ... }
};

Backend Handler:

// backend/shared/routing/withdrawal_handler.go
func (h *WithdrawalHandler) CreateWithdrawal(w http.ResponseWriter, r *http.Request) {
    var req WithdrawalRequest
    json.NewDecoder(r.Body).Decode(&req)

    // Валидация amount через canonical validator
    validationResult := canonical.ValidateWithdrawalAmount(req.Amount, req.Currency)
    if !validationResult.IsValid {
        http_helpers.SendError(w, validationResult.ErrorMessage, http.StatusBadRequest)
        return
    }

    // Создание withdrawal через service
    withdrawal, err := h.service.CreateWithdrawal(ctx, userID, req)
    if err != nil {
        http_helpers.SendError(w, err.Error(), http.StatusInternalServerError)
        return
    }

    http_helpers.SendSuccess(w, withdrawal)
}

Service Layer:

// backend/shared/services/withdrawal_service.go
func (s *WithdrawalService) CreateWithdrawal(ctx context.Context, userID int64, req WithdrawalRequest) (*Withdrawal, error) {
    // 1. Проверка баланса пользователя
    balance, err := s.balanceService.GetUserBalance(ctx, userID)
    if balance.LessThan(req.Amount) {
        return nil, errors.New("insufficient balance")
    }

    // 2. Создание withdrawal записи
    withdrawal := &Withdrawal{
        UserID:        userID,
        Amount:        req.Amount,
        WalletAddress: req.WalletAddress,
        Currency:      req.Currency,
        Status:        "pending",
        CreatedAt:     time.Now().UTC(),
    }

    // 3. Сохранение в БД
    err = s.repo.CreateWithdrawal(ctx, withdrawal)
    if err != nil {
        return nil, err
    }

    // 4. Логирование для мониторинга
    logger.InfoStructured("Withdrawal created",
        "withdrawal_id", withdrawal.ID,
        "user_id", userID,
        "amount", req.Amount.String(),
        "status", "pending",
    )

    return withdrawal, nil
}

Database:

-- withdrawals таблица
INSERT INTO withdrawals (
    user_id, amount, wallet_address, currency, status, created_at
) VALUES (
    $1, $2, $3, $4, 'pending', NOW()
) RETURNING id;

2. Admin Review Phase

Admin App UI:

// GET /api/admin/withdrawals/pending
const pendingWithdrawals = await fetch('/api/admin/withdrawals/pending', {
  headers: {
    'Authorization': `Bearer ${adminJwtToken}`
  }
});

// Displays list of pending withdrawals
// Admin can approve/reject each one

Backend Admin Handler:

// backend/shared/routing/admin_handler.go
func (h *AdminHandler) GetPendingWithdrawals(w http.ResponseWriter, r *http.Request) {
    // Admin auth middleware уже проверил JWT токен

    withdrawals, err := h.service.GetPendingWithdrawals(ctx)
    if err != nil {
        http_helpers.SendError(w, err.Error(), http.StatusInternalServerError)
        return
    }

    http_helpers.SendSuccess(w, withdrawals)
}

func (h *AdminHandler) ApproveWithdrawal(w http.ResponseWriter, r *http.Request) {
    withdrawalID := chi.URLParam(r, "id")

    // Получение admin email из JWT claims
    adminEmail := r.Context().Value("admin_email").(string)

    withdrawal, err := h.service.ApproveWithdrawal(ctx, withdrawalID, adminEmail)
    if err != nil {
        http_helpers.SendError(w, err.Error(), http.StatusInternalServerError)
        return
    }

    http_helpers.SendSuccess(w, withdrawal)
}

Service Approval Logic:

func (s *WithdrawalService) ApproveWithdrawal(ctx context.Context, withdrawalID string, adminEmail string) (*Withdrawal, error) {
    // 1. Получение withdrawal из БД
    withdrawal, err := s.repo.GetWithdrawalByID(ctx, withdrawalID)
    if err != nil {
        return nil, err
    }

    // 2. Проверка текущего статуса
    if withdrawal.Status != "pending" {
        return nil, errors.New("withdrawal is not in pending state")
    }

    // 3. Обновление статуса на approved
    withdrawal.Status = "approved"
    withdrawal.ApprovedBy = adminEmail
    withdrawal.ApprovedAt = time.Now().UTC()

    err = s.repo.UpdateWithdrawal(ctx, withdrawal)
    if err != nil {
        return nil, err
    }

    // 4. Логирование admin действия
    logger.InfoStructured("Withdrawal approved",
        "withdrawal_id", withdrawalID,
        "admin_email", adminEmail,
        "amount", withdrawal.Amount.String(),
        "user_id", withdrawal.UserID,
    )

    // 5. Постановка в очередь на исполнение
    go s.executionService.QueueWithdrawal(withdrawal)

    return withdrawal, nil
}

3. Fordefi MPC Execution Phase

Процесс вывода через Fordefi MPC:

Все выводы средств выполняются вручную администратором через Fordefi MPC custody:

flowchart LR
    A[Admin берёт запрос] --> B[Открывает Fordefi Dashboard]
    B --> C[Создаёт транзакцию]
    C --> D[Multi-sig подтверждение]
    D --> E[Транзакция в блокчейн]
    E --> F[Admin вводит TxHash в систему]
    F --> G[Withdrawal completed]

    style G fill:#90EE90

Admin Workflow:

  1. Взять запрос (POST /api/admin/withdrawals/:id/take)
  2. Статус: pendingprocessing
  3. Запрос заблокирован для других админов

  4. Выполнить в Fordefi Dashboard

  5. Войти в Fordefi MPC custody
  6. Создать транзакцию USDC/USDT
  7. Указать сеть (TRON TRC20 или Ethereum ERC20)
  8. Ввести адрес получателя и сумму
  9. Дождаться multi-sig подтверждения

  10. Завершить запрос (POST /api/admin/withdrawals/:id/complete)

  11. Ввести transaction hash из Fordefi
  12. Статус: processingcompleted

Backend Complete Handler:

// backend/shared/routing/admin_handler.go
func (h *AdminHandler) CompleteWithdrawal(w http.ResponseWriter, r *http.Request) {
    withdrawalID := chi.URLParam(r, "id")
    adminEmail := r.Context().Value("admin_email").(string)

    var req CompleteWithdrawalRequest
    json.NewDecoder(r.Body).Decode(&req)

    // Валидация txHash
    if req.TxHash == "" {
        http_helpers.SendError(w, "txHash is required", http.StatusBadRequest)
        return
    }

    withdrawal, err := h.service.CompleteWithdrawal(ctx, withdrawalID, req.TxHash, adminEmail)
    if err != nil {
        http_helpers.SendError(w, err.Error(), http.StatusInternalServerError)
        return
    }

    http_helpers.SendSuccess(w, withdrawal)
}

Service Complete Logic:

func (s *WithdrawalService) CompleteWithdrawal(ctx context.Context, withdrawalID, txHash, adminEmail string) (*Withdrawal, error) {
    withdrawal, err := s.repo.GetWithdrawalByID(ctx, withdrawalID)
    if err != nil {
        return nil, err
    }

    if withdrawal.Status != "processing" {
        return nil, errors.New("withdrawal must be in processing state")
    }

    // Обновление статуса
    withdrawal.Status = "completed"
    withdrawal.TxHash = txHash
    withdrawal.CompletedBy = adminEmail
    withdrawal.CompletedAt = time.Now().UTC()

    err = s.repo.UpdateWithdrawal(ctx, withdrawal)
    if err != nil {
        return nil, err
    }

    logger.InfoStructured("Withdrawal completed via Fordefi",
        "withdrawal_id", withdrawalID,
        "tx_hash", txHash,
        "admin_email", adminEmail,
        "amount", withdrawal.Amount.String(),
    )

    return withdrawal, nil
}

Security Considerations

1. Authorization Checks

Двухуровневая проверка:

  • User может создавать withdrawal ТОЛЬКО для своего аккаунта
  • Admin может approve/reject withdrawal для любого пользователя
// User endpoint security
func (h *WithdrawalHandler) CreateWithdrawal(w http.ResponseWriter, r *http.Request) {
    // JWT middleware уже проверил токен
    userID := r.Context().Value("user_id").(int64)

    // Пользователь может создать withdrawal только для себя
    withdrawal, err := h.service.CreateWithdrawal(ctx, userID, req)
}

// Admin endpoint security
func (h *AdminHandler) ApproveWithdrawal(w http.ResponseWriter, r *http.Request) {
    // Admin JWT middleware проверил admin права
    adminEmail := r.Context().Value("admin_email").(string)

    // Admin может approve любой withdrawal
    withdrawal, err := h.service.ApproveWithdrawal(ctx, withdrawalID, adminEmail)
}

2. Financial Safety

SafeDecimal для точности:

// Все финансовые операции используют SafeDecimal
type Withdrawal struct {
    Amount SafeDecimal `json:"amount"` // НЕ float64 или decimal.Decimal!
}

// Валидация лимитов
validationResult := canonical.ValidateWithdrawalAmount(amount, currency)
if !validationResult.IsValid {
    return errors.New(validationResult.ErrorMessage)
}

Database transactions для атомарности:

tx, err := db.BeginTx(ctx, nil)
defer tx.Rollback()

// 1. Создание withdrawal записи
// 2. Обновление user balance
// 3. Создание transaction записи

tx.Commit() // Атомарный commit всех изменений

3. Fordefi MPC Security

Multi-Signature Protection:

  • Все транзакции требуют multi-sig подтверждения в Fordefi
  • Ни один администратор не может выполнить вывод единолично
  • Enterprise-grade custody standards

Admin Audit Trail:

// Все действия админов логируются
logger.InfoStructured("Withdrawal action",
    "withdrawal_id", withdrawalID,
    "action", "complete",
    "admin_email", adminEmail,
    "tx_hash", txHash,
    "timestamp", time.Now().UTC(),
)

Transaction Hash Verification:

  • Admin вводит txHash из Fordefi после выполнения транзакции
  • Hash сохраняется в БД для audit trail
  • Пользователь может проверить транзакцию в blockchain explorer

Database Schema

CREATE TABLE withdrawals (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    amount NUMERIC(78,0) NOT NULL, -- SafeDecimal compatible
    wallet_address VARCHAR(42) NOT NULL,
    currency VARCHAR(10) NOT NULL DEFAULT 'USDC',
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    tx_hash VARCHAR(66), -- Blockchain transaction hash
    approved_by VARCHAR(255), -- Admin email
    approved_at TIMESTAMP,
    completed_at TIMESTAMP,
    failed_at TIMESTAMP,
    error_message TEXT,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Индексы для производительности
CREATE INDEX idx_withdrawals_user_id ON withdrawals(user_id);
CREATE INDEX idx_withdrawals_status ON withdrawals(status);
CREATE INDEX idx_withdrawals_created_at ON withdrawals(created_at DESC);

Testing

Integration Test Example

// backend/tests/integration/withdrawal_lifecycle_test.go
func TestWithdrawalLifecycle(t *testing.T) {
    // Setup
    server := setupTestServer()
    defer server.Close()

    // 1. User creates withdrawal request
    withdrawalReq := WithdrawalRequest{
        Amount:        SafeDecimalFromString("100.00"),
        WalletAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
        Currency:      "USDC",
    }

    resp := makeAuthRequest(server, "POST", "/api/withdrawals", withdrawalReq, userToken)
    assert.Equal(t, 200, resp.StatusCode)

    var withdrawal Withdrawal
    json.Unmarshal(resp.Body, &withdrawal)
    assert.Equal(t, "pending", withdrawal.Status)

    // 2. Admin approves withdrawal
    approveResp := makeAuthRequest(server, "POST",
        fmt.Sprintf("/api/admin/withdrawals/%d/approve", withdrawal.ID),
        nil, adminToken)
    assert.Equal(t, 200, approveResp.StatusCode)

    // 3. Wait for blockchain execution
    time.Sleep(5 * time.Second)

    // 4. Check final status
    getResp := makeAuthRequest(server, "GET",
        fmt.Sprintf("/api/withdrawals/%d", withdrawal.ID),
        nil, userToken)

    var finalWithdrawal Withdrawal
    json.Unmarshal(getResp.Body, &finalWithdrawal)
    assert.Equal(t, "completed", finalWithdrawal.Status)
    assert.NotEmpty(t, finalWithdrawal.TxHash)
}

API Endpoints Reference

User Endpoints

  • POST /api/withdrawals - Create withdrawal request
  • GET /api/withdrawals - List user's withdrawals
  • GET /api/withdrawals/{id} - Get withdrawal details
  • POST /api/withdrawals/{id}/cancel - Cancel pending withdrawal (only from pending status)

Admin Endpoints (Упрощённый Workflow 2025-11)

  • GET /api/admin/withdrawals - List all withdrawals
  • GET /api/admin/withdrawals/pending - List pending withdrawals
  • GET /api/admin/withdrawals/{id} - Get withdrawal details
  • POST /api/admin/withdrawals/{id}/take - Take withdrawal for processing (pending → processing)
  • POST /api/admin/withdrawals/bulk-take - Bulk take multiple withdrawals
  • POST /api/admin/withdrawals/{id}/complete - Complete with txHash (processing → completed)
  • POST /api/admin/withdrawals/{id}/reject - Reject withdrawal

Additional Endpoints

  • POST /api/admin/withdrawals/{id}/approve - Approve withdrawal (optional step)