feat: add configurable welist server URL (advanced mode only)

- Add NoServersForChainError(chain, url, reason) with error details
- Expose WELIST_SERVER setting in plugin dialog, hidden in basic mode
- Basic mode uses factory default; advanced mode uses configured URL only
- Show descriptive error with URL and reason when advanced-mode fetch fails
This commit is contained in:
2026-06-29 00:18:42 -04:00
parent 7004953ce2
commit 7a58c69533
3 changed files with 72 additions and 22 deletions

View File

@@ -77,6 +77,14 @@ class Willexecutors:
"""Raised when the welist server responds but returns no data for the """Raised when the welist server responds but returns no data for the
requested chain, indicating no active servers for that network.""" 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 # Expose the networking constants as class attributes so the GUI layer can
# reference them (e.g. to show the "Xs / DEADLINEs" countdown) without # reference them (e.g. to show the "Xs / DEADLINEs" countdown) without
# importing module-level names. Single source of truth: the module # importing module-level names. Single source of truth: the module

View File

@@ -462,6 +462,10 @@ class Plugin(BalPlugin):
self.update_all() self.update_all()
return return
self.USER_TYPE.set("advanced" if idx == 1 else "basic") 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() self.update_all()
user_type_combo.currentIndexChanged.connect(on_user_type_change) 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. # with an external app, so the setting is no longer needed in the dialog.
edit_event_summary = BalLineEdit(self.EVENT_SUMMARY) edit_event_summary = BalLineEdit(self.EVENT_SUMMARY)
edit_event_description = BalTextEdit(self.EVENT_DESCRIPTION) edit_event_description = BalTextEdit(self.EVENT_DESCRIPTION)
edit_welist_server = BalLineEdit(self.WELIST_SERVER)
heir_repush = QPushButton("Rebroadcast transactions") heir_repush = QPushButton("Rebroadcast transactions")
heir_repush.clicked.connect(partial(self.broadcast_transactions, True)) heir_repush.clicked.connect(partial(self.broadcast_transactions, True))
@@ -637,12 +642,27 @@ class Plugin(BalPlugin):
"field and the Raw/Date selector." "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( grid.addWidget(
HelpButton( HelpButton(
"Broadcast all transactions to willexecutors including those already pushed" "Broadcast all transactions to willexecutors including those already pushed"
), ),
9, 10,
2, 2,
) )
@@ -673,6 +693,7 @@ class Plugin(BalPlugin):
(self.NO_WILLEXECUTOR, heir_no_willexecutor, "check"), (self.NO_WILLEXECUTOR, heir_no_willexecutor, "check"),
(self.EVENT_SUMMARY, edit_event_summary, "line"), (self.EVENT_SUMMARY, edit_event_summary, "line"),
(self.EVENT_DESCRIPTION, edit_event_description, "text"), (self.EVENT_DESCRIPTION, edit_event_description, "text"),
(self.WELIST_SERVER, edit_welist_server, "line"),
] ]
for cfg, widget, kind in resets: for cfg, widget, kind in resets:
# Persist the default value back into the Electrum config. # Persist the default value back into the Electrum config.

View File

@@ -1093,26 +1093,26 @@ class BalWindow:
def fetch_will_executors_list(self, old_willexecutors): def fetch_will_executors_list(self, old_willexecutors):
"""Download the will-executor list (runs inside the TaskThread worker). """Download the will-executor list (runs inside the TaskThread worker).
Tries the configured server first, then the original hardcoded endpoint, In BASIC mode the factory-default welist server is used and the setting
so a stale/bad config value cannot break the download. Detailed is hidden. In ADVANCED mode the user-configured URL is used exclusively
per-attempt diagnostics are written to the Electrum log only; the user (no fallback), and any failure produces a descriptive error.
sees a simple message. No business logic in ``bal.core`` is changed.
Returns the downloaded dict (empty ``{}`` on failure). Returns the downloaded dict (empty ``{}`` on failure).
""" """
chainname = BalPlugin.chainname chainname = BalPlugin.chainname
configured = self.bal_plugin.WELIST_SERVER.get() basic = self.bal_plugin.is_basic_mode()
candidates = []
for base in (configured, "https://welist.bitcoin-after.life/"): if basic:
if not base: base = self.bal_plugin.WELIST_SERVER.default
continue else:
base = self.bal_plugin.WELIST_SERVER.get()
base = base if base.endswith("/") else base + "/" base = base if base.endswith("/") else base + "/"
url = f"{base}data/{chainname}?page=0&limit=100" url = f"{base}data/{chainname}?page=0&limit=100"
if url not in candidates: candidates = [url]
candidates.append(url)
result = {} result = {}
any_server_reached = False any_server_reached = False
last_error = None
net = Network.get_instance() net = Network.get_instance()
_logger.info(f"fetch_will_executors_list: network present = {net is not None}") _logger.info(f"fetch_will_executors_list: network present = {net is not None}")
for url in candidates: for url in candidates:
@@ -1141,10 +1141,19 @@ class BalWindow:
break break
_logger.warning(f"fetch_will_executors_list: {url} -> empty response") _logger.warning(f"fetch_will_executors_list: {url} -> empty response")
except Exception as e: except Exception as e:
last_error = str(e)
_logger.error( _logger.error(
f"fetch_will_executors_list: {url} -> {type(e).__name__}: {e}" f"fetch_will_executors_list: {url} -> {type(e).__name__}: {e}"
) )
if not result and any_server_reached: 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) raise Willexecutors.NoServersForChainError(chainname)
return result return result
@@ -1210,10 +1219,22 @@ class BalWindow:
def on_failure(exc_info): def on_failure(exc_info):
_logger.error(f"download_list failed: {exc_info}") _logger.error(f"download_list failed: {exc_info}")
if isinstance(exc_info[1], Willexecutors.NoServersForChainError): if isinstance(exc_info[1], Willexecutors.NoServersForChainError):
chainname = BalPlugin.chainname 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(_( self.show_warning(_(
f"No active will-executor found for the " f"No active will-executor found for the "
f"{chainname} network." f"{err.chain} network."
)) ))
else: else:
self.show_warning(_(self.DOWNLOAD_FAILED_MESSAGE)) self.show_warning(_(self.DOWNLOAD_FAILED_MESSAGE))