Cap the number of inputs a transaction may spend (B-48)
select_utxos had no ceiling on input count, so an address fragmented into many small deposits built an ever-larger transaction whose fee — deducted from the amount being moved — eroded the bet's share of the pool or the withdrawn amount, and past a few hundred inputs stopped being standard at all. MAX_TX_INPUTS (50) now bounds the selection. Reaching the cap without covering the target is reported as its own "too_many_inputs" code, distinct from having no funds, with the cap carried in the error params for the 7 translations. The payout path records the same distinction in its payout_failed audit reason.
This commit is contained in:
@@ -10,6 +10,7 @@ from app.db.base import Base
|
||||
from app.db.models import AuditLog, PendingTransaction, Round, RoundConfig, RoundParticipant, User, UtxoEvent
|
||||
from app.rounds.service import open_new_round_if_needed
|
||||
from app.wallet.hd import derive_user_address
|
||||
from app.wallet.psbt_builder import MAX_TX_INPUTS
|
||||
|
||||
|
||||
class FakeElectrumClient:
|
||||
@@ -91,6 +92,35 @@ async def test_place_bet_rejects_insufficient_balance(session_factory):
|
||||
await place_bet(session, client, user)
|
||||
|
||||
|
||||
async def test_place_bet_reports_a_too_fragmented_balance_distinctly(session_factory): # B-48
|
||||
# 100 x 0.15 PLM = 15 PLM, plenty for a 10 PLM bet, but the 50 largest inputs
|
||||
# only add up to 7.5 PLM — so the build must fail with its own code, not with
|
||||
# the "you have no funds" one, and must carry the cap for the translation.
|
||||
user_id = await _make_funded_user(session_factory, 20, 15_000_000)
|
||||
async with session_factory() as session:
|
||||
for i in range(99):
|
||||
session.add(
|
||||
UtxoEvent(
|
||||
user_id=user_id,
|
||||
txid=f"{i:064x}",
|
||||
vout=0,
|
||||
amount_sats=15_000_000,
|
||||
confirmed_height=100,
|
||||
)
|
||||
)
|
||||
await session.commit()
|
||||
client = FakeElectrumClient()
|
||||
|
||||
async with session_factory() as session:
|
||||
user = await session.get(User, user_id)
|
||||
with pytest.raises(BetError) as excinfo:
|
||||
await place_bet(session, client, user)
|
||||
|
||||
assert excinfo.value.code == "too_many_inputs"
|
||||
assert excinfo.value.params == {"max_inputs": MAX_TX_INPUTS}
|
||||
assert not client.broadcasted
|
||||
|
||||
|
||||
async def test_place_bet_rejects_second_bet_same_round(session_factory):
|
||||
user_id = await _make_funded_user(session_factory, 2, 3_000_000_000)
|
||||
client = FakeElectrumClient()
|
||||
|
||||
Reference in New Issue
Block a user