from datetime import datetime, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import PendingTransaction, RoundParticipant from app.tx.confirmation import register_handler async def _on_bet_confirmed(session: AsyncSession, pending: PendingTransaction) -> None: """Resolved by (round_id, user_id) — the pair is unique per participant and, unlike the txid, cannot change under us. Keying this on bet_txid meant an RBF-bumped bet confirmed under a txid no participant carried, so the row stayed "broadcast" forever and the round could never close (B-02). The txid is kept in step by tx/broadcast.py too, but correctness here no longer depends on it.""" participant = await session.scalar( select(RoundParticipant).where( RoundParticipant.round_id == pending.round_id, RoundParticipant.user_id == pending.user_id, ) ) if participant is None: # Fall back to the txid for rows written before this changed, and for any # pending row missing its round/user link. participant = await session.scalar( select(RoundParticipant).where(RoundParticipant.bet_txid == pending.current_txid) ) if participant is not None and participant.status in ("building", "broadcast"): participant.status = "confirmed" participant.confirmed_at = datetime.now(timezone.utc) register_handler("bet", _on_bet_confirmed)