from embit import script from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.audit.log import write_audit_log from app.db.models import PendingTransaction, User, UtxoEvent, Withdrawal from app.electrum.client import ElectrumClient from app.rounds.config import get_round_config from app.wallet.balance import recompute_balance from app.wallet.hd import derive_user_key from app.wallet.psbt_builder import InsufficientFundsError, Utxo, build_signed_transaction class WithdrawalError(Exception): pass async def request_withdrawal( session: AsyncSession, client: ElectrumClient, user: User, external_address: str, amount_sats: int ) -> Withdrawal: config = await get_round_config(session) if amount_sats < config.min_amount_sats: raise WithdrawalError(f"amount below the minimum of {config.min_amount_sats} sats") unspent = ( await session.scalars( select(UtxoEvent).where(UtxoEvent.user_id == user.id, UtxoEvent.spent_txid.is_(None)) ) ).all() if sum(u.amount_sats for u in unspent) < amount_sats: raise WithdrawalError("insufficient balance") user_key = derive_user_key(user.derivation_index) from_script = script.p2wpkh(user_key.to_public()) utxos = [Utxo(u.txid, u.vout, u.amount_sats) for u in unspent] try: built = build_signed_transaction( signing_key=user_key, from_script=from_script, utxos=utxos, to_address=external_address, amount_sats=amount_sats, change_address=user.address, fee_rate_sat_vb=config.fee_rate_sat_vb, ) except InsufficientFundsError as exc: raise WithdrawalError(str(exc)) from exc await client.broadcast(built.raw_hex) spent_by_key = {(u.txid, u.vout): u for u in unspent} for spent in built.spent_utxos: spent_by_key[(spent.txid, spent.vout)].spent_txid = built.txid await recompute_balance(session, user.id) withdrawal = Withdrawal( user_id=user.id, external_address=external_address, amount_requested_sats=amount_sats, amount_sent_sats=built.recipient_sats, txid=built.txid, status="broadcast", ) session.add(withdrawal) await session.flush() session.add( PendingTransaction( kind="withdrawal", withdrawal_id=withdrawal.id, user_id=user.id, current_txid=built.txid, fee_rate_sat_vb=config.fee_rate_sat_vb, raw_tx_hex=built.raw_hex, status="pending", ) ) await write_audit_log( session, "withdrawal_sent", {"txid": built.txid, "amount_sent_sats": built.recipient_sats, "external_address": external_address}, user_id=user.id, ) await session.commit() await session.refresh(withdrawal) return withdrawal