Always keep a change output, so every tx stays fee-bumpable (B-62)
The max-amount checkbox sends amount_sats == the whole confirmed balance, so change came out at 0, the change output was dropped, and the transaction had a single output. bump_fee has nothing to shrink there: it raised RbfError every 30s until the reconciler abandoned the row six hours later. The RBF single-change-output limitation was a documented gap, but the UI made it the *default* withdrawal path. The extra-input fallback would not have helped this case: a transaction moving the entire balance already spends every UTXO the sender has. So the fix is at build time — build_signed_transaction never produces a change output below DUST_LIMIT_SATS, and never folds it into the fee either: - withdrawals pass reduce_amount_to_keep_change=True and move a dust limit less. The fee already comes out of the withdrawn amount by design, so this is the same rule applied a little harder, and Withdrawal.amount_requested_sats vs amount_sent_sats already existed to record the difference. - bets don't: the bet is a fixed price that can't be quietly reduced. A balance exactly equal to the bet is refused with balance_leaves_no_change (translated into all 7 languages, carrying required_extra_sats), which turns "a user's balance must never exactly equal the bet" from a documented assumption into an enforced one — and stops an unbumpable bet from holding a round open until the reconciler gives up on it. bump_fee's no-change guard stays: a single-output tx broadcast before this change can still be pending across the deploy, and it must fail loudly rather than start shrinking a recipient's output. Its test now hand-builds that shape, precisely because the builder no longer will. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,13 @@ async def request_withdrawal(
|
||||
amount_sats=amount_sats,
|
||||
change_address=user.address,
|
||||
fee_rate_sat_vb=config.fee_rate_sat_vb,
|
||||
# B-62: "withdraw everything" asks for the whole confirmed balance, which
|
||||
# would leave no change output and therefore nothing bump_fee could
|
||||
# shrink — the one tx shape RBF cannot rescue, and the UI's default
|
||||
# withdrawal path at that. Move a dust limit less instead of producing an
|
||||
# unbumpable transaction; amount_sent_sats below records what actually
|
||||
# went out, which is already how a fee-deducted withdrawal is reported.
|
||||
reduce_amount_to_keep_change=True,
|
||||
)
|
||||
except InsufficientFundsError as exc:
|
||||
raise WithdrawalError(exc.code, str(exc), **exc.params) from exc
|
||||
|
||||
Reference in New Issue
Block a user