diff --git a/app/api/routes/rounds.py b/app/api/routes/rounds.py index 69caf80..56ecbb6 100644 --- a/app/api/routes/rounds.py +++ b/app/api/routes/rounds.py @@ -15,7 +15,7 @@ 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.events import EVICTED, RoundEventCapacityError, broadcaster -from app.rounds.service import get_active_round +from app.rounds.service import get_active_round, winner_share router = APIRouter(prefix="/rounds", tags=["rounds"]) @@ -165,7 +165,7 @@ async def current_round( # upper bound by the payout tx's own fee, which is deducted from the winner's # share and isn't knowable until the payout is built — a few hundred sat on a # 1 sat/vB payout, i.e. invisible at PLM amounts, but it is not exact. - jackpot_sats = pool_amount_sats * 70 // 100 + jackpot_sats = winner_share(pool_amount_sats) return CurrentRoundResponse( server_time=datetime.now(timezone.utc).isoformat(), diff --git a/app/rounds/scheduler.py b/app/rounds/scheduler.py index 28f5647..f4953b6 100644 --- a/app/rounds/scheduler.py +++ b/app/rounds/scheduler.py @@ -14,7 +14,7 @@ from app.electrum.scripthash import address_to_scripthash from app.rounds.config import get_round_config from app.rounds.draw import draw_winner, header_hex_to_block_hash from app.rounds.events import broadcaster -from app.rounds.service import open_new_round_if_needed +from app.rounds.service import open_new_round_if_needed, winner_share from app.wallet.hd import derive_pool_key from app.wallet.plm_network import PLM_MAINNET from app.wallet.psbt_builder import InsufficientFundsError, Utxo, build_payout_transaction @@ -338,8 +338,8 @@ class RoundScheduler: await self._log_payout_failure(round_id, winner_user_id, "winner user not found") return - winner_share = pool_amount_sats * 70 // 100 - commission_share = pool_amount_sats - winner_share # remainder from rounding goes to fees + winner_sats = winner_share(pool_amount_sats) + commission_share = pool_amount_sats - winner_sats # remainder from rounding goes to fees # --- Phase 2: build (network read only, no DB write yet) ----------------- try: @@ -358,7 +358,7 @@ class RoundScheduler: from_script=pool_script_obj, utxos=utxos, winner_address=winner_address, - winner_share_sats=winner_share, + winner_share_sats=winner_sats, fee_address=fee_address, commission_sats=commission_share, change_address=pool_address, diff --git a/app/rounds/service.py b/app/rounds/service.py index c8e9c7a..524e39e 100644 --- a/app/rounds/service.py +++ b/app/rounds/service.py @@ -18,6 +18,14 @@ _ACTIVE_STATUSES = ("open", "closing", "drawing", "paying_out") # broken in a way we don't anticipate. _OPEN_ROUND_ATTEMPTS = 3 +# 70% winner / 30% fees. Hardcoded by design (see CLAUDE.md) — changing the split +# is a code change, not an admin-editable setting. Single source of truth so the +# advertised jackpot (rounds.py) and the actual payout (scheduler.py) can't diverge. + + +def winner_share(pool_amount_sats: int) -> int: + return pool_amount_sats * 70 // 100 + async def get_active_round(session: AsyncSession) -> Round | None: """The round currently in progress (in any non-closed state), if any. Rounds