from sqlalchemy.orm import Session from fastapi import HTTPException, status from typing import List, Optional from app.models.transaction import Transaction, FiatTransaction, TransactionStatus, TransactionType from app.models.wallet import Wallet, FiatAccount from app.models.user import User from app.schemas.transaction import TransactionCreate, FiatTransactionCreate from app.services.wallet import WalletService from app.utils.crypto import WalletFactory from app.core.config import settings import uuid import hashlib class TransactionService: def __init__(self, db: Session): self.db = db self.wallet_service = WalletService(db) def send_crypto(self, user: User, transaction_data: TransactionCreate) -> Transaction: # Get sender wallet from_wallet = self.wallet_service.get_wallet_by_id(transaction_data.from_wallet_id, user) if not from_wallet: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Wallet not found" ) # Validate currency match if from_wallet.currency != transaction_data.currency: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Currency mismatch" ) # Check balance if from_wallet.balance < transaction_data.amount: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient balance" ) # Validate recipient address if not WalletFactory.verify_address(transaction_data.currency, transaction_data.to_address): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid recipient address" ) # Calculate fee (simplified - in production, use dynamic fee calculation) fee = self._calculate_transaction_fee(transaction_data.currency, transaction_data.amount) if from_wallet.balance < (transaction_data.amount + fee): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient balance including fees" ) try: # Create transaction record transaction = Transaction( user_id=user.id, from_wallet_id=from_wallet.id, amount=transaction_data.amount, fee=fee, currency=transaction_data.currency, transaction_type=TransactionType.SEND, status=TransactionStatus.PENDING, from_address=from_wallet.address, to_address=transaction_data.to_address, notes=transaction_data.notes ) self.db.add(transaction) self.db.flush() # Get the transaction ID # Sign and broadcast transaction signed_hash = self._sign_transaction(user, from_wallet, transaction) transaction.transaction_hash = signed_hash # Update wallet balance new_balance = from_wallet.balance - transaction_data.amount - fee self.wallet_service.update_wallet_balance(from_wallet.id, new_balance) # In production, you would broadcast to the network here # For now, we'll mark as confirmed transaction.status = TransactionStatus.CONFIRMED transaction.confirmations = 1 self.db.commit() self.db.refresh(transaction) return transaction except Exception as e: self.db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Transaction failed: {str(e)}" ) def receive_crypto(self, user: User, amount: float, currency: str, from_address: str, to_wallet_id: int) -> Transaction: # Get recipient wallet to_wallet = self.wallet_service.get_wallet_by_id(to_wallet_id, user) if not to_wallet: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Wallet not found" ) # Create transaction record transaction = Transaction( user_id=user.id, to_wallet_id=to_wallet.id, amount=amount, currency=currency, transaction_type=TransactionType.RECEIVE, status=TransactionStatus.CONFIRMED, from_address=from_address, to_address=to_wallet.address, confirmations=1 ) self.db.add(transaction) # Update wallet balance new_balance = to_wallet.balance + amount self.wallet_service.update_wallet_balance(to_wallet.id, new_balance) self.db.commit() self.db.refresh(transaction) return transaction def deposit_fiat(self, user: User, transaction_data: FiatTransactionCreate) -> FiatTransaction: # Get fiat account fiat_account = self.db.query(FiatAccount).filter( FiatAccount.user_id == user.id, FiatAccount.currency == transaction_data.currency ).first() if not fiat_account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Fiat account not found" ) # Validate deposit limits if transaction_data.amount < settings.min_fiat_deposit: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Minimum deposit amount is {settings.min_fiat_deposit}" ) if transaction_data.amount > settings.max_fiat_deposit: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Maximum deposit amount is {settings.max_fiat_deposit}" ) # Create fiat transaction transaction = FiatTransaction( account_id=fiat_account.id, amount=transaction_data.amount, currency=transaction_data.currency, transaction_type=TransactionType.DEPOSIT, status=TransactionStatus.PENDING, payment_method=transaction_data.payment_method, notes=transaction_data.notes, bank_reference=str(uuid.uuid4()) ) self.db.add(transaction) self.db.commit() self.db.refresh(transaction) return transaction def withdraw_fiat(self, user: User, transaction_data: FiatTransactionCreate) -> FiatTransaction: # Get fiat account fiat_account = self.db.query(FiatAccount).filter( FiatAccount.user_id == user.id, FiatAccount.currency == transaction_data.currency ).first() if not fiat_account: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Fiat account not found" ) # Check balance if fiat_account.balance < transaction_data.amount: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Insufficient balance" ) # Validate withdrawal limits if transaction_data.amount < settings.min_fiat_withdrawal: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Minimum withdrawal amount is {settings.min_fiat_withdrawal}" ) if transaction_data.amount > settings.max_fiat_withdrawal: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Maximum withdrawal amount is {settings.max_fiat_withdrawal}" ) # Create fiat transaction transaction = FiatTransaction( account_id=fiat_account.id, amount=transaction_data.amount, currency=transaction_data.currency, transaction_type=TransactionType.WITHDRAWAL, status=TransactionStatus.PENDING, payment_method=transaction_data.payment_method, notes=transaction_data.notes, bank_reference=str(uuid.uuid4()) ) self.db.add(transaction) # Update fiat account balance new_balance = fiat_account.balance - transaction_data.amount self.wallet_service.update_fiat_balance(fiat_account.id, new_balance) self.db.commit() self.db.refresh(transaction) return transaction def get_user_transactions(self, user: User, limit: int = 50, offset: int = 0) -> List[Transaction]: return self.db.query(Transaction).filter( Transaction.user_id == user.id ).order_by(Transaction.created_at.desc()).offset(offset).limit(limit).all() def get_user_fiat_transactions(self, user: User, limit: int = 50, offset: int = 0) -> List[FiatTransaction]: fiat_accounts = self.wallet_service.get_user_fiat_accounts(user) account_ids = [acc.id for acc in fiat_accounts] return self.db.query(FiatTransaction).filter( FiatTransaction.account_id.in_(account_ids) ).order_by(FiatTransaction.created_at.desc()).offset(offset).limit(limit).all() def get_transaction_by_id(self, transaction_id: int, user: User) -> Optional[Transaction]: return self.db.query(Transaction).filter( Transaction.id == transaction_id, Transaction.user_id == user.id ).first() def _sign_transaction(self, user: User, from_wallet: Wallet, transaction: Transaction) -> str: """Sign the transaction using the wallet's private key""" try: # Get private key private_key = self.wallet_service.get_private_key(from_wallet, user) # Prepare transaction data for signing transaction_data = { 'from': from_wallet.address, 'to': transaction.to_address, 'amount': transaction.amount, 'fee': transaction.fee, 'nonce': transaction.id, 'currency': transaction.currency } # Sign transaction signature = WalletFactory.sign_transaction( transaction.currency, private_key, transaction_data ) # Generate transaction hash tx_hash = hashlib.sha256( f"{signature}_{transaction.id}_{transaction.from_address}_{transaction.to_address}".encode() ).hexdigest() return tx_hash except Exception as e: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to sign transaction: {str(e)}" ) def _calculate_transaction_fee(self, currency: str, amount: float) -> float: """Calculate transaction fee based on currency and amount""" # Simplified fee calculation - in production, use dynamic fee estimation fee_rates = { 'BTC': 0.0001, # Fixed fee 'ETH': 0.002, # 0.2% of amount 'USDT': 0.001 # 0.1% of amount } if currency == 'BTC': return fee_rates['BTC'] else: return amount * fee_rates.get(currency, 0.001)