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)¶
- requested → admin_review → fordefi_pending → fordefi_approved → completed
- requested → admin_review → rejected (admin decision)
- fordefi_pending → fordefi_rejected (compliance/security)
- fordefi_approved → failed (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 для больших выводов
Связанная документация¶
- ADR-0001: Email-First Authentication with Supabase - JWT authentication принципы
- ADR-0004: Integration-Only Architecture - Архитектурные принципы без HD Wallet
- Deposits Flow - Crypto2B integration для депозитов
- System Overview - Общая архитектура Integration-Only
- Backend: Fordefi Integration - Подробности Fordefi API
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