Files
plm-lottery/app/api/routes/rounds.py
T
davideandClaude Sonnet 5 7dcf6d2756 Sync round countdown across clients and enforce the bet cutoff on deadline, not scheduler tick
The round timer relied on each client's own wall clock, so two browsers with
skewed local clocks showed different countdowns for the same round; the
server now also returns server_time so the frontend can correct for clock
skew. Also drop out-of-order /rounds/current responses (multiple independent
triggers could resolve late and revert the UI to a stale drawing/result
state) and prune per-round bookkeeping maps on round transitions.

Separately, place_bet only checked status == "open", leaving a window (up to
the scheduler's 5s tick interval) after a round's timer hit zero where a new
bet could still be accepted. place_bet now checks the round's own deadline
directly (round_accepts_bets), acting as an immediate "yellow light" for new
entries while still letting already-broadcast bets confirm before the round
closes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:07:09 +02:00

70 lines
2.6 KiB
Python

from datetime import datetime, timedelta, timezone
from fastapi import APIRouter, Depends, Request
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import RoundParticipant
from app.db.session import get_session
from app.rounds.config import get_round_config
from app.rounds.service import get_active_round
router = APIRouter(prefix="/rounds", tags=["rounds"])
class CurrentRoundResponse(BaseModel):
server_time: str
round_id: int | None = None
status: str | None = None
opened_at: str | None = None
closes_at: str | None = None
participant_count: int = 0
bet_amount_sats: int
jackpot_sats: int = 0
draw_animation_seconds: int
winner_user_id: int | None = None
winner_amount_sats: int | None = None
chain_tip_height: int | None = None
lottery_paused: bool = False
@router.get("/current", response_model=CurrentRoundResponse)
async def current_round(request: Request, session: AsyncSession = Depends(get_session)) -> CurrentRoundResponse:
config = await get_round_config(session)
round_ = await get_active_round(session)
listener = request.app.state.electrum_listener
chain_tip_height = listener.tip_height or None
if round_ is None:
await session.commit()
return CurrentRoundResponse(
server_time=datetime.now(timezone.utc).isoformat(),
bet_amount_sats=config.bet_amount_sats,
draw_animation_seconds=config.draw_animation_seconds,
chain_tip_height=chain_tip_height,
lottery_paused=config.paused,
)
participant_count = await session.scalar(
select(func.count()).select_from(RoundParticipant).where(RoundParticipant.round_id == round_.id)
) or 0
opened_at = round_.opened_at.replace(tzinfo=timezone.utc)
closes_at = opened_at + timedelta(seconds=config.round_duration_seconds)
await session.commit()
return CurrentRoundResponse(
server_time=datetime.now(timezone.utc).isoformat(),
round_id=round_.id,
status=round_.status,
opened_at=opened_at.isoformat(),
closes_at=closes_at.isoformat(),
participant_count=participant_count,
bet_amount_sats=config.bet_amount_sats,
jackpot_sats=participant_count * config.bet_amount_sats,
draw_animation_seconds=config.draw_animation_seconds,
winner_user_id=round_.winner_user_id,
winner_amount_sats=round_.winner_amount_sats,
chain_tip_height=chain_tip_height,
lottery_paused=config.paused,
)