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

Withdrawals Flow - Критический путь выводов средств

Дата обновления: 2025-11-14 Архитектура: Integration-Only (НИКАКИХ WALLET ОПЕРАЦИЙ)

Архитектурные принципы

  • ✅ ОБЯЗАТЕЛЬНО: Fordefi API для всех withdrawal операций
  • ✅ ОБЯЗАТЕЛЬНО: Admin approval через Fordefi Dashboard
  • ✅ ОБЯЗАТЕЛЬНО: Balance validation перед выводом
  • ✅ ОБЯЗАТЕЛЬНО: Immutable transaction records
  • ❌ ЗАПРЕТ: HD Wallet, приватные ключи, собственные blockchain операции
  • ✅ ОБЯЗАТЕЛЬНО: Многоступенчатый процесс: Request → Admin → Fordefi → Completion

Компоненты системы

Backend Stack

Компонент Файл Роль
Admin Service /backend/shared/services/withdrawal_admin_service.go Admin approval логика
Fordefi Service /backend/integrations/fordefi/service.go Fordefi API интеграция
Webhook Handler /backend/integrations/fordefi/webhook_handler.go Fordefi webhook обработка
Router user_router.go + admin endpoints HTTP API
Repository /backend/shared/repository/fordefi_repo.go Fordefi транзакции

Frontend Stack

Компонент Файл Роль
Service /frontend/user-app/src/lib/services/withdrawal-service.ts API интеграция
Component /frontend/user-app/src/components/withdrawals/WithdrawForm.tsx UI форма
Types Withdrawal, CreateWithdrawalParams, FordefiTransaction TypeScript типы

External Infrastructure

  • Fordefi API: Enterprise custody и withdrawal execution
  • Multi-sig security: Institutional-grade безопасность
  • Admin Dashboard: Fordefi web interface для approvals
  • Compliance: Встроенные AML/KYC checks
  • Webhook notifications: Real-time status updates

Workflow States (Integration-Only)

  • requestedadmin_reviewfordefi_pendingfordefi_approvedcompleted
  • requestedadmin_reviewrejected (admin decision)
  • fordefi_pendingfordefi_rejected (compliance/security)
  • fordefi_approvedfailed (blockchain error)

Детальный Flow

1. Создание запроса на вывод (Integration-Only)

sequenceDiagram
    participant U as User
    participant F as Frontend
    participant B as Backend
    participant DB as Database
    participant N as NotificationService

    U->>F: Заполняет WithdrawForm
    Note right of U: amount, toAddress, currency

    F->>F: Валидация формы (amount, address format)
    F->>B: GET /api/user/balance
    B-->>F: Current balance

    alt Достаточно средств
        F->>B: POST /api/user/withdrawals
        Note right of F: {amount, toAddress, currency, note}

        B->>B: Валидация business rules
        Note right of B: Min/max limits, supported currencies
        B->>DB: INSERT withdrawal_requests
        Note right of DB: status='requested', no signatures required

        B->>N: Email уведомление админу
        Note right of N: Новый withdrawal request требует review

        DB-->>B: Withdrawal request created
        B-->>F: {id, status: 'requested', estimated_completion}

        F->>F: Показать "Запрос создан, ожидает admin approval"

    else Недостаточно средств
        F->>F: Показать ошибку "Insufficient balance"
    end

2. Admin review и Fordefi submission

sequenceDiagram
    participant A as Admin
    participant AF as Admin Frontend
    participant B as Backend
    participant FD as Fordefi API
    participant DB as Database
    participant N as NotificationService

    A->>AF: Открывает Admin Panel
    AF->>B: GET /api/admin/withdrawals?status=requested
    B->>DB: SELECT * FROM withdrawal_requests WHERE status='requested'
    DB-->>B: Pending withdrawals
    B-->>AF: List of pending withdrawals

    loop Для каждого withdrawal
        A->>A: Анализирует запрос (amount, user, history, compliance)
        alt Admin Approved
            A->>AF: Клик "Submit to Fordefi"
            AF->>B: POST /api/admin/withdrawals/{id}/submit-to-fordefi

            B->>FD: POST /transactions/transfer
            Note right of FD: {amount, destination_address, currency, vault_id}
            FD-->>B: {transaction_id, status: 'pending_approval'}

            B->>DB: UPDATE withdrawal_requests SET status='fordefi_pending', fordefi_tx_id
            DB-->>B: Updated

            B->>N: Уведомить пользователя "Submitted to custody approval"
            N-->>B: Notification sent

        else Admin Rejected
            A->>AF: Клик "Reject" + причина
            AF->>B: POST /api/admin/withdrawals/{id}/reject
            B->>DB: UPDATE withdrawal_requests SET status='rejected'

            B->>N: Уведомить пользователя о rejection с причиной
            N-->>B: Notification sent
        end
    end

