Skip to content

cli/swaps: fast fail swap cli on changed fees - #10840

Open
f321x wants to merge 2 commits into
spesmilo:masterfrom
f321x:swap_cli_fail_before_req
Open

cli/swaps: fast fail swap cli on changed fees#10840
f321x wants to merge 2 commits into
spesmilo:masterfrom
f321x:swap_cli_fail_before_req

Conversation

@f321x

@f321x f321x commented Aug 10, 2026

Copy link
Copy Markdown
Member

CLI swaps happen in two steps.
First the command is ran as "dryrun" to calculate the expected amounts, then the swap is initiated with these amounts.

If the mining fee estimates increase during the time between those two calls, the swap provider might return an unexpected amount for us in their response, causing the response checks in SwapManager to fail a swap after a the network round trip with the swap provider.

Instead we should fail early before the swap is registered with the provider, to avoid stale swaps in the servers db and give the CLI user a clear exception.
Note: the issue can still happen if the fees change in the brief moment during the request, so scripts consuming the CLI should handle a failure either way.

Also fixes a bug in that the onchain_amount returned by the swap branch of the reverse_swap command was inconsistent with the value returned by the dryrun branch (it included the claim_fee).

Closes #10826

Comment thread electrum/commands.py
Comment on lines +2164 to +2166
raise UserFacingException(
_("Swap fees have changed since the dryrun was calculated. Do a new dryrun first.")
+ f" ({required_onchain_amount_sat} != {onchain_amount_sat} sat)")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# Note: the CLI is intentionally always non-localized.

(though we maybe should re-state that at the top of commands.py)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.
I added a commit 4368018 which breaks the _() function call in commands.py to enforce this programmatically.

@SomberNight SomberNight left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

52a961f is mostly ok. some nits.

Comment thread electrum/commands.py
required_onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)
# same 1 sat rounding tolerance as in `request_normal_swap()`
if not required_onchain_amount_sat \
or not onchain_amount_sat - 1 <= required_onchain_amount_sat <= onchain_amount_sat:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use parens here? There is so much operator precedence stuff going on in this if, it's hard to imagine the AST in my head.

Suggested change
or not onchain_amount_sat - 1 <= required_onchain_amount_sat <= onchain_amount_sat:
or not (onchain_amount_sat - 1 <= required_onchain_amount_sat <= onchain_amount_sat):

it's a small nit, but I also mean in general in the codebase :)
Too many layers of parens makes it hard to read but too few makes it easy to make a mistake in operator precedence. Delicate tradeoff :P

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 89b10fe. I see your point.

Comment thread electrum/commands.py
Comment on lines +2217 to +2222
funding_utxo_value_sat = requested_recv_amount_sat + claim_fee
onchain_recv_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
if not onchain_recv_amount_sat or onchain_recv_amount_sat < requested_recv_amount_sat:
raise UserFacingException(
_("Swap fees have changed since the dryrun was calculated. Do a new dryrun first.")
+ f" ({onchain_recv_amount_sat} < {requested_recv_amount_sat} sat)")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I understand correctly that this is the "same" check as in reverse_swap()?

# check that the onchain amount is what we expected
if onchain_amount < expected_onchain_amount_sat:
raise Exception(f"rswap check failed: onchain_amount is less than what we expected: "
f"{onchain_amount} < {expected_onchain_amount_sat}")

Except there in sswaps.py, both amounts being compared are offset by +claim_fee ?
Uff took me like 10 minutes to untangle that >.<

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's a similar check, it just re-calculates the amount with the latest known fees client side to reduce the time window for disagreements and potentially prevent a useless request to the swap provider as the other check only happens after we got the servers response.

f321x added 2 commits August 17, 2026 10:49
CLI swaps happen in two steps. First the command is ran as "dryrun"
to calculate the expected amounts (considering the feerates), then
the swap is initiated with these amounts. If the mining fee estimates
increase during the time between those two calls the swap provider
might return a lower onchain amount for us, causing the
`if onchain_amount < expected_onchain_amount_sat` check in
`SwapManager` to fail the swap after a the network
round trip with the swap provider. Instead we should fail early
before the swap is registered with the provider.
Note: the issue can still happen if the fees change in the brief
moment during the request, so scripts consuming the CLI should
handle failures either way.
The CLI is intentionally not translated, break the `_()` import
to prevent accidental usage.
@f321x
f321x force-pushed the swap_cli_fail_before_req branch from 52a961f to 4368018 Compare August 17, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Electrum Swap] Claim fee calculation problem

2 participants