Files
plm-lottery/tests/unit/test_confirmation.py
T
davideandClaude Sonnet 5 fc2aadbc7e Add transaction broadcast, confirmation polling and RBF fee-bump
Shared per-user locking to serialize bet/withdrawal PSBT builds
(tx/locks.py), a confirmation poller for pending outgoing transactions,
and the timeout->fee-bump->rebroadcast loop used by bets, payouts and
withdrawals alike (tx/broadcast.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:25:57 +02:00

83 lines
3.3 KiB
Python

import pytest
from sqlalchemy import select
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
import app.bets.confirmation # noqa: F401 (registers the "bet" handler)
import app.rounds.confirmation # noqa: F401 (registers the "payout" handler)
from app.db.base import Base
from app.db.models import PendingTransaction, Round, RoundParticipant
from app.tx.confirmation import poll_once
class FakeClient:
def __init__(self, confirmations_by_txid: dict[str, int]):
self._confirmations = confirmations_by_txid
async def get_transaction(self, txid: str, verbose: bool = False) -> dict:
return {"confirmations": self._confirmations.get(txid, 0)}
@pytest.fixture
async def session_factory():
engine = create_async_engine("sqlite+aiosqlite:///:memory:")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield async_sessionmaker(engine, expire_on_commit=False)
await engine.dispose()
async def test_bet_confirmation_marks_participant_confirmed(session_factory):
async with session_factory() as session:
session.add(Round(id=1, status="open"))
session.add(
RoundParticipant(
round_id=1, user_id=1, bet_amount_sats=1_000, bet_txid="tx1", status="broadcast"
)
)
session.add(
PendingTransaction(kind="bet", round_id=1, user_id=1, current_txid="tx1", fee_rate_sat_vb=1, raw_tx_hex="00", status="pending")
)
await session.commit()
client = FakeClient({"tx1": 1})
confirmed = await poll_once(session_factory, client)
assert confirmed == 1
async with session_factory() as session:
participant = (await session.scalars(select(RoundParticipant))).one()
assert participant.status == "confirmed"
assert participant.confirmed_at is not None
pending = (await session.scalars(select(PendingTransaction))).one()
assert pending.status == "confirmed"
async def test_unconfirmed_tx_is_left_pending(session_factory):
async with session_factory() as session:
session.add(Round(id=2, status="open"))
session.add(RoundParticipant(round_id=2, user_id=1, bet_amount_sats=1_000, bet_txid="tx2", status="broadcast"))
session.add(PendingTransaction(kind="bet", round_id=2, user_id=1, current_txid="tx2", fee_rate_sat_vb=1, raw_tx_hex="00", status="pending"))
await session.commit()
client = FakeClient({"tx2": 0})
confirmed = await poll_once(session_factory, client)
assert confirmed == 0
async with session_factory() as session:
participant = (await session.scalars(select(RoundParticipant))).one()
assert participant.status == "broadcast"
async def test_payout_confirmation_closes_round(session_factory):
async with session_factory() as session:
session.add(Round(id=3, status="paying_out", payout_txid="tx3"))
session.add(PendingTransaction(kind="payout", round_id=3, current_txid="tx3", fee_rate_sat_vb=1, raw_tx_hex="00", status="pending"))
await session.commit()
client = FakeClient({"tx3": 2})
confirmed = await poll_once(session_factory, client)
assert confirmed == 1
async with session_factory() as session:
round_ = await session.get(Round, 3)
assert round_.status == "closed"