3. Fordefi approval и execution (Integration-Only)

sequenceDiagram
    participant FA as Fordefi Admin
    participant FD as Fordefi Dashboard
    participant FApi as Fordefi API
    participant B as Backend
    participant DB as Database
    participant U as User
    participant N as NotificationService

    Note over FA,N: Fordefi multi-sig approval process

    FA->>FD: Открывает Fordefi Dashboard
    FD->>FD: Показывает pending transactions from Saga
    FA->>FA: Review transaction (compliance, limits, destination)

    alt Fordefi Approved
        FA->>FD: Approve transaction (multi-sig)
        FD->>FApi: Execute blockchain transaction
        Note right of FApi: Enterprise custody execution

        FApi->>B: Webhook: POST /api/webhooks/fordefi
        Note right of FApi: {"event": "transaction_approved", "tx_id": "...", "blockchain_hash": "..."}

        B->>B: Verify Fordefi webhook signature
        B->>DB: UPDATE withdrawal_requests SET status='fordefi_approved', blockchain_tx=hash
        B->>DB: INSERT transactions (type='withdrawal')
        B->>DB: UPDATE user_balances (subtract amount)

        B->>N: Email пользователю "Withdrawal approved and sent"
        N-->>U: "Your withdrawal is being processed on blockchain"

    else Fordefi Rejected
        FA->>FD: Reject transaction (compliance/security)
        FD->>FApi: Cancel transaction

        FApi->>B: Webhook: POST /api/webhooks/fordefi
        Note right of FApi: {"event": "transaction_rejected", "reason": "..."}

        B->>DB: UPDATE withdrawal_requests SET status='fordefi_rejected'
        B->>N: Email пользователю "Withdrawal rejected by custody provider"
    end

    Note over B,N: Automatic blockchain confirmation handling

    alt Transaction Confirmed on Blockchain
        FApi->>B: Webhook: POST /api/webhooks/fordefi
        Note right of FApi: {"event": "transaction_confirmed", "confirmations": 12}

        B->>DB: UPDATE withdrawal_requests SET status='completed'
        B->>DB: UPDATE transactions SET status='confirmed'
        B->>N: Email "Withdrawal completed successfully"

    else Transaction Failed
        FApi->>B: Webhook: POST /api/webhooks/fordefi
        Note right of FApi: {"event": "transaction_failed", "error": "..."}

        B->>DB: UPDATE withdrawal_requests SET status='failed'
        B->>DB: UPDATE user_balances (refund amount)
        B->>N: Email "Withdrawal failed - funds refunded"
    end

Security Architecture (Integration-Only)

Enterprise Security через Fordefi

1. JWT Authentication: - Все withdrawal requests проходят через Supabase Auth JWT validation - Никаких wallet signatures - только email/OAuth authentication - Backend валидирует JWT токены для авторизации

2. Fordefi Multi-sig Protection: - Все криптографические операции выполняет Fordefi enterprise custody - Multi-signature approval процесс институционального уровня - Compliance и AML/KYC checks встроены в Fordefi workflow

3. API-Based Security: - HTTPS-only коммуникация с Fordefi API - Webhook signature verification для входящих уведомлений - Rate limiting и request validation на application уровне

Balance Validation

Atomic Balance Checks:

-- Проверка баланса с блокировкой строки
BEGIN TRANSACTION;

SELECT balance FROM user_balances 
WHERE user_id = $1 
FOR UPDATE; -- Row-level lock

-- Если balance >= withdrawal_amount
UPDATE user_balances 
SET balance = balance - $2,
    total_withdrawals = total_withdrawals + $2
WHERE user_id = $1 AND balance >= $2;

-- Если UPDATE затронул 0 строк = недостаточно средств
COMMIT;

Admin Approval Rules

Автоматические лимиты:

# config.yaml
WITHDRAWAL_LIMITS:
  AUTO_APPROVE_THRESHOLD: 100.0  # USDC
  DAILY_LIMIT_PER_USER: 1000.0
  ADMIN_REQUIRED_ABOVE: 500.0

