Deduplicate the 70/30 prize split formula
pool_amount_sats * 70 // 100 was hardcoded identically in both rounds/scheduler.py (the actual payout) and api/routes/rounds.py (the advertised jackpot). They happened to agree, but nothing enforced it — changing one without the other would have made GET /rounds/current's jackpot silently diverge from the real payout. Extract winner_share() into rounds/service.py as the single source of truth. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@ from app.db.models import RoundParticipant, User
|
|||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
from app.rounds.config import get_round_config
|
from app.rounds.config import get_round_config
|
||||||
from app.rounds.events import EVICTED, RoundEventCapacityError, broadcaster
|
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"])
|
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
|
# 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
|
# 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.
|
# 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(
|
return CurrentRoundResponse(
|
||||||
server_time=datetime.now(timezone.utc).isoformat(),
|
server_time=datetime.now(timezone.utc).isoformat(),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from app.electrum.scripthash import address_to_scripthash
|
|||||||
from app.rounds.config import get_round_config
|
from app.rounds.config import get_round_config
|
||||||
from app.rounds.draw import draw_winner, header_hex_to_block_hash
|
from app.rounds.draw import draw_winner, header_hex_to_block_hash
|
||||||
from app.rounds.events import broadcaster
|
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.hd import derive_pool_key
|
||||||
from app.wallet.plm_network import PLM_MAINNET
|
from app.wallet.plm_network import PLM_MAINNET
|
||||||
from app.wallet.psbt_builder import InsufficientFundsError, Utxo, build_payout_transaction
|
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")
|
await self._log_payout_failure(round_id, winner_user_id, "winner user not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
winner_share = pool_amount_sats * 70 // 100
|
winner_sats = winner_share(pool_amount_sats)
|
||||||
commission_share = pool_amount_sats - winner_share # remainder from rounding goes to fees
|
commission_share = pool_amount_sats - winner_sats # remainder from rounding goes to fees
|
||||||
|
|
||||||
# --- Phase 2: build (network read only, no DB write yet) -----------------
|
# --- Phase 2: build (network read only, no DB write yet) -----------------
|
||||||
try:
|
try:
|
||||||
@@ -358,7 +358,7 @@ class RoundScheduler:
|
|||||||
from_script=pool_script_obj,
|
from_script=pool_script_obj,
|
||||||
utxos=utxos,
|
utxos=utxos,
|
||||||
winner_address=winner_address,
|
winner_address=winner_address,
|
||||||
winner_share_sats=winner_share,
|
winner_share_sats=winner_sats,
|
||||||
fee_address=fee_address,
|
fee_address=fee_address,
|
||||||
commission_sats=commission_share,
|
commission_sats=commission_share,
|
||||||
change_address=pool_address,
|
change_address=pool_address,
|
||||||
|
|||||||
@@ -18,6 +18,14 @@ _ACTIVE_STATUSES = ("open", "closing", "drawing", "paying_out")
|
|||||||
# broken in a way we don't anticipate.
|
# broken in a way we don't anticipate.
|
||||||
_OPEN_ROUND_ATTEMPTS = 3
|
_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:
|
async def get_active_round(session: AsyncSession) -> Round | None:
|
||||||
"""The round currently in progress (in any non-closed state), if any. Rounds
|
"""The round currently in progress (in any non-closed state), if any. Rounds
|
||||||
|
|||||||
Reference in New Issue
Block a user