diff --git a/bal/core/willexecutors.py b/bal/core/willexecutors.py index e1468b8..ca0c54a 100644 --- a/bal/core/willexecutors.py +++ b/bal/core/willexecutors.py @@ -77,6 +77,14 @@ class Willexecutors: """Raised when the welist server responds but returns no data for the requested chain, indicating no active servers for that network.""" + def __init__(self, chain, url=None, reason=None): + self.chain = chain + self.url = url + self.reason = reason + super().__init__( + f"NoServersForChainError: chain={chain} url={url} reason={reason}" + ) + # Expose the networking constants as class attributes so the GUI layer can # reference them (e.g. to show the "Xs / DEADLINEs" countdown) without # importing module-level names. Single source of truth: the module diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py index b1ffe8d..d7ac743 100644 --- a/bal/gui/qt/plugin.py +++ b/bal/gui/qt/plugin.py @@ -462,6 +462,10 @@ class Plugin(BalPlugin): self.update_all() return self.USER_TYPE.set("advanced" if idx == 1 else "basic") + # Show/hide the welist server row (advanced-only). + basic = idx != 1 + for w in (lbl_welist_server, edit_welist_server, help_welist_server): + w.setVisible(not basic) self.update_all() user_type_combo.currentIndexChanged.connect(on_user_type_change) @@ -474,6 +478,7 @@ class Plugin(BalPlugin): # with an external app, so the setting is no longer needed in the dialog. edit_event_summary = BalLineEdit(self.EVENT_SUMMARY) edit_event_description = BalTextEdit(self.EVENT_DESCRIPTION) + edit_welist_server = BalLineEdit(self.WELIST_SERVER) heir_repush = QPushButton("Rebroadcast transactions") heir_repush.clicked.connect(partial(self.broadcast_transactions, True)) @@ -637,12 +642,27 @@ class Plugin(BalPlugin): "field and the Raw/Date selector." ), ) - grid.addWidget(heir_repush, 9, 0) + # Welist server URL: shown only in ADVANCED mode. In BASIC mode the + # factory default is always used and the setting is hidden. + lbl_welist_server = QLabel(_("Welist Server URL")) + help_welist_server = HelpButton( + "URL of the server that provides the will-executor list. " + "Only used in ADVANCED mode." + ) + grid.addWidget(lbl_welist_server, 9, 0) + grid.addWidget(edit_welist_server, 9, 1) + grid.addWidget(help_welist_server, 9, 2) + # Initial visibility: hidden in basic, visible in advanced. + basic_init = str(self.USER_TYPE.get()).lower() != "advanced" + for w in (lbl_welist_server, edit_welist_server, help_welist_server): + w.setVisible(not basic_init) + + grid.addWidget(heir_repush, 10, 0) grid.addWidget( HelpButton( "Broadcast all transactions to willexecutors including those already pushed" ), - 9, + 10, 2, ) @@ -673,6 +693,7 @@ class Plugin(BalPlugin): (self.NO_WILLEXECUTOR, heir_no_willexecutor, "check"), (self.EVENT_SUMMARY, edit_event_summary, "line"), (self.EVENT_DESCRIPTION, edit_event_description, "text"), + (self.WELIST_SERVER, edit_welist_server, "line"), ] for cfg, widget, kind in resets: # Persist the default value back into the Electrum config. diff --git a/bal/gui/qt/window.py b/bal/gui/qt/window.py index 3cedf62..838e6a1 100644 --- a/bal/gui/qt/window.py +++ b/bal/gui/qt/window.py @@ -1093,26 +1093,26 @@ class BalWindow: def fetch_will_executors_list(self, old_willexecutors): """Download the will-executor list (runs inside the TaskThread worker). - Tries the configured server first, then the original hardcoded endpoint, - so a stale/bad config value cannot break the download. Detailed - per-attempt diagnostics are written to the Electrum log only; the user - sees a simple message. No business logic in ``bal.core`` is changed. + In BASIC mode the factory-default welist server is used and the setting + is hidden. In ADVANCED mode the user-configured URL is used exclusively + (no fallback), and any failure produces a descriptive error. Returns the downloaded dict (empty ``{}`` on failure). """ chainname = BalPlugin.chainname - configured = self.bal_plugin.WELIST_SERVER.get() - candidates = [] - for base in (configured, "https://welist.bitcoin-after.life/"): - if not base: - continue - base = base if base.endswith("/") else base + "/" - url = f"{base}data/{chainname}?page=0&limit=100" - if url not in candidates: - candidates.append(url) + basic = self.bal_plugin.is_basic_mode() + + if basic: + base = self.bal_plugin.WELIST_SERVER.default + else: + base = self.bal_plugin.WELIST_SERVER.get() + base = base if base.endswith("/") else base + "/" + url = f"{base}data/{chainname}?page=0&limit=100" + candidates = [url] result = {} any_server_reached = False + last_error = None net = Network.get_instance() _logger.info(f"fetch_will_executors_list: network present = {net is not None}") for url in candidates: @@ -1141,11 +1141,20 @@ class BalWindow: break _logger.warning(f"fetch_will_executors_list: {url} -> empty response") except Exception as e: + last_error = str(e) _logger.error( f"fetch_will_executors_list: {url} -> {type(e).__name__}: {e}" ) - if not result and any_server_reached: - raise Willexecutors.NoServersForChainError(chainname) + if not result: + if not basic: + # Advanced mode: always raise with full details. + raise Willexecutors.NoServersForChainError( + chainname, + url=url, + reason=last_error or "empty response", + ) + if any_server_reached: + raise Willexecutors.NoServersForChainError(chainname) return result # Simple, user-facing message shown when the download fails for any reason @@ -1210,11 +1219,23 @@ class BalWindow: def on_failure(exc_info): _logger.error(f"download_list failed: {exc_info}") if isinstance(exc_info[1], Willexecutors.NoServersForChainError): - chainname = BalPlugin.chainname - self.show_warning(_( - f"No active will-executor found for the " - f"{chainname} network." - )) + err = exc_info[1] + if err.url: + # Advanced mode: show the actual URL and error reason. + self.show_warning(_( + f"Could not reach the configured welist server.\n\n" + f"Server: {err.url}\n" + f"Error: {err.reason}\n\n" + f"Please verify the welist server URL in the plugin " + f"settings." + )) + else: + # Basic mode: the server responded but has no data for this + # chain. + self.show_warning(_( + f"No active will-executor found for the " + f"{err.chain} network." + )) else: self.show_warning(_(self.DOWNLOAD_FAILED_MESSAGE))