ADMIN_APPROVAL:
  REQUIRED_FOR:
    - amount > AUTO_APPROVE_THRESHOLD
    - user_total_today > DAILY_LIMIT_PER_USER  
    - suspicious_pattern_detected
    - new_user_first_withdrawal

База данных схема

Withdrawal Requests Table (Integration-Only)

CREATE TABLE withdrawal_requests (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id),

    -- Request данные
    amount DECIMAL(18,8) NOT NULL CHECK (amount > 0),
    token VARCHAR(10) NOT NULL DEFAULT 'USDC',
    to_address VARCHAR(42) NOT NULL,
    note TEXT, -- Опциональное описание от пользователя

    -- Integration-Only workflow states
    status VARCHAR(30) NOT NULL DEFAULT 'requested',
    -- 'requested', 'admin_review', 'fordefi_pending', 'fordefi_approved', 'fordefi_rejected', 'completed', 'failed'

    -- Admin данные
    admin_id UUID REFERENCES users(id),
    admin_action_at TIMESTAMP,
    rejection_reason TEXT,

    -- Fordefi integration данные
    fordefi_tx_id VARCHAR(100), -- Fordefi transaction ID
    blockchain_tx_hash VARCHAR(66), -- Blockchain hash after execution

    -- Audit
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    processed_at TIMESTAMP,

    CONSTRAINT valid_status CHECK (
        status IN ('requested', 'admin_review', 'fordefi_pending', 'fordefi_approved', 'fordefi_rejected', 'completed', 'failed')
    ),
    CONSTRAINT valid_ethereum_address CHECK (
        to_address ~ '^0x[a-fA-F0-9]{40}$'
    )
);

-- Индексы для эффективности
CREATE INDEX idx_withdrawal_requests_user_status ON withdrawal_requests(user_id, status);
CREATE INDEX idx_withdrawal_requests_status_created ON withdrawal_requests(status, created_at);
CREATE INDEX idx_withdrawal_requests_admin_pending ON withdrawal_requests(admin_id) WHERE status = 'pending';

Daily Limits Tracking

CREATE TABLE daily_withdrawal_limits (
    user_id UUID PRIMARY KEY REFERENCES users(id),
    date DATE NOT NULL DEFAULT CURRENT_DATE,
    total_withdrawn DECIMAL(18,8) DEFAULT 0,
    withdrawal_count INTEGER DEFAULT 0,

    updated_at TIMESTAMP DEFAULT NOW(),

    CONSTRAINT unique_user_date UNIQUE(user_id, date)
);

-- Automatic cleanup старых записей
CREATE OR REPLACE FUNCTION cleanup_old_limits()
RETURNS void AS $$
BEGIN
    DELETE FROM daily_withdrawal_limits 
    WHERE date < CURRENT_DATE - INTERVAL '30 days';
END;
$$ LANGUAGE plpgsql;

Risk Management система

Suspicious Pattern Detection

type RiskAnalyzer struct {
    rules []RiskRule
}

func (r *RiskAnalyzer) AnalyzeWithdrawal(req WithdrawalRequest) RiskLevel {
    riskFactors := []RiskFactor{}

    // 1. Frequency analysis
    if getUserWithdrawalCount(req.UserID, 24*time.Hour) > 5 {
        riskFactors = append(riskFactors, HighFrequencyWithdrawals)
    }

    // 2. Amount analysis  
    if req.Amount > getUserAverageWithdrawal(req.UserID) * 5 {
        riskFactors = append(riskFactors, UnusuallyLargeAmount)
    }

    // 3. Address analysis
    if !isKnownAddress(req.ToAddress) && isExchangeAddress(req.ToAddress) {
        riskFactors = append(riskFactors, UnknownExchangeAddress)
    }

    // 4. Time pattern analysis
    if isUnusualTime(req.CreatedAt, req.UserID) {
        riskFactors = append(riskFactors, UnusualTiming)
    }

    return calculateRiskLevel(riskFactors)
}

Admin Dashboard для Risk Management

interface WithdrawalForReview {
    id: string;
    user: {
        id: string;
        walletAddress: string;
        totalDeposited: number;
        accountAge: number;
    };
    amount: number;
    toAddress: string;
    riskLevel: 'low' | 'medium' | 'high' | 'critical';
    riskFactors: string[];
    similarWithdrawals: WithdrawalHistory[];

