diff --git a/basicswap/basicswap.py b/basicswap/basicswap.py index 581c4fc..daeeb38 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(): @@ -1651,11 +1659,16 @@ 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() + 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( + ci.coin_name() + ) ) - ) if self.coin_clients[c]["connection_type"] not in ("rpc", "electrum"): continue if c in (Coins.XMR, Coins.WOW): diff --git a/basicswap/bin/prepare.py b/basicswap/bin/prepare.py index 2da2a4e..31721c7 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/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/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 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 }}