Merge pull request #450 from gerlofvanek/segfault

Fix: Segfault + various fixes, Bump GUI: v3.5.0
This commit is contained in:
tecnovert
2026-04-28 17:42:48 +00:00
committed by GitHub
12 changed files with 210 additions and 63 deletions
+17 -4
View File
@@ -1388,8 +1388,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
self._initializeElectrumWallets() self._initializeElectrumWallets()
is_locked = False
try:
_, is_locked = self.getLockedState()
except Exception:
pass
for c in self.activeCoins(): for c in self.activeCoins():
if self.coin_clients[c]["connection_type"] == "electrum": if self.coin_clients[c]["connection_type"] == "electrum":
if is_locked:
continue
self.checkWalletSeed(c) self.checkWalletSeed(c)
for c in self.activeCoins(): for c in self.activeCoins():
@@ -1651,11 +1659,16 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
for c in check_coins: for c in check_coins:
ci = self.ci(c) ci = self.ci(c)
if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed(): if self._restrict_unknown_seed_wallets and not ci.knownWalletSeed():
raise ValueError( try:
'{} has an unexpected wallet seed and "restrict_unknown_seed_wallets" is enabled.'.format( self.checkWalletSeed(c)
ci.coin_name() 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"): if self.coin_clients[c]["connection_type"] not in ("rpc", "electrum"):
continue continue
if c in (Coins.XMR, Coins.WOW): if c in (Coins.XMR, Coins.WOW):
+6 -1
View File
@@ -2106,7 +2106,12 @@ def initialise_wallets(
continue continue
try: try:
ci = swap_client.ci(c) 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) seed_key = swap_client.getWalletKey(c, 1)
account_key = ci.getAccountKey(seed_key, zprv_prefix) account_key = ci.getAccountKey(seed_key, zprv_prefix)
extended_keys[getCoinName(c)] = account_key extended_keys[getCoinName(c)] = account_key
+4 -1
View File
@@ -3099,7 +3099,10 @@ class BTCInterface(Secp256k1Interface):
} }
except Exception as e: except Exception as e:
error_msg = str(e).lower() 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( self._log.debug(
f"checkWatchedOutput exception for {txid_hex}:{vout}: {e}" f"checkWatchedOutput exception for {txid_hex}:{vout}: {e}"
) )
+106 -50
View File
@@ -82,9 +82,24 @@ class ElectrumConnection:
self._proxy_host = proxy_host self._proxy_host = proxy_host
self._proxy_port = proxy_port 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): def connect(self):
try: 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 import socks
sock = socks.socksocket() sock = socks.socksocket()
@@ -101,6 +116,10 @@ class ElectrumConnection:
sock = socket.create_connection( sock = socket.create_connection(
(self._host, self._port), timeout=self._timeout (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: if self._use_ssl:
context = ssl.create_default_context() context = ssl.create_default_context()
context.check_hostname = False context.check_hostname = False
@@ -546,11 +565,6 @@ class ElectrumServer:
elif isinstance(srv, dict): elif isinstance(srv, dict):
user_onion.append(srv) user_onion.append(srv)
final_clearnet = (
user_clearnet
if user_clearnet
else DEFAULT_ELECTRUM_SERVERS.get(coin_name, [])
)
final_onion = ( final_onion = (
user_onion if user_onion else DEFAULT_ONION_SERVERS.get(coin_name, []) 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 self._using_default_servers = not user_clearnet and not user_onion
if use_tor: 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) self._servers = list(final_onion) + list(final_clearnet)
if self._log and final_onion: if self._log:
self._log.info( self._log.info(
f"ElectrumServer {coin_name}: TOR enabled - " f"ElectrumServer {coin_name}: TOR enabled - "
f"{len(final_onion)} .onion + {len(final_clearnet)} clearnet servers" f"{len(final_onion)} .onion + {len(final_clearnet)} clearnet servers"
) )
else: else:
final_clearnet = (
user_clearnet
if user_clearnet
else DEFAULT_ELECTRUM_SERVERS.get(coin_name, [])
)
self._servers = list(final_clearnet) self._servers = list(final_clearnet)
if self._log: if self._log:
self._log.info( self._log.info(
@@ -983,55 +1010,84 @@ class ElectrumServer:
def call_background(self, method, params=None, timeout=20): def call_background(self, method, params=None, timeout=20):
if self._stopping: if self._stopping:
raise TemporaryError("Electrum server is shutting down") raise TemporaryError("Electrum server is shutting down")
conn = self._connection lock_acquired = self._lock.acquire(timeout=timeout + 5)
if conn is None or not conn.is_connected(): if not lock_acquired:
if self._stopping: raise TemporaryError(
raise TemporaryError("Electrum server is shutting down") f"Electrum background call timed out waiting for lock: {method}"
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")
try: try:
result = conn.call(method, params, timeout=timeout) for attempt in range(2):
self._last_activity = time.time() if self._stopping:
return result raise TemporaryError("Electrum server is shutting down")
except TemporaryError as e: if self._connection is None or not self._connection.is_connected():
if self._stopping: self.connect()
raise TemporaryError("Electrum server is shutting down") if self._connection is None:
if "timed out" in str(e).lower(): raise TemporaryError("Electrum call failed: no connection")
self._record_timeout() try:
raise 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): def call_batch_background(self, requests, timeout=30):
if self._stopping: if self._stopping:
raise TemporaryError("Electrum server is shutting down") raise TemporaryError("Electrum server is shutting down")
conn = self._connection lock_acquired = self._lock.acquire(timeout=timeout + 5)
if conn is None or not conn.is_connected(): if not lock_acquired:
if self._stopping: raise TemporaryError(
raise TemporaryError("Electrum server is shutting down") "Electrum background batch call timed out waiting for lock"
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")
try: try:
result = conn.call_batch(requests) for attempt in range(2):
self._last_activity = time.time() if self._stopping:
return result raise TemporaryError("Electrum server is shutting down")
except TemporaryError as e: if self._connection is None or not self._connection.is_connected():
if self._stopping: self.connect()
raise TemporaryError("Electrum server is shutting down") if self._connection is None:
if "timed out" in str(e).lower(): raise TemporaryError(
self._record_timeout() "Electrum batch call failed: no connection"
raise )
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): def call_user(self, method, params=None, timeout=10):
if self._stopping: if self._stopping:
+4 -1
View File
@@ -1631,7 +1631,10 @@ def js_wallettransactions(self, url_split, post_string, is_json) -> bytes:
or (current_time - cache_entry["time"]) > TX_CACHE_DURATION or (current_time - cache_entry["time"]) > TX_CACHE_DURATION
): ):
all_txs = ci.listWalletTransactions(count=10000, skip=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 = []
swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time} swap_client._tx_cache[coin_id] = {"txs": all_txs, "time": current_time}
else: else:
all_txs = cache_entry["txs"] all_txs = cache_entry["txs"]
+19
View File
@@ -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() { document.addEventListener('DOMContentLoaded', function() {
const burger = document.querySelectorAll('.navbar-burger'); const burger = document.querySelectorAll('.navbar-burger');
const menu = document.querySelectorAll('.navbar-menu'); const menu = document.querySelectorAll('.navbar-menu');
+1 -1
View File
@@ -148,7 +148,7 @@ const BidPage = {
11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED 11: { phase: 'locking', order: 10, label: 'Locking' }, // XMR_SWAP_NOSCRIPT_COIN_LOCKED
12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED 12: { phase: 'redemption', order: 11, label: 'Redemption' }, // XMR_SWAP_LOCK_RELEASED
13: { phase: 'redemption', order: 12, label: 'Redemption' }, // XMR_SWAP_SCRIPT_TX_REDEEMED 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 15: { phase: 'redemption', order: 13, label: 'Redemption' }, // XMR_SWAP_NOSCRIPT_TX_REDEEMED
16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED 16: { phase: 'failed', order: 91, label: 'Recovered' }, // XMR_SWAP_NOSCRIPT_TX_RECOVERED
17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED 17: { phase: 'failed', order: 92, label: 'Failed' }, // XMR_SWAP_FAILED_REFUNDED
+1 -1
View File
@@ -26,7 +26,7 @@
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm text-gray-90 dark:text-white font-medium">© 2026~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-gray-90 dark:text-white font-medium">© 2026~ (BSX) BasicSwap</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-coolGray-400 font-medium">BSX: v{{ version }}</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="text-sm text-coolGray-400 font-medium">GUI: v3.4.1</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span> <p class="text-sm text-coolGray-400 font-medium">GUI: v3.5.0</p> <span class="w-1 h-1 mx-1.5 bg-gray-500 dark:bg-white rounded-full"></span>
<p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p> <p class="mr-2 text-sm font-bold dark:text-white text-gray-90 ">Made with </p>
{{ love_svg | safe }} {{ love_svg | safe }}
</div> </div>
+10
View File
@@ -337,6 +337,16 @@
<td class="py-3 px-6">{{ w.expected_seed }}</td> <td class="py-3 px-6">{{ w.expected_seed }}</td>
</tr> </tr>
{% endif %} {% endif %}
{% if w.account_key %}
<tr class="opacity-100 text-gray-500 dark:text-gray-100 hover:bg-coolGray-200 dark:hover:bg-gray-600">
<td class="py-3 px-6 bold">Extended Private Key:</td>
<td class="py-3 px-6">
<span id="account-key-hidden" class="font-mono text-sm">••••••••••••••••</span>
<span id="account-key-value" class="font-mono text-sm hidden break-all">{{ w.account_key }}</span>
<button type="button" id="toggle-account-key" onclick="var h=document.getElementById('account-key-hidden'),v=document.getElementById('account-key-value');if(v.classList.contains('hidden')){v.classList.remove('hidden');h.classList.add('hidden');this.textContent='Hide';}else{v.classList.add('hidden');h.classList.remove('hidden');this.textContent='Show';}" class="ml-2 px-2 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded">Show</button>
</td>
</tr>
{% endif %}
</table> </table>
</div> </div>
</div> </div>
+13 -1
View File
@@ -473,6 +473,15 @@ def page_wallet(self, url_split, post_string):
getattr(ci, "_connection_type", "rpc") == "electrum" 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) fee_rate, fee_src = swap_client.getFeeRateForCoin(k)
est_fee = swap_client.estimateWithdrawFee(k, fee_rate) est_fee = swap_client.estimateWithdrawFee(k, fee_rate)
wallet_data["fee_rate"] = ci.format_amount(int(fee_rate * ci.COIN())) wallet_data["fee_rate"] = ci.format_amount(int(fee_rate * ci.COIN()))
@@ -559,7 +568,10 @@ def page_wallet(self, url_split, post_string):
skip = tx_filters.get("offset", 0) skip = tx_filters.get("offset", 0)
all_txs = ci.listWalletTransactions(count=10000, skip=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) total_transactions = len(all_txs)
raw_txs = all_txs[skip : skip + count] if all_txs else [] raw_txs = all_txs[skip : skip + count] if all_txs else []
+3 -1
View File
@@ -810,7 +810,9 @@ class ElectrumBackend(WalletBackend):
now = time.time() now = time.time()
stale_threshold = 300 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 { return {
"height": height, "height": height,
"synced": is_synced, "synced": is_synced,
+26 -2
View File
@@ -38,6 +38,7 @@ class WalletManager:
} }
GAP_LIMIT = 50 GAP_LIMIT = 50
ELECTRUM_GAP_LIMIT = 20
def __init__(self, swap_client, log): def __init__(self, swap_client, log):
self._gap_limits: Dict[Coins, int] = {} self._gap_limits: Dict[Coins, int] = {}
@@ -149,6 +150,18 @@ class WalletManager:
) )
self._swap_client.commitDB() 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( def getNewAddress(
self, coin_type: Coins, internal: bool = False, label: str = "", cursor=None self, coin_type: Coins, internal: bool = False, label: str = "", cursor=None
) -> str: ) -> str:
@@ -157,8 +170,6 @@ class WalletManager:
use_cursor = self._swap_client.openDB(cursor) use_cursor = self._swap_client.openDB(cursor)
try: try:
self._syncStateIndices(coin_type, use_cursor)
state = self._swap_client.queryOne( state = self._swap_client.queryOne(
WalletState, use_cursor, {"coin_type": int(coin_type)} WalletState, use_cursor, {"coin_type": int(coin_type)}
) )
@@ -184,6 +195,19 @@ class WalletManager:
else: else:
next_index = (state.last_external_index or 0) + 1 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( existing = self._swap_client.queryOne(
WalletAddress, WalletAddress,
use_cursor, use_cursor,