Show pending-inclusive balance and per-player round outcome reliably
Balance display: place_bet/request_withdrawal spend whole UTXOs and mark them spent at broadcast time, well before confirmation, so the confirmed-only balance could drop by far more than the amount actually moving. Add compute_pending_balance() (app/wallet/balance.py) to fold the unconfirmed change from in-flight bet/withdrawal PendingTransactions back in; GET /users/me now returns pending_balance_sats + has_pending, and the frontend shows it colored green (settled) or amber (still pending) instead of the confirmed-only figure. Round outcome display: the win/lose reveal and the "pagamento al vincitore in corso" status were fighting over the same UI slot, and the reveal broke across a page refresh. Now: - The round-status box (generic phase progress) and the personal win/lose box are independent and can both be visible at once. - The win/lose box only renders for users who actually played in that round (new user_played field on GET /rounds/current, via a new optional-auth dependency so the endpoint stays usable logged-out). - The reveal delay is anchored to the round's server-provided closes_at instead of a client-side "first seen" timestamp, so repeated reloads can't reset it, and the revealed result is persisted in localStorage so it survives a refresh even after the round has fully closed. - GET /users/me/last-round-result is a durable DB-backed backstop for players who miss the live window entirely (backgrounded tab, offline). Also hardens the frontend polling loop: call() now times out instead of hanging forever, and a session-epoch counter stops an in-flight request from a previous login from resurrecting a duplicate poll loop after logout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,8 @@ from pydantic import BaseModel
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import RoundParticipant
|
||||
from app.auth.dependencies import get_optional_user
|
||||
from app.db.models import RoundParticipant, User
|
||||
from app.db.session import get_session
|
||||
from app.rounds.config import get_round_config
|
||||
from app.rounds.service import get_active_round
|
||||
@@ -29,10 +30,15 @@ class CurrentRoundResponse(BaseModel):
|
||||
draw_block_hash: str | None = None
|
||||
chain_tip_height: int | None = None
|
||||
lottery_paused: bool = False
|
||||
user_played: bool = False
|
||||
|
||||
|
||||
@router.get("/current", response_model=CurrentRoundResponse)
|
||||
async def current_round(request: Request, session: AsyncSession = Depends(get_session)) -> CurrentRoundResponse:
|
||||
async def current_round(
|
||||
request: Request,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
user: User | None = Depends(get_optional_user),
|
||||
) -> CurrentRoundResponse:
|
||||
config = await get_round_config(session)
|
||||
round_ = await get_active_round(session)
|
||||
listener = request.app.state.electrum_listener
|
||||
@@ -52,6 +58,21 @@ async def current_round(request: Request, session: AsyncSession = Depends(get_se
|
||||
) or 0
|
||||
opened_at = round_.opened_at.replace(tzinfo=timezone.utc)
|
||||
closes_at = opened_at + timedelta(seconds=config.round_duration_seconds)
|
||||
|
||||
# Lets the frontend show the personalized win/lose reveal only to players in
|
||||
# this round — everyone else (not logged in, or logged in but didn't bet)
|
||||
# just sees the generic phase progress instead of a "non hai vinto" that
|
||||
# wouldn't mean anything to them.
|
||||
user_played = False
|
||||
if user is not None:
|
||||
user_played = (
|
||||
await session.scalar(
|
||||
select(RoundParticipant).where(
|
||||
RoundParticipant.round_id == round_.id, RoundParticipant.user_id == user.id
|
||||
)
|
||||
)
|
||||
) is not None
|
||||
|
||||
await session.commit()
|
||||
|
||||
# Shown to players as "jackpot": the winner's 70% share of the pool (same
|
||||
@@ -76,4 +97,5 @@ async def current_round(request: Request, session: AsyncSession = Depends(get_se
|
||||
draw_block_hash=round_.draw_block_hash,
|
||||
chain_tip_height=chain_tip_height,
|
||||
lottery_paused=config.paused,
|
||||
user_played=user_played,
|
||||
)
|
||||
|
||||
+10
-1
@@ -7,6 +7,7 @@ from app.auth.dependencies import get_current_user
|
||||
from app.auth.security import hash_password, verify_password
|
||||
from app.db.models import Round, RoundParticipant, User
|
||||
from app.db.session import get_session
|
||||
from app.wallet.balance import compute_pending_balance
|
||||
|
||||
router = APIRouter(prefix="/users", tags=["users"])
|
||||
|
||||
@@ -18,16 +19,24 @@ class MeResponse(BaseModel):
|
||||
username: str
|
||||
address: str
|
||||
balance_sats: int
|
||||
pending_balance_sats: int
|
||||
has_pending: bool
|
||||
created_at: str
|
||||
|
||||
|
||||
@router.get("/me", response_model=MeResponse)
|
||||
async def me(user: User = Depends(get_current_user)) -> MeResponse:
|
||||
async def me(
|
||||
user: User = Depends(get_current_user),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> MeResponse:
|
||||
pending_balance_sats, has_pending = await compute_pending_balance(session, user)
|
||||
return MeResponse(
|
||||
id=user.id,
|
||||
username=user.username,
|
||||
address=user.address,
|
||||
balance_sats=user.cached_balance_sats,
|
||||
pending_balance_sats=pending_balance_sats,
|
||||
has_pending=has_pending,
|
||||
created_at=user.created_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user