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>
This commit is contained in:
1 parent
f27fe6243c
commit
7dcf6d2756
8 files changed
+87
-17
No files matched your search
+30
-1
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
@@ -5,7 +7,8 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from app.bets.service import BetError, place_bet
|
||||
from app.config import settings
|
||||
from app.db.base import Base
|
||||
from app.db.models import AuditLog, PendingTransaction, RoundParticipant, User, UtxoEvent
|
||||
from app.db.models import AuditLog, PendingTransaction, Round, RoundConfig, RoundParticipant, User, UtxoEvent
|
||||
from app.rounds.service import open_new_round_if_needed
|
||||
from app.wallet.hd import derive_user_address
|
||||
|
||||
|
||||
@@ -104,3 +107,29 @@ async def test_place_bet_rejects_second_bet_same_round(session_factory):
|
||||
async with session_factory() as session:
|
||||
participants = (await session.scalars(select(RoundParticipant))).all()
|
||||
assert len(participants) == 1
|
||||
|
||||
|
||||
async def test_place_bet_rejects_after_timer_expires_even_if_still_open(session_factory):
|
||||
"""The scheduler only flips status "open" -> "closing" on its next tick (up
|
||||
to a few seconds late) — place_bet must independently refuse bets once the
|
||||
round's own deadline has passed, so no new player can sneak in during that
|
||||
gap (see rounds/service.round_accepts_bets)."""
|
||||
user_id = await _make_funded_user(session_factory, 3, 3_000_000_000)
|
||||
client = FakeElectrumClient()
|
||||
|
||||
async with session_factory() as session:
|
||||
session.add(RoundConfig(fee_address="", round_duration_seconds=60))
|
||||
round_ = await open_new_round_if_needed(session)
|
||||
round_.opened_at = datetime.now(timezone.utc) - timedelta(seconds=61)
|
||||
await session.commit()
|
||||
|
||||
async with session_factory() as session:
|
||||
user = await session.get(User, user_id)
|
||||
with pytest.raises(BetError, match="closing"):
|
||||
await place_bet(session, client, user)
|
||||
|
||||
async with session_factory() as session:
|
||||
participants = (await session.scalars(select(RoundParticipant))).all()
|
||||
assert len(participants) == 0
|
||||
round_ = (await session.scalars(select(Round))).one()
|
||||
assert round_.status == "open" # scheduler hasn't ticked — status is unchanged, only the check is deadline-aware
|
||||
Reference in new issue
Block a user