diff --git a/electrum/commands.py b/electrum/commands.py index 2a004db77..73f8ce3f0 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -1975,7 +1975,7 @@ class Commands(Logger): arg:decimal_or_dryrun:onchain_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value """ sm = wallet.lnworker.swap_manager - with sm.create_transport() as transport: + async with sm.create_transport() as transport: await sm.is_initialized.wait() if lightning_amount == 'dryrun': onchain_amount_sat = satoshis(onchain_amount) @@ -2002,37 +2002,46 @@ class Commands(Logger): } @command('wnpl') - async def reverse_swap(self, lightning_amount, onchain_amount, password=None, wallet: Abstract_Wallet = None): + async def reverse_swap(self, lightning_amount, onchain_amount, provider_mining_fee, password=None, wallet: Abstract_Wallet = None): """ Reverse submarine swap: send on Lightning, receive on-chain arg:decimal_or_dryrun:lightning_amount:Amount to be sent, in BTC. Set it to 'dryrun' to receive a value arg:decimal_or_dryrun:onchain_amount:Amount to be received, in BTC. Set it to 'dryrun' to receive a value + arg:decimal_or_dryrun:provider_mining_fee:Mining fee required by the swap provider, in BTC. Set it to 'dryrun' to receive a value """ sm = wallet.lnworker.swap_manager - with sm.create_transport() as transport: + async with sm.create_transport() as transport: await sm.is_initialized.wait() if onchain_amount == 'dryrun': lightning_amount_sat = satoshis(lightning_amount) onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True) + assert provider_mining_fee == "dryrun", f"Cannot use {provider_mining_fee=} in dryrun. Set it to 'dryrun'." + provider_mining_fee = sm.mining_fee funding_txid = None elif lightning_amount == 'dryrun': onchain_amount_sat = satoshis(onchain_amount) lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True) + assert provider_mining_fee == "dryrun", f"Cannot use {provider_mining_fee=} in dryrun. Set it to 'dryrun'." + provider_mining_fee = sm.mining_fee funding_txid = None else: lightning_amount_sat = satoshis(lightning_amount) claim_fee = sm.get_fee_for_txbatcher() onchain_amount_sat = satoshis(onchain_amount) + claim_fee + assert provider_mining_fee != "dryrun", "Provide the 'provder_mining_fee' obtained from the dryrun." + provider_mining_fee = satoshis(provider_mining_fee) funding_txid = await wallet.lnworker.swap_manager.reverse_swap( transport=transport, lightning_amount_sat=lightning_amount_sat, expected_onchain_amount_sat=onchain_amount_sat, + server_mining_fee_sat=provider_mining_fee, ) return { 'funding_txid': funding_txid, 'lightning_amount': format_satoshis(lightning_amount_sat), 'onchain_amount': format_satoshis(onchain_amount_sat), + 'provider_mining_fee': format_satoshis(provider_mining_fee) } @command('n') diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 80dede72b..008442633 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -719,6 +719,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): transport=self.swap_transport, lightning_amount_sat=lightning_amount, expected_onchain_amount_sat=onchain_amount + swap_manager.get_fee_for_txbatcher(), + server_mining_fee_sat=self.serverMiningfee.satsInt, ) try: # swaphelper might be destroyed at this point if txid: diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index fbf1c8e2c..012028478 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -94,6 +94,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): self.swap_limits_label = QLabel() self.fee_label = QLabel() self.server_fee_label = QLabel() + self.last_server_mining_fee_sat = None h = QGridLayout() h.addWidget(self.description_label, 0, 0, 1, 3) h.addWidget(self.toggle_button, 0, 3) @@ -153,7 +154,12 @@ class SwapDialog(WindowModalDialog, QtEventListener): @qt_event_listener def on_event_swap_offers_changed(self, recent_offers: Sequence['SwapOffer']): self.set_server_button_text(len(recent_offers)) - self.update() + if not self.ok_button.isEnabled(): + # only update the dialog with the new offer if the user hasn't entered an amount yet. + # if the user has already entered an amount we prefer the swap to fail due to outdated + # fees than the possibility of a swap happening with fees the user hasn't seen + # due to an update happening just before the user initiated the swap + self.update() def set_server_button_text(self, offer_count: int): button_text = f' {offer_count} ' + (_('providers') if offer_count != 1 else _('provider')) @@ -282,8 +288,8 @@ class SwapDialog(WindowModalDialog, QtEventListener): f"{self.window.format_amount(max_swap_limit)} {w_base_unit}") self.swap_limits_label.setText(swap_limit_str) self.swap_limits_label.repaint() # macOS hack for #6269 - server_mining_fee = sm.mining_fee - server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(server_mining_fee) + ' ' + w_base_unit + self.last_server_mining_fee_sat = sm.mining_fee + server_fee_str = '%.2f'%sm.percentage + '% + ' + self.window.format_amount(sm.mining_fee) + ' ' + w_base_unit self.server_fee_label.setText(server_fee_str) self.server_fee_label.repaint() # macOS hack for #6269 self.needs_tx_update = True @@ -332,6 +338,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): transport=transport, lightning_amount_sat=lightning_amount, expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_fee_for_txbatcher(), + server_mining_fee_sat=self.last_server_mining_fee_sat, ) try: # we must not leave the context, so we use run_couroutine_dialog diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index fcd6cd66f..97561d596 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -904,6 +904,7 @@ class SwapManager(Logger): transport: 'SwapServerTransport', lightning_amount_sat: int, expected_onchain_amount_sat: int, + server_mining_fee_sat: int, channels: Optional[Sequence['Channel']] = None, ) -> Optional[str]: """send on Lightning, receive on-chain @@ -920,6 +921,9 @@ class SwapManager(Logger): - Server fulfills HTLC using preimage. Note: expected_onchain_amount_sat is BEFORE deducting the on-chain claim tx fee. + Note: server_mining_fee_sat is passed as argument instead of accessing self.mining_fee to ensure + the mining fees the user sees in the GUI are also the values used for the checks performed here. + We commit to server_mining_fee_sat as it limits the max fee pre-payment amt, which the server is trusted with. """ assert self.network assert self.lnwatcher @@ -977,16 +981,16 @@ class SwapManager(Logger): invoice_amount = int(lnaddr.get_amount_sat()) if lnaddr.paymenthash != payment_hash: raise Exception("rswap check failed: inconsistent RHASH and invoice") - # check that the lightning amount is what we requested if fee_invoice: fee_lnaddr = self.lnworker._check_bolt11_invoice(fee_invoice) - if fee_lnaddr.get_amount_sat() > self.mining_fee * 2: + if fee_lnaddr.get_amount_sat() > server_mining_fee_sat * 2: raise SwapServerError(_("Mining fee requested by swap-server larger " "than what was announced in their offer.")) invoice_amount += fee_lnaddr.get_amount_sat() prepay_hash = fee_lnaddr.paymenthash else: prepay_hash = None + # check that the lightning amount is what we requested if int(invoice_amount) != lightning_amount_sat: raise Exception(f"rswap check failed: invoice_amount ({invoice_amount}) " f"not what we requested ({lightning_amount_sat})")