Files
plm-lottery/app/tx/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

68 lines
2.4 KiB
Python

import asyncio
import logging
from collections.abc import Awaitable, Callable
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.db.models import PendingTransaction
from app.electrum.client import ElectrumClient
logger = logging.getLogger(__name__)
_POLL_INTERVAL_SECONDS = 10
ConfirmationHandler = Callable[[AsyncSession, PendingTransaction], Awaitable[None]]
_handlers: dict[str, ConfirmationHandler] = {}
def register_handler(kind: str, handler: ConfirmationHandler) -> None:
"""Domain modules (bets, rounds, withdrawals) register here so this generic
poller can notify them when one of their outgoing txs gets its 1st
confirmation, without this module importing them directly."""
_handlers[kind] = handler
async def poll_once(session_factory: async_sessionmaker, client: ElectrumClient) -> int:
async with session_factory() as session:
pending = (
await session.scalars(select(PendingTransaction).where(PendingTransaction.status == "pending"))
).all()
pending_ids = [p.id for p in pending]
confirmed = 0
for pending_id, txid, kind in [(p.id, p.current_txid, p.kind) for p in pending]:
tx = await client.get_transaction(txid, verbose=True)
if not tx or tx.get("confirmations", 0) < 1:
continue
async with session_factory() as session:
row = await session.get(PendingTransaction, pending_id)
if row is None or row.status != "pending":
continue
row.status = "confirmed"
handler = _handlers.get(kind)
if handler is not None:
await handler(session, row)
await session.commit()
confirmed += 1
return confirmed
class ConfirmationPoller:
def __init__(self, session_factory: async_sessionmaker, get_client: Callable[[], ElectrumClient | None]):
self._session_factory = session_factory
self._get_client = get_client
async def run(self) -> None:
while True:
client = self._get_client()
if client is not None:
try:
await poll_once(self._session_factory, client)
except asyncio.CancelledError:
raise
except Exception:
logger.exception("confirmation poll failed")
await asyncio.sleep(_POLL_INTERVAL_SECONDS)