Розробка системи депозитів/виводів крипто-казино
Фінансова система крипто-казино є технічно найкритичнішим компонентом. Вона повинна обробляти тисячі транзакцій, підтримувати миттєві депозити, мінімізувати затримку виводів та запобігати помилкам в обліку коштів користувачів.
Архітектура: Internal Ledger
Крипто-казино не зберігає кошти користувачів безпосередньо на блокчейні. Правильна архітектура — internal ledger (внутрішня книга обліку):
Користувач → Deposit Address (blockchain) → Hot Wallet → Internal Balance
↓
Вивід: Internal Balance → Hot Wallet → Гаманець користувача (blockchain)
Внутрішній баланс — це запис у базі даних. Реальні монети знаходяться у гарячому гаманці казино. Це стандартна модель для всіх казино та бірж.
Переваги:
- Миттєві операції в системі (ставки, бонуси, переводи між іграми)
- Немає необхідності чекати підтвердження блокчейну для кожної ставки
- Можливість дробових балансів нижче мінімальної суми транзакції
Потік депозиту
class DepositService:
REQUIRED_CONFIRMATIONS = {
"BTC": 2,
"ETH": 12,
"USDT_TRC20": 20,
"SOL": 30,
"BNB": 15,
}
async def get_deposit_address(self, user_id: str, currency: str) -> str:
"""Повертає унікальну адресу депозиту для користувача"""
# Перевіряємо існуючу адресу
existing = await self.address_repo.get(user_id=user_id, currency=currency)
if existing:
return existing.address
# Генеруємо нову адресу з HD Wallet
address = await self.wallet_manager.generate_address(currency, user_id)
await self.address_repo.save(
user_id=user_id,
currency=currency,
address=address
)
return address
async def on_incoming_transaction(self, tx: BlockchainTransaction):
"""Викликається при кожній вхідній транзакції"""
# Знаходимо користувача за адресою
address_record = await self.address_repo.find_by_address(
tx.to_address, tx.currency
)
if not address_record:
return # Не наша адреса
# Зберігаємо очікуваний депозит
deposit = PendingDeposit(
user_id=address_record.user_id,
currency=tx.currency,
amount=tx.amount,
tx_hash=tx.hash,
required_confirmations=self.REQUIRED_CONFIRMATIONS.get(tx.currency, 12),
current_confirmations=tx.confirmations,
status="PENDING",
)
await self.deposit_repo.save(deposit)
async def on_confirmation_update(self, tx_hash: str, confirmations: int):
"""Оновлення кількості підтверджень"""
deposit = await self.deposit_repo.get_by_tx(tx_hash)
if not deposit or deposit.status != "PENDING":
return
if confirmations >= deposit.required_confirmations:
await self.credit_user(deposit)
async def credit_user(self, deposit: PendingDeposit):
"""Зачислюємо на внутрішній баланс (ідемпотентна операція)"""
async with self.db.transaction():
# Перевіряємо що ще не зачислювали (idempotency)
if await self.deposit_repo.is_credited(deposit.id):
return
await self.balance_repo.credit(
user_id=deposit.user_id,
currency=deposit.currency,
amount=deposit.amount,
reference=f"DEPOSIT:{deposit.tx_hash}",
)
await self.deposit_repo.mark_credited(deposit.id)
# Повідомляємо користувача
await self.notifier.send_deposit_confirmed(
user_id=deposit.user_id,
amount=deposit.amount,
currency=deposit.currency,
)
Внутрішній册облікових записів
Весь облік коштів здійснюється через книгу обліку з подвійним записом:
CREATE TABLE ledger_entries (
id BIGSERIAL PRIMARY KEY,
entry_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
user_id UUID NOT NULL,
currency VARCHAR(16) NOT NULL,
amount NUMERIC(24, 8) NOT NULL, -- позитивне = кредит, негативне = дебет
balance_after NUMERIC(24, 8) NOT NULL,
type VARCHAR(32) NOT NULL, -- DEPOSIT, WITHDRAWAL, BET_WIN, BET_LOSS, BONUS, etc.
reference_id VARCHAR(64), -- tx_hash, bet_id, bonus_id
description VARCHAR(255),
INDEX(user_id, currency, entry_time DESC)
);
Кожна операція з балансом створює запис у книзі обліку. Поточний баланс — це сума всіх записів користувача за валютою:
-- Отримання актуального балансу
SELECT currency, SUM(amount) as balance
FROM ledger_entries
WHERE user_id = $1
GROUP BY currency;
-- Або денормалізована таблиця балансів (оновлюється атомарно)
CREATE TABLE user_balances (
user_id UUID NOT NULL,
currency VARCHAR(16) NOT NULL,
balance NUMERIC(24, 8) NOT NULL DEFAULT 0,
locked NUMERIC(24, 8) NOT NULL DEFAULT 0, -- у процесі виводу
PRIMARY KEY (user_id, currency),
CHECK (balance >= 0),
CHECK (locked >= 0),
CHECK (balance >= locked)
);
Потік виводу
class WithdrawalService:
MIN_WITHDRAWAL = {
"BTC": Decimal("0.0001"),
"ETH": Decimal("0.005"),
"USDT_TRC20": Decimal("5"),
}
async def request_withdrawal(
self,
user_id: str,
currency: str,
amount: Decimal,
destination_address: str,
) -> WithdrawalRequest:
# Валідація
if amount < self.MIN_WITHDRAWAL.get(currency, Decimal("1")):
raise ValidationError(f"Мінімальний вивід: {self.MIN_WITHDRAWAL[currency]} {currency}")
user_balance = await self.balance_repo.get_balance(user_id, currency)
if user_balance.available < amount:
raise InsufficientFundsError()
# Валідація адреси блокчейну
if not self.validate_address(destination_address, currency):
raise ValidationError("Невірна адреса призначення")
# Перевірка AML
aml_result = await self.aml_service.check_address(destination_address, currency)
if aml_result.risk_score > 7:
raise ComplianceError("Адреса призначення не пройшла перевірку AML")
async with self.db.transaction():
# Блокуємо кошти
await self.balance_repo.lock_funds(user_id, currency, amount)
# Створюємо заявку
request = WithdrawalRequest(
user_id=user_id,
currency=currency,
amount=amount,
destination=destination_address,
status="PENDING",
aml_score=aml_result.risk_score,
)
await self.withdrawal_repo.save(request)
# Ставимо в чергу на обробку
await self.withdrawal_queue.enqueue(request.id)
return request
async def process_withdrawal(self, request_id: str):
"""Обробляється воркером з черги"""
request = await self.withdrawal_repo.get(request_id)
# Ручна перевірка для великих виводів
if request.amount_usd > 10_000:
if not request.manual_approved:
await self.notify_compliance_team(request)
return
# Відправляємо транзакцію з гарячого гаманця
try:
tx_hash = await self.hot_wallet.send(
currency=request.currency,
to=request.destination,
amount=request.amount - self.get_network_fee(request.currency),
)
await self.withdrawal_repo.mark_sent(request.id, tx_hash)
except InsufficientHotWalletFunds:
# Гарячий гаманець потребує поповнення з холодного сховища
await self.alert_treasury("Hot wallet needs refill")
await self.withdrawal_repo.mark_queued(request.id)
Управління гарячим/холодним гаманцем
Гарячий гаманець — онлайн гаманець для обробки виводів. Повинен містити лише оперативні резерви: 15–20% від сукупних коштів користувачів.
Холодний гаманець — офлайн сховище. Multi-signature (3 з 5 ключів), ключі зберігаються у різних відповідальних осіб, поповнення гарячого гаманця — ручний процес з кількома підписами.
class HotWalletManager:
TARGET_BALANCE_PCT = 0.15 # 15% від сукупних депозитів
LOW_BALANCE_THRESHOLD_PCT = 0.05 # сигнал при < 5%
async def check_balance_health(self, currency: str):
hot_balance = await self.get_hot_wallet_balance(currency)
total_user_balances = await self.balance_repo.get_total_user_balance(currency)
ratio = float(hot_balance / total_user_balances) if total_user_balances > 0 else 1.0
if ratio < self.LOW_BALANCE_THRESHOLD_PCT:
await self.alert_treasury(
f"Hot wallet {currency} low: {ratio:.1%} of user balances. "
f"Refill needed: {total_user_balances * Decimal('0.15') - hot_balance:.4f} {currency}"
)
Звірка
Щоденна звірка внутрішніх балансів з реальними даними блокчейну:
async def daily_reconciliation(self, currency: str):
"""Звіряємо внутрішні дані з blockchain"""
# Сукупний внутрішній баланс користувачів
internal_total = await self.balance_repo.get_total_user_balance(currency)
# Реальний баланс на наших адресах
hot_balance = await self.wallet.get_balance(currency, 'hot')
cold_balance = await self.wallet.get_balance(currency, 'cold')
real_total = hot_balance + cold_balance
# Очікуючі виводи (ще не відправлені)
pending = await self.withdrawal_repo.get_pending_total(currency)
discrepancy = real_total + pending - internal_total
if abs(discrepancy) > Decimal("0.0001"):
await self.alert_finance(
f"RECONCILIATION DISCREPANCY {currency}: {discrepancy}"
)
Система звірки — страховка від помилок у коді та потенційних зловживань. Будь-яке розходження потребує негайного розслідування.