    createdAt: Date;
    timeUntilAutoReject: number; // milliseconds
}

// Admin может видеть весь контекст для принятия решения

Error Handling и Edge Cases

Blockchain Failures

Transaction Failed на blockchain:

func handleFailedWithdrawal(withdrawalID uuid.UUID, reason string) error {
    tx := db.Begin()

    // 1. Отметить withdrawal как failed
    if err := tx.Model(&WithdrawalRequest{}).
        Where("id = ?", withdrawalID).
        Updates(map[string]interface{}{
            "status": "failed", 
            "failure_reason": reason,
            "failed_at": time.Now(),
        }).Error; err != nil {
        tx.Rollback()
        return err
    }

    // 2. Вернуть средства пользователю
    var req WithdrawalRequest
    tx.First(&req, withdrawalID)

    if err := tx.Model(&UserBalance{}).
        Where("user_id = ?", req.UserID).
        Update("balance", gorm.Expr("balance + ?", req.Amount)).Error; err != nil {
        tx.Rollback()
        return err
    }

    // 3. Создать refund transaction record
    refund := Transaction{
        UserID: req.UserID,
        Type: "refund",
        Amount: req.Amount,
        Status: "confirmed", 
        RelatedWithdrawalID: &withdrawalID,
    }

    if err := tx.Create(&refund).Error; err != nil {
        tx.Rollback()
        return err
    }

    return tx.Commit().Error
}

Insufficient Gas/Network Issues

// Retry логика для network issues
func retryBlockchainTransaction(withdrawal WithdrawalRequest) error {
    maxRetries := 3
    baseDelay := 5 * time.Second

    for attempt := 0; attempt < maxRetries; attempt++ {
        tx, err := executeOnChain(withdrawal)
        if err == nil {
            return updateWithdrawalSuccess(withdrawal.ID, tx.Hash)
        }

        if isNetworkError(err) {
            delay := baseDelay * time.Duration(math.Pow(2, float64(attempt)))
            time.Sleep(delay)
            continue
        }

        // Permanent error - don't retry
        return handleFailedWithdrawal(withdrawal.ID, err.Error())
    }

    return handleFailedWithdrawal(withdrawal.ID, "max retries exceeded")
}

Frontend User Experience (Integration-Only)

WithdrawForm Component

export function WithdrawForm() {
    const [amount, setAmount] = useState('');
    const [toAddress, setToAddress] = useState('');
    const [note, setNote] = useState('');
    const [isLoading, setIsLoading] = useState(false);
    const { balance } = useBalance();
    const { user } = useSupabaseAuth(); // JWT через Supabase Auth

    const handleSubmit = async (e: FormEvent) => {
        e.preventDefault();
        setIsLoading(true);

        try {
            // 1. Валидация
            if (parseFloat(amount) > balance) {
                throw new Error('Insufficient balance');
            }

            if (!isValidEthereumAddress(toAddress)) {
                throw new Error('Invalid Ethereum address');
            }

            // 2. Простой API вызов - БЕЗ wallet signatures
            const response = await withdrawalService.createWithdrawal({
                amount: parseFloat(amount),
                toAddress,
                note, // Опциональное описание
                currency: 'USDC'
            });

            // 3. UI feedback - показать workflow статус
            toast.success('Withdrawal request created! Awaiting admin approval.');
            router.push(`/withdrawals/${response.id}`);

        } catch (error) {
            toast.error(error.message);
        } finally {
            setIsLoading(false);
        }
    };

    return (
        <form onSubmit={handleSubmit} className="withdrawal-form">
            <div className="form-section">
                <AmountInput
                    label="Amount (USDC)"
                    value={amount}
                    onChange={setAmount}
                    maxValue={balance}
                    disabled={isLoading}
                />
                <div className="balance-info">
                    Available: {balance} USDC
                </div>
            </div>

            <AddressInput
                label="Destination Address"
                value={toAddress}
                onChange={setToAddress}
                placeholder="0x..."
                validate={isValidEthereumAddress}
                disabled={isLoading}
            />

            <TextArea
                label="Note (optional)"
                value={note}
                onChange={setNote}
                placeholder="Purpose of this withdrawal..."
                disabled={isLoading}
            />

            <SubmitButton loading={isLoading}>
                Request Withdrawal
            </SubmitButton>

            <div className="info-panel">
                <h4>Withdrawal Process:</h4>
                <ul>
                    <li> Request submitted</li>
                    <li>🔄 Admin review (1-2 hours)</li>
                    <li>🔄 Enterprise custody approval</li>
                    <li>🔄 Blockchain execution</li>
                    <li> Completed</li>
                </ul>
            </div>
        </form>
    );
}

