From ffd259287de15a7e048ae0164757843b3c55c0ad Mon Sep 17 00:00:00 2001 From: f321x Date: Fri, 13 Mar 2026 17:32:39 +0100 Subject: [PATCH 1/4] qt: SwapServerDialog: resize server list with dialog Waste less space in the dialog by limiting the stretch to 10px and resize the servers_list with the dialog by setting stretch=1 so it can be made larger. Fixes https://github.com/spesmilo/electrum/issues/10519 --- electrum/gui/qt/swap_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 3cdc0843f..0f2d7c06b 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -534,8 +534,8 @@ class SwapServerDialog(WindowModalDialog, QtEventListener): vbox = QVBoxLayout() self.setLayout(vbox) vbox.addWidget(WWLabel(msg)) - vbox.addWidget(self.servers_list) - vbox.addStretch() + vbox.addWidget(self.servers_list, stretch=1) + vbox.addSpacing(10) self.ok_button = OkButton(self) vbox.addLayout(Buttons(CancelButton(self), self.ok_button)) self.setMinimumWidth(650) From 88c7a731c0e83c08628097fab65df2c41c6dce81 Mon Sep 17 00:00:00 2001 From: f321x Date: Fri, 13 Mar 2026 17:37:39 +0100 Subject: [PATCH 2/4] swaps: rm until filter when fetching server pairs The `until` filter would limit the relay to only send us events created up until this timestamp. If the user opens a swap transport by opening the swap dialog, and keeps the dialog open the dialog will naturally age above this limit and the relay will stop sending the client swapserver events as they have (legitimately) been created after this timestamp. As sanity check we still have the comparison against the current timestamp in the event parsing loop to prevent pre/backdating. Fixes https://github.com/spesmilo/electrum/issues/10520 --- electrum/submarine_swaps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 1a761230d..c320a1009 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -1847,7 +1847,6 @@ class NostrTransport(SwapServerTransport): "#d": [f"electrum-swapserver-{self.NOSTR_EVENT_VERSION}"], "#r": [f"net:{constants.net.NET_NAME}"], "since": int(time.time()) - 60 * 60, - "until": int(time.time()) + 60 * 60, } async for event in self.relay_manager.get_events(query, single_event=False, only_stored=False): try: From 022a1bf0e7a45cc733104c19c37178361459a87b Mon Sep 17 00:00:00 2001 From: f321x Date: Fri, 13 Mar 2026 17:59:13 +0100 Subject: [PATCH 3/4] swapserver: cli: skip pending swaps in history commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit skip pending swaps in the swapserver history/summary cli commands. They are not relevant and don't contain all required informations yet. Fixes https://github.com/spesmilo/electrum/issues/10521 Fixes https://github.com/spesmilo/electrum/issues/10525 ```  File "/home/electrum/electrum-fork/electrum/daemon.py", line 268, in handle    response['result'] = await f(*params)                         ^^^^^^^^^^^^^^^^  File "/home/electrum/electrum-fork/electrum/daemon.py", line 381, in run_cmdline    result = await func(*args, **kwargs)             ^^^^^^^^^^^^^^^^^^^^^^^^^^^  File "/home/electrum/electrum-fork/electrum/commands.py", line 207, in func_wrapper  File "/home/electrum/electrum-fork/electrum/commands.py", line 2349, in func_wrapper    group = parser.add_argument_group('network options')                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^  File "/home/electrum/electrum-fork/electrum/plugins/swapserver/__init__.py", line 79, in get_summary    swap_history = await get_history(self)                   ^^^^^^^^^^^^^^^^^^^^^^^  File "/home/electrum/electrum-fork/electrum/commands.py", line 207, in func_wrapper  File "/home/electrum/electrum-fork/electrum/commands.py", line 2349, in func_wrapper    group = parser.add_argument_group('network options')                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^  File "/home/electrum/electrum-fork/electrum/plugins/swapserver/__init__.py", line 60, in get_history    'date': swap['date'].strftime("%Y-%m-%d"),            ^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'NoneType' object has no attribute 'strftime' ``` --- electrum/plugins/swapserver/__init__.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/electrum/plugins/swapserver/__init__.py b/electrum/plugins/swapserver/__init__.py index d8b1934a5..a67041759 100644 --- a/electrum/plugins/swapserver/__init__.py +++ b/electrum/plugins/swapserver/__init__.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, List from electrum.simple_config import ConfigVar, SimpleConfig from electrum.commands import plugin_command +from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED if TYPE_CHECKING: from electrum.commands import Commands @@ -19,7 +20,7 @@ SimpleConfig.SWAPSERVER_ANN_POW_NONCE = ConfigVar('plugins.swapserver.ann_pow_no @plugin_command('wl', plugin_name) async def get_history(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin = None) -> List[dict]: """ - Get a list of all swaps provided by this swapserver. + Get a list of all confirmed swaps provided by this swapserver. Single elements can potentially cover multiple swaps if transactions have been batched. Example result: @@ -42,12 +43,20 @@ async def get_history(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin assert wallet.lnworker, "lightning not available" assert wallet.lnworker.swap_manager, "swap manager not available" - full_history = wallet.get_full_history() - swap_group_ids = set( - x['group_id'] for x in wallet.lnworker.swap_manager.get_groups_for_onchain_history().values() - ) + sm = wallet.lnworker.swap_manager + swap_group_ids = set() + for swap in sm._swaps.values(): + group_id = swap.spending_txid if swap.is_reverse else swap.funding_txid + if group_id is None: + continue + if swap.spending_txid is None \ + or wallet.adb.get_tx_height(swap.spending_txid).height() <= TX_HEIGHT_UNCONFIRMED: + # get only final swaps so the history is stable and doesn't include pending swaps + continue + swap_group_ids.add(group_id) swap_history_items = [] + full_history = wallet.get_full_history() for swap_group_id in swap_group_ids: if swap_history_item := full_history.get('group:' + swap_group_id): swap_history_items.append(swap_history_item) @@ -66,7 +75,7 @@ async def get_history(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin @plugin_command('wl', plugin_name) async def get_summary(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin = None) -> dict: - """Get a summary of all swaps provided by this swapserver. + """Get a summary of all confirmed swaps provided by this swapserver. Can become incorrect if closed lightning channels have been deleted in this wallet. Example result: From f56e6318e3230bd8d73c38e617bde21c887ae324 Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 16 Mar 2026 13:42:53 +0100 Subject: [PATCH 4/4] swaps: make SwapManager.percentage Decimal If SwapManager.percentage was a 0.2 float, rounding differences would cause an exception in the fee calculation inverse sanity check when entering 20 000 sats into the SwapDialog. By making self.percentage a decimal we can prevent this kind of issue. ``` File "/home/user/code/vibecoding_vm/electrum/electrum/gui/qt/swap_dialog.py", line 294, in on_send_edited recv_amount = self.swap_manager.get_recv_amount(send_amount, is_reverse=self.is_reverse) File "/home/user/code/vibecoding_vm/electrum/electrum/submarine_swaps.py", line 1320, in get_recv_amount if abs(send_amount - inverted_send_amount) > 1: ~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~ TypeError: unsupported operand type(s) for -: 'int' and 'NoneType' ``` --- electrum/commands.py | 2 +- electrum/gui/qml/qeswaphelper.py | 2 +- electrum/plugins/swapserver/server.py | 2 +- electrum/submarine_swaps.py | 16 ++++++++-------- tests/test_commands.py | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/electrum/commands.py b/electrum/commands.py index 9ff0eaa60..419d1c4b5 100644 --- a/electrum/commands.py +++ b/electrum/commands.py @@ -2016,7 +2016,7 @@ class Commands(Logger): result = {} for offer in offers: result[offer.server_npub] = { - "percentage_fee": offer.pairs.percentage, + "percentage_fee": float(offer.pairs.percentage), "max_forward_sat": offer.pairs.max_forward, "max_reverse_sat": offer.pairs.max_reverse, "min_amount_sat": offer.pairs.min_amount, diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 984befb27..7475216c4 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -75,7 +75,7 @@ class QESwapServerNPubListModel(QAbstractListModel): return { 'npub': x.server_npub, 'server_pubkey': x.server_pubkey, - 'percentage_fee': x.pairs.percentage, + 'percentage_fee': float(x.pairs.percentage), 'mining_fee': x.pairs.mining_fee, 'min_amount': x.pairs.min_amount, 'max_forward_amount': x.pairs.max_forward, diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index b2163736e..7d645e7e8 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -95,7 +95,7 @@ class HttpSwapServer(Logger, EventListener): "minimal": sm._min_amount, }, "fees": { - "percentage": sm.percentage, + "percentage": float(sm.percentage), # cast to float for <= 4.7.1 backwards compatibility "minerFees": { "baseAsset": { "normal": sm.mining_fee, diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index c320a1009..ef55aa632 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -168,7 +168,7 @@ def now(): @attr.s(frozen=True) class SwapFees: - percentage = attr.ib(type=int) + percentage = attr.ib(type=Decimal) mining_fee = attr.ib(type=int) min_amount = attr.ib(type=int) max_forward = attr.ib(type=int) @@ -235,7 +235,7 @@ class SwapManager(Logger): def __init__(self, *, wallet: 'Abstract_Wallet', lnworker: 'LNWallet'): Logger.__init__(self) self.mining_fee = None - self.percentage = None + self.percentage = None # type: Optional[Decimal] self._min_amount = None self._max_forward = None self._max_reverse = None @@ -1193,7 +1193,7 @@ class SwapManager(Logger): def server_update_pairs(self) -> None: """ for server """ - self.percentage = float(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000 # type: ignore + self.percentage = Decimal(self.config.SWAPSERVER_FEE_MILLIONTHS) / 10000 # type: ignore self._min_amount = MIN_SWAP_AMOUNT_SAT oc_balance_sat: int = self.wallet.get_spendable_balance_sat() max_forward: int = min(int(self.lnworker.num_sats_can_receive()), oc_balance_sat, 10000000) @@ -1259,7 +1259,7 @@ class SwapManager(Logger): if send_amount is None: return None x = Decimal(send_amount) - percentage = Decimal(self.percentage) + percentage = self.percentage if is_reverse: if not self.check_invoice_amount(x, is_reverse): return None @@ -1289,7 +1289,7 @@ class SwapManager(Logger): if not recv_amount: return None x = Decimal(recv_amount) - percentage = Decimal(self.percentage) + percentage = self.percentage if is_reverse: # see/ref: # https://github.com/BoltzExchange/boltz-backend/blob/e7e2d30f42a5bea3665b164feb85f84c64d86658/lib/service/Service.ts#L928 @@ -1638,7 +1638,7 @@ class HttpTransport(SwapServerTransport): fees = response['pairs']['BTC/BTC']['fees'] limits = response['pairs']['BTC/BTC']['limits'] pairs = SwapFees( - percentage=fees['percentage'], + percentage=Decimal(str(fees['percentage'])), mining_fee=fees['minerFees']['baseAsset']['mining_fee'], min_amount=limits['minimal'], max_forward=limits['max_forward_amount'], @@ -1777,7 +1777,7 @@ class NostrTransport(SwapServerTransport): self.logger.warning(f"not publishing swap offer, no liquidity available: {sm._max_forward=}, {sm._max_reverse=}") return offer = { - 'percentage_fee': sm.percentage, + 'percentage_fee': float(sm.percentage), # cast to float for <= 4.7.1 backwards compatibility 'mining_fee': sm.mining_fee, 'min_amount': sm._min_amount, 'max_forward_amount': sm._max_forward, @@ -1883,7 +1883,7 @@ class NostrTransport(SwapServerTransport): continue try: pairs = SwapFees( - percentage=content['percentage_fee'], + percentage=Decimal(str(content['percentage_fee'])), mining_fee=content['mining_fee'], min_amount=content['min_amount'], max_forward=content['max_forward_amount'], diff --git a/tests/test_commands.py b/tests/test_commands.py index dddf293fd..69b833b7a 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -678,7 +678,7 @@ class TestCommandsTestnet(ElectrumTestCase): offer1 = SwapOffer( pairs=SwapFees( - percentage=0.5, + percentage=Decimal('0.5'), mining_fee=2000, min_amount=10000, max_forward=1000000, @@ -692,7 +692,7 @@ class TestCommandsTestnet(ElectrumTestCase): offer2 = SwapOffer( pairs=SwapFees( - percentage=1.0, + percentage=Decimal('1.0'), mining_fee=3000, min_amount=20000, max_forward=2000000,