transfer_split sends one on-chain tx to many destinations, so payout rows
in a batch share a tx_hash. The old UNIQUE index on payouts.tx_hash made
the 2nd+ row fail to record, leaving funds sent but rows stranded in
'processing' and re-sendable by the retry service.
- Migration 015 drops the payouts tx_hash unique index (idempotency is
enforced by idx_payouts_one_per_game) and extends that guard to cover
'processing' so no second payable row can be inserted mid-flight.
- Batch completion now updates the whole batch in a single transaction via
id = ANY(...), so rows can't be left half-completed. Per-user stats use
BIGINT arithmetic in SQL (no JS float).
- Ambiguous batch failures are marked 'needs_review' (which the retry
service does NOT pick up) instead of 'failed', preventing a double-send
when transfer_split may have already broadcast. Operator is alerted.
Adds batchPayout.test.js covering the shared-tx_hash completion and the
needs_review failure path. Full suite: 12 suites / 115 tests.
Phase 0.1 — Underpayment confirmation bug (CRITICAL):
Payment confirmation gated on `status.confirmed`, which checkPaymentStatus
sets for ANY incoming tx regardless of amount, so sending a single atomic
unit granted a full game/credits. Now gate on `status.complete` (received
>= expected). Underpayers are warned once and the payment stays pending so
a top-up can complete it. Persist `received_amount` for single-game and
credits confirmations for reconciliation. Adds an underpayment regression
test.
Phase 0.2 — Provably-fair was decorative (CRITICAL):
The committed seed had zero effect on the dungeon (created seededRNG was
unused; generation used Math.random + a globally-seeded ROT.RNG). Thread a
per-game seeded RNG derived from the committed seed through dungeon
generation (reseed the Digger via seedToInt + replace all Math.random) and
monster movement (seededShuffle). Add DungeonGenerator.regenerateFromSeed
and layoutFingerprint; /api/verify now regenerates the dungeon from the
seed and returns a fingerprint a player can independently reproduce. Adds a
determinism test suite.
Full suite: 11 suites / 113 tests passing.
Plan and progress tracked in docs/REMEDIATION_PLAN.md.
Smirk extension is migrating from SHA256 pre-hash to standard RFC 8032
Ed25519 signatures. Accept both formats during transition:
- Try raw message bytes first (new standard)
- Fall back to SHA256(message) (legacy, remove after 2026-07-01)
- Fix wallet balance always showing 'Disconnected' in admin dashboard
- Always attempt balance fetch even if isHealthy=false
- Show error state with message instead of generic 'Disconnected'
- Add POST /api/admin/alerts/test-email endpoint to verify Resend config
- Move alertService to module scope for endpoint access
When both payment modes are enabled (mixed mode) and user has no credits
or confirmed payment, the 'choose_payment' action was falling through to
the error case. Now 'choose_payment' triggers the payment options modal
just like 'make_payment' and 'purchase_credits' do.
- Replace asterisk (*) sparkles with floor tiles + bright golden color
- Remove skull emoji (☠) and death particles (% *) - use red flash effect
- Fix monster shadows causing black boxes - use red floor glow instead
- Replace 'ESCAPED!' emoji text with character-by-character transparent rendering
- Use GameTiles.getPlayerTile() instead of hardcoded '@'
- Use GameTiles.getMonsterTile() instead of hardcoded '~'
All tiles now render with proper transparency on floor tile base.
New dedicated animation while waiting for payment:
- Player runs toward BIG pulsating golden treasure
- Player always grabs the treasure (with dramatic pickup moment)
- Monster pack (4-6 monsters) spawns from multiple directions
- 60% chance: player escapes with treasure to the exit
- 40% chance: player gets devoured by monster pack (skull/death effect)
Animation features:
- Massive golden aura around treasure with sparkle ring
- Player glows gold when carrying treasure
- Death particles expand outward on devour
- Monsters converge from edges (left, right, top, bottom)
Existing 'Awaiting next block' animation preserved unchanged.
- Created SuspendedGameManager to preserve game state when users disconnect
- Games are keyed by DB userId (persistent) instead of ephemeral socketId
- handleDisconnect now suspends games for 5 minutes instead of deleting
- handleConnection restores suspended games on reconnect with session token
- Payment monitoring is restored for pending payments on reconnect
- Added hasActiveMonitoring() method to PaymentHandlers
- Queue entries preserved for users with active sessions/payments
- Updated TODO.md with reconnection feature documentation
Users in dungeon or awaiting payment can now close/refresh browser and
seamlessly resume within 5 minutes when reconnecting.