Advertise the jackpot that will actually be paid (B-65)
participant_count and jackpot_sats were computed over every round_participants row, while the draw only picks from confirmed participants and the payout only spends their sats. So the advertised jackpot could exceed the one paid out, and a player whose bet was later abandoned appeared in the count and then vanished again. Counting only confirmed rows would have fixed the arithmetic and broken something else: the player who just bet would see neither themselves nor their money for a whole block. So this is the same confirmed/in-flight split the balance already exposes (balance_sats vs pending_balance_sats): participant_count and jackpot_sats are now the confirmed, authoritative figures, and pending_participant_count/pending_jackpot_sats/has_pending_bets report what is in flight — inclusive figures, not deltas, matching the balance pair's convention. /'s round card shows the confirmed numbers big and the difference as an amber "+N in attesa" suffix, reusing .balance-pending's colour for the same "not settled yet" meaning. The two new spans render from server data through t(), so they carry no data-i18n and onLanguageChange() repaints them from the last response — the one-mechanism-per-element rule. Both strings are in all 7 languages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -91,9 +91,21 @@ class CurrentRoundResponse(BaseModel):
|
||||
status: str | None = None
|
||||
opened_at: str | None = None
|
||||
closes_at: str | None = None
|
||||
# B-65: confirmed participants only — the ones the draw actually picks from and
|
||||
# whose sats are actually in the pool. The pending_* pair below is the same
|
||||
# confirmed/in-flight split the balance already exposes (see
|
||||
# wallet/balance.py's balance_sats vs pending_balance_sats), and for the same
|
||||
# reason: the authoritative number must be the one that will be paid, while the
|
||||
# player who just bet still needs to see their own bet somewhere.
|
||||
participant_count: int = 0
|
||||
bet_amount_sats: int
|
||||
jackpot_sats: int = 0
|
||||
# Inclusive of bets still building/broadcast, exactly like pending_balance_sats
|
||||
# is inclusive of unconfirmed change — not deltas. Equal to the confirmed
|
||||
# figures above when nothing is in flight, which is what has_pending_bets says.
|
||||
pending_participant_count: int = 0
|
||||
pending_jackpot_sats: int = 0
|
||||
has_pending_bets: bool = False
|
||||
draw_animation_seconds: int
|
||||
winner_user_id: int | None = None
|
||||
winner_amount_sats: int | None = None
|
||||
@@ -128,19 +140,32 @@ async def current_round(
|
||||
lottery_paused=config.paused,
|
||||
)
|
||||
|
||||
participant_count = await session.scalar(
|
||||
select(func.count()).select_from(RoundParticipant).where(RoundParticipant.round_id == round_.id)
|
||||
) or 0
|
||||
# The pool is the sum of what the participants' bets actually paid into the pool
|
||||
# address — each one is already net of that bet's network fee. Deriving it from
|
||||
# participant_count * the *current* bet_amount_sats instead overstated it, and
|
||||
# silently changed the advertised jackpot of a round in progress whenever an
|
||||
# operator edited the bet amount (B-11).
|
||||
pool_amount_sats = await session.scalar(
|
||||
select(func.coalesce(func.sum(RoundParticipant.bet_amount_sats), 0)).where(
|
||||
RoundParticipant.round_id == round_.id
|
||||
#
|
||||
# Split confirmed from in-flight (B-65): the draw only picks from confirmed
|
||||
# participants and the payout only spends their sats, so counting every row
|
||||
# advertised a jackpot larger than the one that would be paid, and made a
|
||||
# participant appear and then vanish again if their bet was later abandoned.
|
||||
counts = (
|
||||
await session.execute(
|
||||
select(
|
||||
func.count(),
|
||||
func.coalesce(func.sum(RoundParticipant.bet_amount_sats), 0),
|
||||
func.count().filter(RoundParticipant.status == "confirmed"),
|
||||
func.coalesce(
|
||||
func.sum(RoundParticipant.bet_amount_sats).filter(
|
||||
RoundParticipant.status == "confirmed"
|
||||
),
|
||||
0,
|
||||
),
|
||||
).where(RoundParticipant.round_id == round_.id)
|
||||
)
|
||||
) or 0
|
||||
).one()
|
||||
all_count, all_pool_sats, participant_count, pool_amount_sats = counts
|
||||
opened_at = round_.opened_at.replace(tzinfo=timezone.utc)
|
||||
# B-61: from the round's own duration — the countdown clients are watching must
|
||||
# not jump because an operator edited the config mid-round.
|
||||
@@ -178,6 +203,9 @@ async def current_round(
|
||||
participant_count=participant_count,
|
||||
bet_amount_sats=config.bet_amount_sats,
|
||||
jackpot_sats=jackpot_sats,
|
||||
pending_participant_count=all_count,
|
||||
pending_jackpot_sats=winner_share(all_pool_sats),
|
||||
has_pending_bets=all_count > participant_count,
|
||||
draw_animation_seconds=config.draw_animation_seconds,
|
||||
winner_user_id=round_.winner_user_id,
|
||||
winner_amount_sats=round_.winner_amount_sats,
|
||||
|
||||
Reference in New Issue
Block a user