From 0c0fb8360ecd4b73e45337d581a6aa9a3b844761 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Fri, 24 Apr 2026 21:02:37 +0200 Subject: [PATCH 01/10] Fix: Segfault + log spam and various fixes. --- basicswap/interface/btc.py | 5 +- basicswap/interface/electrumx.py | 156 +++++++++++++++++++++---------- basicswap/js_server.py | 5 +- basicswap/static/js/global.js | 19 ++++ basicswap/ui/page_wallet.py | 5 +- 5 files changed, 137 insertions(+), 53 deletions(-) diff --git a/basicswap/interface/btc.py b/basicswap/interface/btc.py index 1afd6b7..c1cbd8a 100644 --- a/basicswap/interface/btc.py +++ b/basicswap/interface/btc.py @@ -3099,7 +3099,10 @@ class BTCInterface(Secp256k1Interface): } except Exception as e: error_msg = str(e).lower() - if "no such mempool or blockchain transaction" not in error_msg: + if ( + "no such mempool or blockchain transaction" not in error_msg + and "missing transaction" not in error_msg + ): self._log.debug( f"checkWatchedOutput exception for {txid_hex}:{vout}: {e}" ) diff --git a/basicswap/interface/electrumx.py b/basicswap/interface/electrumx.py index b8d27db..f7f83d3 100644 --- a/basicswap/interface/electrumx.py +++ b/basicswap/interface/electrumx.py @@ -82,9 +82,24 @@ class ElectrumConnection: self._proxy_host = proxy_host self._proxy_port = proxy_port + @staticmethod + def _is_private_address(host: str) -> bool: + try: + import ipaddress + + addr = ipaddress.ip_address(host) + return addr.is_private or addr.is_loopback or addr.is_link_local + except ValueError: + return host == "localhost" + def connect(self): try: - if self._proxy_host and self._proxy_port: + use_proxy = ( + self._proxy_host + and self._proxy_port + and not self._is_private_address(self._host) + ) + if use_proxy: import socks sock = socks.socksocket() @@ -101,6 +116,10 @@ class ElectrumConnection: sock = socket.create_connection( (self._host, self._port), timeout=self._timeout ) + if self._log and self._proxy_host and self._proxy_port: + self._log.debug( + f"Electrum connecting directly to LAN server {self._host}:{self._port} (bypassing proxy)" + ) if self._use_ssl: context = ssl.create_default_context() context.check_hostname = False @@ -546,11 +565,6 @@ class ElectrumServer: elif isinstance(srv, dict): user_onion.append(srv) - final_clearnet = ( - user_clearnet - if user_clearnet - else DEFAULT_ELECTRUM_SERVERS.get(coin_name, []) - ) final_onion = ( user_onion if user_onion else DEFAULT_ONION_SERVERS.get(coin_name, []) ) @@ -558,13 +572,26 @@ class ElectrumServer: self._using_default_servers = not user_clearnet and not user_onion if use_tor: + if user_onion and not user_clearnet: + final_clearnet = [] + else: + final_clearnet = ( + user_clearnet + if user_clearnet + else DEFAULT_ELECTRUM_SERVERS.get(coin_name, []) + ) self._servers = list(final_onion) + list(final_clearnet) - if self._log and final_onion: + if self._log: self._log.info( f"ElectrumServer {coin_name}: TOR enabled - " f"{len(final_onion)} .onion + {len(final_clearnet)} clearnet servers" ) else: + final_clearnet = ( + user_clearnet + if user_clearnet + else DEFAULT_ELECTRUM_SERVERS.get(coin_name, []) + ) self._servers = list(final_clearnet) if self._log: self._log.info( @@ -983,55 +1010,84 @@ class ElectrumServer: def call_background(self, method, params=None, timeout=20): if self._stopping: raise TemporaryError("Electrum server is shutting down") - conn = self._connection - if conn is None or not conn.is_connected(): - if self._stopping: - raise TemporaryError("Electrum server is shutting down") - try: - self.connect() - conn = self._connection - except Exception: - raise TemporaryError("Electrum call failed: no connection") - if conn is None or not conn.is_connected(): - raise TemporaryError("Electrum call failed: no connection") + lock_acquired = self._lock.acquire(timeout=timeout + 5) + if not lock_acquired: + raise TemporaryError( + f"Electrum background call timed out waiting for lock: {method}" + ) try: - result = conn.call(method, params, timeout=timeout) - self._last_activity = time.time() - return result - except TemporaryError as e: - if self._stopping: - raise TemporaryError("Electrum server is shutting down") - if "timed out" in str(e).lower(): - self._record_timeout() - raise + for attempt in range(2): + if self._stopping: + raise TemporaryError("Electrum server is shutting down") + if self._connection is None or not self._connection.is_connected(): + self.connect() + if self._connection is None: + raise TemporaryError("Electrum call failed: no connection") + try: + result = self._connection.call(method, params, timeout=timeout) + self._last_activity = time.time() + return result + except TemporaryError as e: + if self._stopping: + raise TemporaryError("Electrum server is shutting down") + if "timed out" in str(e).lower(): + self._record_timeout() + if attempt == 0: + self._retry_on_failure() + else: + raise + except Exception as e: + if self._is_rate_limit_error(str(e)): + server = self._get_server(self._current_server_idx) + self._blacklist_server(server, str(e)) + if attempt == 0: + self._retry_on_failure() + else: + raise + finally: + self._lock.release() def call_batch_background(self, requests, timeout=30): if self._stopping: raise TemporaryError("Electrum server is shutting down") - conn = self._connection - if conn is None or not conn.is_connected(): - if self._stopping: - raise TemporaryError("Electrum server is shutting down") - self._record_timeout() - conn = self._connection - if conn is None or not conn.is_connected(): - try: - self.connect() - conn = self._connection - except Exception: - raise TemporaryError("Electrum batch call failed: no connection") - if conn is None or not conn.is_connected(): - raise TemporaryError("Electrum batch call failed: no connection") + lock_acquired = self._lock.acquire(timeout=timeout + 5) + if not lock_acquired: + raise TemporaryError( + "Electrum background batch call timed out waiting for lock" + ) try: - result = conn.call_batch(requests) - self._last_activity = time.time() - return result - except TemporaryError as e: - if self._stopping: - raise TemporaryError("Electrum server is shutting down") - if "timed out" in str(e).lower(): - self._record_timeout() - raise + for attempt in range(2): + if self._stopping: + raise TemporaryError("Electrum server is shutting down") + if self._connection is None or not self._connection.is_connected(): + self.connect() + if self._connection is None: + raise TemporaryError( + "Electrum batch call failed: no connection" + ) + try: + result = self._connection.call_batch(requests) + self._last_activity = time.time() + return result + except TemporaryError as e: + if self._stopping: + raise TemporaryError("Electrum server is shutting down") + if "timed out" in str(e).lower(): + self._record_timeout() + if attempt == 0: + self._retry_on_failure() + else: + raise + except Exception as e: + if self._is_rate_limit_error(str(e)): + server = self._get_server(self._current_server_idx) + self._blacklist_server(server, str(e)) + if attempt == 0: + self._retry_on_failure() + else: + raise + finally: + self._lock.release() def call_user(self, method, params=None, timeout=10): if self._stopping: diff --git a/basicswap/js_server.py b/basicswap/js_server.py index e54c4c4..b927a7f 100644 --- a/basicswap/js_server.py +++ b/basicswap/js_server.py @@ -1631,7 +1631,10 @@ def js_wallettransactions(self, url_split, post_string, is_json) -> bytes: or (current_time - cache_entry["time"]) > TX_CACHE_DURATION ): all_txs = ci.listWalletTransactions(count=10000, skip=0) - all_txs = list(reversed(all_txs)) if all_txs else [] + if all_txs and coin_id not in (Coins.XMR, Coins.WOW): + all_txs = list(reversed(all_txs)) + elif not all_txs: + all_txs = [] swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time} else: all_txs = cache_entry["txs"] diff --git a/basicswap/static/js/global.js b/basicswap/static/js/global.js index f6c24e2..b34b957 100644 --- a/basicswap/static/js/global.js +++ b/basicswap/static/js/global.js @@ -1,3 +1,22 @@ +(function() { + const originalFetch = window.fetch; + window.fetch = function(url, options) { + return originalFetch.apply(this, arguments).then(function(response) { + if (response.status === 401) { + const urlStr = typeof url === 'string' ? url : (url && url.url) || ''; + if (urlStr.startsWith('/json/') || urlStr.startsWith('/json')) { + window.location.href = '/login'; + return new Response(JSON.stringify({error: 'Session expired'}), { + status: 401, + headers: {'Content-Type': 'application/json'} + }); + } + } + return response; + }); + }; +})(); + document.addEventListener('DOMContentLoaded', function() { const burger = document.querySelectorAll('.navbar-burger'); const menu = document.querySelectorAll('.navbar-menu'); diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py index 2d669ce..8b4ff4f 100644 --- a/basicswap/ui/page_wallet.py +++ b/basicswap/ui/page_wallet.py @@ -559,7 +559,10 @@ def page_wallet(self, url_split, post_string): skip = tx_filters.get("offset", 0) all_txs = ci.listWalletTransactions(count=10000, skip=0) - all_txs = list(reversed(all_txs)) if all_txs else [] + if all_txs and coin_id not in (Coins.XMR, Coins.WOW): + all_txs = list(reversed(all_txs)) + elif not all_txs: + all_txs = [] total_transactions = len(all_txs) raw_txs = all_txs[skip : skip + count] if all_txs else [] From 0dc5284e51206060629a11500e1e944ed1488755 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Sat, 25 Apr 2026 19:45:27 +0200 Subject: [PATCH 02/10] Fix: Waiting on Electrum Server + re-check the seed after error. --- basicswap/basicswap.py | 17 ++++++++++++----- basicswap/interface/xmr.py | 1 + basicswap/wallet_backend.py | 4 +++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 581c4fc..ac59cea 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1651,11 +1651,14 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): for c in check_coins: ci = self.ci(c) if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed(): - raise ValueError( - '{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format( - ci.coin_name() + if not ci._have_checked_seed: + self.checkWalletSeed(c) + if not ci.knownWalletSeed(): + raise ValueError( + '{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format( + ci.coin_name() + ) ) - ) if self.coin_clients[c]["connection_type"] not in ("rpc", "electrum"): continue if c in (Coins.XMR, Coins.WOW): @@ -13896,7 +13899,11 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): "unconfirmed": ci.format_amount( walletinfo["unconfirmed_balance"], conv_int=True ), - "expected_seed": ci.knownWalletSeed(), + "expected_seed": ( + ci.knownWalletSeed() + if ci._have_checked_seed + else self.checkWalletSeed(coin) + ), "encrypted": walletinfo["encrypted"], "locked": walletinfo["locked"], "connection_type": self.coin_clients[coin].get( diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 1b34be2..7517777 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -257,6 +257,7 @@ class XMRInterface(CoinInterface): ) raise os.rename(walletpath, bkp_path) + self._have_checked_seed = False # Drop through to open_wallet else: raise diff --git a/basicswap/wallet_backend.py b/basicswap/wallet_backend.py index feda902..f876b50 100644 --- a/basicswap/wallet_backend.py +++ b/basicswap/wallet_backend.py @@ -810,7 +810,9 @@ class ElectrumBackend(WalletBackend): now = time.time() stale_threshold = 300 - is_synced = height > 0 and (now - height_time) < stale_threshold + last_activity = getattr(self._server, "_last_activity", 0) + most_recent = max(height_time, last_activity) + is_synced = height > 0 and (now - most_recent) < stale_threshold return { "height": height, "synced": is_synced, From aeff117fdc0878e82d9260b4711a6630255cba20 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Sun, 26 Apr 2026 04:25:54 +0200 Subject: [PATCH 03/10] Fix warning when locked for electrum. --- basicswap/basicswap.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index ac59cea..f828a2e 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1388,8 +1388,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): self._initializeElectrumWallets() + is_locked = False + try: + _, is_locked = self.getLockedState() + except Exception: + pass + for c in self.activeCoins(): if self.coin_clients[c]["connection_type"] == "electrum": + if is_locked: + continue self.checkWalletSeed(c) for c in self.activeCoins(): From fe0de8405419fcc57101bf396806c287bc62c26a Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Mon, 27 Apr 2026 18:32:44 +0200 Subject: [PATCH 04/10] Fixing commented issue. --- basicswap/basicswap.py | 9 ++------- basicswap/interface/xmr.py | 1 - 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index f828a2e..ff5aee6 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1659,8 +1659,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): for c in check_coins: ci = self.ci(c) if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed(): - if not ci._have_checked_seed: - self.checkWalletSeed(c) + self.checkWalletSeed(c) if not ci.knownWalletSeed(): raise ValueError( '{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format( @@ -13907,11 +13906,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): "unconfirmed": ci.format_amount( walletinfo["unconfirmed_balance"], conv_int=True ), - "expected_seed": ( - ci.knownWalletSeed() - if ci._have_checked_seed - else self.checkWalletSeed(coin) - ), + "expected_seed": ci.knownWalletSeed(), "encrypted": walletinfo["encrypted"], "locked": walletinfo["locked"], "connection_type": self.coin_clients[coin].get( diff --git a/basicswap/interface/xmr.py b/basicswap/interface/xmr.py index 7517777..1b34be2 100644 --- a/basicswap/interface/xmr.py +++ b/basicswap/interface/xmr.py @@ -257,7 +257,6 @@ class XMRInterface(CoinInterface): ) raise os.rename(walletpath, bkp_path) - self._have_checked_seed = False # Drop through to open_wallet else: raise From a3e6d0cf170a95a5df94154d9bf064b79bdd8aab Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Mon, 27 Apr 2026 18:43:31 +0200 Subject: [PATCH 05/10] Fix: Extended private key for electrum. --- basicswap/bin/prepare.py | 7 ++++++- basicswap/templates/wallet.html | 10 ++++++++++ basicswap/ui/page_wallet.py | 9 +++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index 0e440e9..247425e 100755 --- a/basicswap/bin/prepare.py +++ b/basicswap/bin/prepare.py @@ -2106,7 +2106,12 @@ def initialise_wallets( continue try: ci = swap_client.ci(c) - if hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum(): + coin_settings = settings["chainclients"].get(coin_name, {}) + is_electrum = coin_settings.get("connection_type") == "electrum" + can_export = ( + hasattr(ci, "canExportToElectrum") and ci.canExportToElectrum() + ) + if can_export or (is_electrum and hasattr(ci, "getAccountKey")): seed_key = swap_client.getWalletKey(c, 1) account_key = ci.getAccountKey(seed_key, zprv_prefix) extended_keys[getCoinName(c)] = account_key diff --git a/basicswap/templates/wallet.html b/basicswap/templates/wallet.html index f3de993..0293a75 100644 --- a/basicswap/templates/wallet.html +++ b/basicswap/templates/wallet.html @@ -337,6 +337,16 @@ {{ w.expected_seed }} {% endif %} + {% if w.account_key %} + + Extended Private Key: + + •••••••••••••••• + + + + + {% endif %} diff --git a/basicswap/ui/page_wallet.py b/basicswap/ui/page_wallet.py index 8b4ff4f..449d349 100644 --- a/basicswap/ui/page_wallet.py +++ b/basicswap/ui/page_wallet.py @@ -473,6 +473,15 @@ def page_wallet(self, url_split, post_string): getattr(ci, "_connection_type", "rpc") == "electrum" ) + if hasattr(ci, "getAccountKey") and k not in (Coins.XMR, Coins.WOW): + try: + chain = swap_client.chain + zprv_prefix = 0x04B2430C if chain == "mainnet" else 0x045F18BC + seed_key = swap_client.getWalletKey(k, 1) + wallet_data["account_key"] = ci.getAccountKey(seed_key, zprv_prefix) + except Exception: + pass + fee_rate, fee_src = swap_client.getFeeRateForCoin(k) est_fee = swap_client.estimateWithdrawFee(k, fee_rate) wallet_data["fee_rate"] = ci.format_amount(int(fee_rate * ci.COIN())) From f84c46376e29e8d66203eaa51228336e38c638d4 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Mon, 27 Apr 2026 18:55:44 +0200 Subject: [PATCH 06/10] Fix: Pre-fund State --- basicswap/static/js/pages/bid-page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basicswap/static/js/pages/bid-page.js b/basicswap/static/js/pages/bid-page.js index cb6b930..adb297d 100644 --- a/basicswap/static/js/pages/bid-page.js +++ b/basicswap/static/js/pages/bid-page.js @@ -148,7 +148,7 @@ const BidPage = { 11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED 12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED 13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED - 14: { phase: 'failed', order: 90, label: 'Failed' }, // XMR_SWAP_SCRIPT_TX_PREREFUND + 14: { phase: 'redemption', order: 11.5, label: 'Refunding' }, // XMR_SWAP_SCRIPT_TX_PREREFUND 15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED 16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED 17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED From f2fff7292bc5bc2f483f102fd78adb8f38f27896 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Tue, 28 Apr 2026 01:19:00 +0200 Subject: [PATCH 07/10] Fix: Deposit address gap limit for electrum wallet. --- basicswap/wallet_manager.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/basicswap/wallet_manager.py b/basicswap/wallet_manager.py index 3739cab..df588c5 100644 --- a/basicswap/wallet_manager.py +++ b/basicswap/wallet_manager.py @@ -38,6 +38,7 @@ class WalletManager: } GAP_LIMIT = 50 + ELECTRUM_GAP_LIMIT = 20 def __init__(self, swap_client, log): self._gap_limits: Dict[Coins, int] = {} @@ -149,6 +150,18 @@ class WalletManager: ) self._swap_client.commitDB() + def _findReusableAddress(self, coin_type: Coins, internal: bool, cursor): + query = ( + "SELECT derivation_index, address FROM wallet_addresses" + " WHERE coin_type = ? AND is_internal = ? AND is_funded = 0" + " ORDER BY derivation_index ASC LIMIT 1" + ) + cursor.execute(query, (int(coin_type), internal)) + row = cursor.fetchone() + if row: + return row[0], row[1] + return None, None + def getNewAddress( self, coin_type: Coins, internal: bool = False, label: str = "", cursor=None ) -> str: @@ -184,6 +197,19 @@ class WalletManager: else: next_index = (state.last_external_index or 0) + 1 + if next_index >= self.ELECTRUM_GAP_LIMIT: + reuse_index, reuse_addr = self._findReusableAddress( + coin_type, internal, use_cursor + ) + if reuse_addr is not None: + self._log.debug( + f"Reusing unfunded address at index {reuse_index}" + f" (next would be {next_index}," + f" electrum gap limit {self.ELECTRUM_GAP_LIMIT})" + ) + self._swap_client.commitDB() + return reuse_addr + existing = self._swap_client.queryOne( WalletAddress, use_cursor, From 1d80f479c02c2d6545b354bb91dc4375f5d5a3fd Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Tue, 28 Apr 2026 09:34:01 +0200 Subject: [PATCH 08/10] Fix CI test --- basicswap/basicswap.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index ff5aee6..daeeb38 100644 --- a/basicswap/basicswap.py +++ b/basicswap/basicswap.py @@ -1659,7 +1659,10 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp): for c in check_coins: ci = self.ci(c) if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed(): - self.checkWalletSeed(c) + try: + self.checkWalletSeed(c) + except Exception as e: + self.log.debug(f"checkWalletSeed failed for {ci.coin_name()}: {e}") if not ci.knownWalletSeed(): raise ValueError( '{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format( From ff6d1ad0ba03d4b787ca0995e7be054ed99f4d62 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Tue, 28 Apr 2026 09:44:17 +0200 Subject: [PATCH 09/10] Fix electrum deposit addresses jumping past gap limit. --- basicswap/wallet_manager.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/basicswap/wallet_manager.py b/basicswap/wallet_manager.py index df588c5..e55c9ea 100644 --- a/basicswap/wallet_manager.py +++ b/basicswap/wallet_manager.py @@ -170,8 +170,6 @@ class WalletManager: use_cursor = self._swap_client.openDB(cursor) try: - self._syncStateIndices(coin_type, use_cursor) - state = self._swap_client.queryOne( WalletState, use_cursor, {"coin_type": int(coin_type)} ) From c5e703dfb36d038983f922ef812baa060b577323 Mon Sep 17 00:00:00 2001 From: gerlofvanek Date: Tue, 28 Apr 2026 10:10:24 +0200 Subject: [PATCH 10/10] GUI: v3.5.0 --- basicswap/templates/footer.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basicswap/templates/footer.html b/basicswap/templates/footer.html index 1c4abb9..37937b3 100644 --- a/basicswap/templates/footer.html +++ b/basicswap/templates/footer.html @@ -26,7 +26,7 @@

© 2026~ (BSX) BasicSwap

BSX: v{{ version }}

-

GUI: v3.4.1

+

GUI: v3.5.0

Made with

{{ love_svg | safe }}