Select coins until the change output is guaranteed (B-98, BET-24)
select_utxos took the first coin covering the amount, so 10 + 1 PLM and a 10 PLM bet was refused with balance_leaves_no_change and a 10 PLM withdrawal was reduced. User txs now ask for target + DUST_LIMIT_SATS; the old outcome is kept only when no allowed combination leaves change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
264441f5d6
commit
32df50cb75
7 files changed
+77
-34
No files matched your search
@@ -279,3 +279,55 @@ def test_dust_sized_recipient_amount_is_refused():
|
||||
change_address=change_address,
|
||||
fee_rate_sat_vb=1,
|
||||
)
|
||||
|
||||
|
||||
# --- B-98 (BET-24): selection leaves a change output whenever the coins allow it ---
|
||||
|
||||
|
||||
def _built(utxos, amount, reduce=False):
|
||||
signer = _key(7)
|
||||
from_script = script.p2wpkh(signer.to_public())
|
||||
return build_signed_transaction(
|
||||
signing_key=signer,
|
||||
from_script=from_script,
|
||||
utxos=utxos,
|
||||
to_address=script.p2wpkh(_key(8).to_public()).address(network=PLM_MAINNET),
|
||||
amount_sats=amount,
|
||||
change_address=from_script.address(network=PLM_MAINNET),
|
||||
fee_rate_sat_vb=1,
|
||||
reduce_amount_to_keep_change=reduce,
|
||||
)
|
||||
|
||||
|
||||
def test_a_bet_takes_a_second_coin_when_the_first_covers_it_exactly():
|
||||
"""BET-24: 10 + 1 PLM and a 10 PLM bet — both coins, 1 PLM change, no refusal."""
|
||||
utxos = [Utxo("aa" * 32, 0, 1_000_000_000), Utxo("bb" * 32, 0, 100_000_000)]
|
||||
built = _built(utxos, 1_000_000_000)
|
||||
assert built.change_sats == 100_000_000
|
||||
assert len(Transaction.parse(bytes.fromhex(built.raw_hex)).vin) == 2
|
||||
|
||||
|
||||
def test_a_withdrawal_uses_the_second_coin_instead_of_being_reduced():
|
||||
"""BET-24 / WDR-09: the reduction only happens when no combination leaves change."""
|
||||
utxos = [Utxo("aa" * 32, 0, 1_000_000_000), Utxo("bb" * 32, 0, 100_000_000)]
|
||||
built = _built(utxos, 1_000_000_000, reduce=True)
|
||||
assert built.change_sats == 100_000_000
|
||||
assert built.recipient_sats == 1_000_000_000 - built.fee_sats
|
||||
|
||||
|
||||
def test_with_no_coin_to_spare_the_old_outcome_stands():
|
||||
"""BET-24: a lone coin equal to the bet is still refused / the withdrawal still reduced."""
|
||||
from app.wallet.psbt_builder import DUST_LIMIT_SATS
|
||||
|
||||
utxos = [Utxo("aa" * 32, 0, 1_000_000_000)]
|
||||
with pytest.raises(InsufficientFundsError) as excinfo:
|
||||
_built(utxos, 1_000_000_000)
|
||||
assert excinfo.value.code == "balance_leaves_no_change"
|
||||
assert _built(utxos, 1_000_000_000, reduce=True).change_sats == DUST_LIMIT_SATS
|
||||
|
||||
|
||||
def test_the_change_target_never_pushes_past_the_input_cap():
|
||||
"""BET-24 / BET-08: capped at MAX_TX_INPUTS, a total that covers the amount is kept."""
|
||||
utxos = [Utxo(f"{i:064x}", 0, 100_000) for i in range(200)]
|
||||
selected, total = select_utxos(utxos, 100_000 * MAX_TX_INPUTS, min_change_sats=294)
|
||||
assert len(selected) == MAX_TX_INPUTS and total == 100_000 * MAX_TX_INPUTS
|
||||
Reference in new issue
Block a user