diff --git a/app/api/routes/rounds.py b/app/api/routes/rounds.py index 2f88fad..38a44b1 100644 --- a/app/api/routes/rounds.py +++ b/app/api/routes/rounds.py @@ -25,6 +25,8 @@ class CurrentRoundResponse(BaseModel): draw_animation_seconds: int winner_user_id: int | None = None winner_amount_sats: int | None = None + draw_block_height: int | None = None + draw_block_hash: str | None = None chain_tip_height: int | None = None lottery_paused: bool = False @@ -70,6 +72,8 @@ async def current_round(request: Request, session: AsyncSession = Depends(get_se draw_animation_seconds=config.draw_animation_seconds, winner_user_id=round_.winner_user_id, winner_amount_sats=round_.winner_amount_sats, + draw_block_height=round_.draw_block_height, + draw_block_hash=round_.draw_block_hash, chain_tip_height=chain_tip_height, lottery_paused=config.paused, ) diff --git a/app/rounds/confirmation.py b/app/rounds/confirmation.py index c11991c..64660be 100644 --- a/app/rounds/confirmation.py +++ b/app/rounds/confirmation.py @@ -1,3 +1,5 @@ +from datetime import datetime, timezone + from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -9,6 +11,12 @@ async def _on_payout_confirmed(session: AsyncSession, pending: PendingTransactio round_ = await session.scalar(select(Round).where(Round.payout_txid == pending.current_txid)) if round_ is not None and round_.status == "paying_out": round_.status = "closed" + # closed_at is what round_cooldown_seconds counts from (service.py's + # open_new_round_if_needed) — re-stamp it here at actual payout + # confirmation time rather than leaving it at the earlier "closing" + # timestamp, so a short cooldown (e.g. 20s) is a real pause after the + # winner's tx confirms, not swallowed by the ~2 block-time draw+payout wait. + round_.closed_at = datetime.now(timezone.utc) # The winner's own address is already watched by the Electrum listener, so # their balance is credited by the normal deposit path once this confirms. diff --git a/app/rounds/scheduler.py b/app/rounds/scheduler.py index 4fb05fc..b6bf4eb 100644 --- a/app/rounds/scheduler.py +++ b/app/rounds/scheduler.py @@ -54,12 +54,23 @@ class RoundScheduler: round_id, status, opened_at = round_.id, round_.status, round_.opened_at round_duration_seconds = (await get_round_config(session)).round_duration_seconds - if status != "open": - return # already closing/drawing/paying_out; progress happens elsewhere + if status not in ("open", "closing"): + return # already drawing/paying_out; progress happens elsewhere - opened_at = opened_at.replace(tzinfo=timezone.utc) - if datetime.now(timezone.utc) < opened_at + timedelta(seconds=round_duration_seconds): - return + if status == "open": + opened_at = opened_at.replace(tzinfo=timezone.utc) + if datetime.now(timezone.utc) < opened_at + timedelta(seconds=round_duration_seconds): + return + + async with self._session_factory() as session: + round_ = await session.get(Round, round_id) + # No new bets from here on, regardless of how long the pending-bet + # wait below takes — flip to "closing" immediately so it's observable + # via /rounds/current (e.g. "round closed, waiting for jackpot + # confirmation") instead of silently staying "open" past the deadline. + round_.status = "closing" + round_.closed_at = datetime.now(timezone.utc) + await session.commit() async with self._session_factory() as session: pending_count = await session.scalar( @@ -68,19 +79,13 @@ class RoundScheduler: .where(RoundParticipant.round_id == round_id, RoundParticipant.status == "broadcast") ) if pending_count: - return # wait for in-flight bets to confirm before closing + return # wait for in-flight bets to confirm before closing; stays "closing" await self._close_and_draw(round_id) async def _close_and_draw(self, round_id: int) -> None: async with self._session_factory() as session: round_ = await session.get(Round, round_id) - round_.status = "closing" - round_.closed_at = datetime.now(timezone.utc) - # Committed on its own so "closing" is actually observable (e.g. by - # /rounds/current) instead of being overwritten in memory by "closed" - # or "drawing" below before ever reaching the database. - await session.commit() participants = ( await session.scalars(