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:
+46
-1
@@ -1,7 +1,9 @@
|
||||
from embit.transaction import Transaction
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models import User, UtxoEvent
|
||||
from app.db.models import PendingTransaction, User, UtxoEvent
|
||||
from app.wallet.plm_network import PLM_MAINNET
|
||||
|
||||
|
||||
async def recompute_balance(session: AsyncSession, user_id: int) -> int:
|
||||
@@ -14,3 +16,46 @@ async def recompute_balance(session: AsyncSession, user_id: int) -> int:
|
||||
user = await session.get(User, user_id)
|
||||
user.cached_balance_sats = balance or 0
|
||||
return user.cached_balance_sats
|
||||
|
||||
|
||||
async def compute_pending_balance(session: AsyncSession, user: User) -> tuple[int, bool]:
|
||||
"""Balance including the user's own change still in flight.
|
||||
|
||||
Placing a bet or a withdrawal spends whatever UTXOs cover the amount — often
|
||||
much larger than the amount actually moving, since select_utxos() picks
|
||||
whole UTXOs — and recompute_balance() drops that entire input total from
|
||||
cached_balance_sats the moment the tx is broadcast (spent_txid is set right
|
||||
away, well before the tx has any confirmations). The change output that
|
||||
returns the difference only becomes a UtxoEvent (and so counts toward the
|
||||
confirmed balance again) once it gets its own 1st confirmation. In between,
|
||||
User.cached_balance_sats alone understates the user's real balance by the
|
||||
full unconfirmed change amount, which can look like a much bigger loss than
|
||||
the tx actually represents.
|
||||
|
||||
This walks every in-flight (status="pending") bet/withdrawal PendingTransaction
|
||||
of this user, decodes its current raw tx (kept up to date across RBF bumps —
|
||||
see tx/broadcast.py:bump_fee), and sums whichever outputs pay back to the
|
||||
user's own address. Adding that to cached_balance_sats gives the balance the
|
||||
user will end up with once everything currently in flight confirms.
|
||||
|
||||
Returns (pending_inclusive_balance_sats, has_pending) — has_pending tells the
|
||||
caller whether this differs from the confirmed-only balance at all.
|
||||
"""
|
||||
pending = (
|
||||
await session.scalars(
|
||||
select(PendingTransaction).where(
|
||||
PendingTransaction.user_id == user.id,
|
||||
PendingTransaction.kind.in_(("bet", "withdrawal")),
|
||||
PendingTransaction.status == "pending",
|
||||
)
|
||||
)
|
||||
).all()
|
||||
|
||||
pending_change_sats = 0
|
||||
for row in pending:
|
||||
tx = Transaction.parse(bytes.fromhex(row.raw_tx_hex))
|
||||
for out in tx.vout:
|
||||
if out.script_pubkey.address(network=PLM_MAINNET) == user.address:
|
||||
pending_change_sats += out.value
|
||||
|
||||
return user.cached_balance_sats + pending_change_sats, bool(pending)
|
||||
|
||||
Reference in New Issue
Block a user