Real-time Status Updates

export function WithdrawalStatus({ withdrawalId }: { withdrawalId: string }) {
    const [withdrawal, setWithdrawal] = useState<Withdrawal | null>(null);

    useEffect(() => {
        // 1. Initial load
        transactionService.getWithdrawal(withdrawalId)
            .then(setWithdrawal);

        // 2. WebSocket подписка на обновления
        const ws = new WebSocket(`/api/withdrawals/${withdrawalId}/stream`);

        ws.onmessage = (event) => {
            const updatedWithdrawal = JSON.parse(event.data);
            setWithdrawal(updatedWithdrawal);

            // Notifications
            if (updatedWithdrawal.status === 'approved') {
                toast.success('Withdrawal approved! Ready to execute.');
            } else if (updatedWithdrawal.status === 'completed') {
                toast.success('Withdrawal completed successfully!');
            }
        };

        return () => ws.close();
    }, [withdrawalId]);

    if (!withdrawal) return <LoadingSpinner />;

    return (
        <StatusTimeline>
            <TimelineStep 
                status="completed" 
                title="Request Created"
                timestamp={withdrawal.createdAt}
            />
            <TimelineStep 
                status={withdrawal.status === 'pending' ? 'current' : 'completed'}
                title="Admin Review"
                description={withdrawal.status === 'rejected' ? withdrawal.rejectionReason : undefined}
            />
            {withdrawal.status === 'approved' && (
                <ExecuteButton withdrawalId={withdrawal.id}>
                    Execute Withdrawal
                </ExecuteButton>
            )}
            <TimelineStep 
                status={withdrawal.status === 'completed' ? 'completed' : 'pending'}
                title="Blockchain Execution"
                txHash={withdrawal.blockchainTxHash}
            />
        </StatusTimeline>
    );
}

Мониторинг и Алерты

Критические метрики

Операционные:

  • Withdrawals per day/hour
  • Average approval time
  • Success/failure ratio
  • Pending queue size

Безопасность:

  • Large withdrawals (> $1000)
  • High-risk pattern detections
  • Failed signature verifications
  • Admin approval patterns

Финансовые:

  • Total outflow per day
  • User balance consistency
  • Refund rates
  • Gas cost optimization

Alerting Rules

alerts:
  - name: "Large Withdrawal Pending"
    condition: "withdrawal_amount > 1000_USDC"
    severity: "warning"
    notification: "admin_team"

  - name: "Withdrawal Queue Backlog"
    condition: "pending_withdrawals > 50"
    severity: "high"

  - name: "High Failure Rate"
    condition: "failed_withdrawals_ratio > 0.1" 
    severity: "critical"

  - name: "Balance Inconsistency"
    condition: "balance_audit_error"
    severity: "critical"
    immediate: true

Связь с другими системами

Authentication Integration

  • Wallet signature verification использует тот же механизм что и auth
  • Admin approval проверяет admin status из authentication system
  • См. также: Authentication Flow

Deposits Integration

  • Balance validation использует balance накопленные от депозитов
  • Single source of truth в transactions table
  • См. также: Deposits Flow

Investment Integration

  • Withdrawal requests могут затрагивать invested funds
  • Portfolio liquidation для больших выводов

Связанная документация

Security Checklist (Integration-Only)

При изменениях в withdrawals flow:

  • ✅ JWT authentication обязателен для всех withdrawal requests
  • ✅ Balance validation работает atomically
  • ✅ Admin approval для всех withdrawals через Fordefi
  • ✅ Multi-phase workflow (request → admin → fordefi → completion)
  • ✅ Risk analysis и compliance через Fordefi
  • ✅ Fordefi webhook monitoring для статусов
  • ✅ Failed withdrawal recovery и refund логика
  • ✅ Audit trail полный (включая Fordefi transaction IDs)
  • ❌ НЕТ wallet signatures или приватных ключей
  • ✅ Rate limiting применяется на API уровне
  • ✅ Fordefi webhook signature verification

Архитектурные требования:

  • Clean Architecture соблюдена (Handler → Service → Repository)
  • Immutable transaction records
  • Proper error handling с rollback
  • Real-time status communication с frontend