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переводит сразу вprocessingPOST /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:
- Взять запрос (
POST /api/admin/withdrawals/:id/take) - Статус:
pending→processing -
Запрос заблокирован для других админов
-
Выполнить в Fordefi Dashboard
- Войти в Fordefi MPC custody
- Создать транзакцию USDC/USDT
- Указать сеть (TRON TRC20 или Ethereum ERC20)
- Ввести адрес получателя и сумму
-
Дождаться multi-sig подтверждения
-
Завершить запрос (
POST /api/admin/withdrawals/:id/complete) - Ввести transaction hash из Fordefi
- Статус:
processing→completed
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)
}
Related Documentation¶
API Endpoints Reference¶
User Endpoints¶
POST /api/withdrawals- Create withdrawal requestGET /api/withdrawals- List user's withdrawalsGET /api/withdrawals/{id}- Get withdrawal detailsPOST /api/withdrawals/{id}/cancel- Cancel pending withdrawal (only frompendingstatus)
Admin Endpoints (Упрощённый Workflow 2025-11)¶
GET /api/admin/withdrawals- List all withdrawalsGET /api/admin/withdrawals/pending- List pending withdrawalsGET /api/admin/withdrawals/{id}- Get withdrawal detailsPOST /api/admin/withdrawals/{id}/take- Take withdrawal for processing (pending → processing)POST /api/admin/withdrawals/bulk-take- Bulk take multiple withdrawalsPOST /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)