From 4dfb3fc41cd44f6988cd485e7910ff42857c4e27 Mon Sep 17 00:00:00 2001 From: svatantrya Date: Mon, 17 Aug 2026 12:06:21 -0400 Subject: [PATCH] fix: chainname regtest bug (classproperty); add no-heirs buttons in build-will dialog - bal/core/plugin_base.py: change chainname from frozen class attribute to @classproperty so it reads constants.net.NET_NAME at runtime, fixing regtest/testnet always downloading the mainnet executor list - bal/core/willexecutors.py: remove module-level chainname capture; all uses now read BalPlugin.chainname directly - bal/gui/qt/dialogs.py: when BalBuildWillDialog detects no heirs, return 'no_heirs' signal and show Heirs/Wizard/Close buttons (mirroring the existing no-willexecutor pattern); add HeirsDialog with full HeirListWidget (New Heir, Import, Export) --- bal/core/plugin_base.py | 10 +++- bal/core/willexecutors.py | 16 +++--- bal/gui/qt/dialogs.py | 117 +++++++++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 14 deletions(-) diff --git a/bal/core/plugin_base.py b/bal/core/plugin_base.py index 63e90e4..7ac2f4e 100644 --- a/bal/core/plugin_base.py +++ b/bal/core/plugin_base.py @@ -30,6 +30,7 @@ from electrum import constants, json_db from electrum.logging import get_logger from electrum.plugin import BasePlugin from electrum.transaction import tx_from_any +from electrum.util import classproperty _logger = get_logger(__name__) @@ -169,9 +170,12 @@ class BalPlugin(BasePlugin): } # Human-readable chain name ("bitcoin", "testnet", "regtest", ...). - chainname = ( - constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin" - ) + # Must be a classproperty (not a plain class attribute) because the class + # is defined before constants.net is set to the correct network — a plain + # attribute would capture "bitcoin" and never update. + @classproperty + def chainname(cls): + return constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin" # Default geometry hint for some dialogs (kept from the original code). SIZE = (159, 97) diff --git a/bal/core/willexecutors.py b/bal/core/willexecutors.py index 8de78b7..eaea558 100644 --- a/bal/core/willexecutors.py +++ b/bal/core/willexecutors.py @@ -112,8 +112,6 @@ def is_tor_active(): return False -chainname = BalPlugin.chainname - class Willexecutors: @@ -146,9 +144,9 @@ class Willexecutors: @staticmethod def save(bal_plugin, willexecutors): - _logger.debug(f"save {willexecutors},{chainname}") + _logger.debug(f"save {willexecutors},{BalPlugin.chainname}") aw = bal_plugin.WILLEXECUTORS.get() - aw[chainname] = willexecutors + aw[BalPlugin.chainname] = willexecutors bal_plugin.WILLEXECUTORS.set(aw) _logger.debug(f"saved: {aw}") # bal_plugin.WILLEXECUTORS.set(willexecutors) @@ -158,7 +156,7 @@ class Willexecutors: bal_plugin, update=False, bal_window: Any = None, force=False, task=True ): willexecutors = bal_plugin.WILLEXECUTORS.get() - willexecutors = willexecutors.get(chainname, {}) + willexecutors = willexecutors.get(BalPlugin.chainname, {}) to_del = [] for w in willexecutors: if not isinstance(willexecutors[w], dict): @@ -172,7 +170,7 @@ class Willexecutors: ) ) del willexecutors[w] - bal = bal_plugin.WILLEXECUTORS.default.get(chainname, {}) + bal = bal_plugin.WILLEXECUTORS.default.get(BalPlugin.chainname, {}) for bal_url, bal_executor in bal.items(): if bal_url not in willexecutors: _logger.debug(f"force add {bal_url} willexecutor") @@ -368,7 +366,7 @@ class Willexecutors: _logger.debug(f"{willexecutor['url']}: {willexecutor['txs']}") if w := Willexecutors.send_request( "post", - willexecutor["url"] + "/" + chainname + "/pushtxs", + willexecutor["url"] + "/" + BalPlugin.chainname + "/pushtxs", data=willexecutor["txs"].encode("ascii"), timeout=timeout, max_retries=max_retries, @@ -408,7 +406,7 @@ class Willexecutors: # single short timeout instead of retrying 10x with sleeps, which # used to freeze the UI for minutes per unreachable server. w = Willexecutors.send_request( - "get", url + "/" + chainname + "/info", + "get", url + "/" + BalPlugin.chainname + "/info", timeout=timeout, max_retries=max_retries, retry_sleep=retry_sleep, ) if isinstance(w, dict): @@ -788,7 +786,7 @@ class Willexecutors: welist_server = welist_server if welist_server[-1] == '/' else welist_server+'/' willexecutors = Willexecutors.send_request( "get", - f"{welist_server}data/{chainname}?page=0&limit=100", + f"{welist_server}data/{BalPlugin.chainname}?page=0&limit=100", ) if not isinstance(willexecutors, dict): _logger.warning( diff --git a/bal/gui/qt/dialogs.py b/bal/gui/qt/dialogs.py index 735618f..8089770 100644 --- a/bal/gui/qt/dialogs.py +++ b/bal/gui/qt/dialogs.py @@ -643,8 +643,7 @@ class BalBuildWillDialog(BalDialog): return None, tx except NoHeirsException: self.msg_set_status("Checking variables", varrow,"No Heirs",self.COLOR_ERROR) - #self.msg_set_checking("No Heirs") - return False, None + return "no_heirs", None except Exception as e: raise e try: @@ -1439,6 +1438,10 @@ class BalBuildWillDialog(BalDialog): self._add_no_willexecutor_buttons() return + if self.have_to_sign == "no_heirs": + self._add_no_heirs_buttons() + return + _logger.debug("have to sign {}".format(self.have_to_sign)) password = None if self.have_to_sign is None: @@ -1646,6 +1649,70 @@ class BalBuildWillDialog(BalDialog): on_error=self.on_error_phase1, ) + # ------------------------------------------------------------------ # + # No-heirs error handling (mirrors the no-willexecutor pattern above) + # ------------------------------------------------------------------ # + + def _add_no_heirs_buttons(self): + """Add "Heirs", "Wizard" and "Close" buttons when no heirs are + configured.""" + if getattr(self, "_no_heirs_buttons_added", False): + return + self._no_heirs_buttons_added = True + btn_row = QHBoxLayout() + btn_row.addStretch(1) + + heirs_btn = QPushButton(_("Heirs")) + heirs_btn.clicked.connect(self._open_heir_dialog) + btn_row.addWidget(heirs_btn) + + wizard_btn = QPushButton(_("\U0001f52e Wizard")) + wizard_btn.clicked.connect(self._open_heirs_wizard) + btn_row.addWidget(wizard_btn) + + close_btn = QPushButton(_("Close")) + close_btn.clicked.connect(self.close) + btn_row.addWidget(close_btn) + + self._no_heirs_layout = btn_row + self.vbox.addLayout(btn_row) + self.resize(self.vbox.sizeHint()) + + def _open_heir_dialog(self): + """Open the heirs management dialog, then retry the build.""" + d = HeirsDialog(self.bal_window, parent=self) + d.exec() + self._retry_build_after_heirs() + + def _open_heirs_wizard(self): + """Close the build-will dialog and open the wizard at the heirs + step so the user can add heirs.""" + self.close() + wizard = BalWizardDialog(self.bal_window) + wizard.exec() + + def _retry_build_after_heirs(self): + """Remove the no-heirs buttons, reset the message panel, + and re-run ``task_phase1`` on the same thread.""" + self._no_heirs_buttons_added = False + if self._no_heirs_layout: + while self._no_heirs_layout.count(): + item = self._no_heirs_layout.takeAt(0) + w = item.widget() + if w: + w.setParent(None) + w.deleteLater() + self.vbox.removeItem(self._no_heirs_layout) + self._no_heirs_layout = None + self.labels = [] + self.msg_update() + self.thread.add( + self.task_phase1, + on_success=self.on_success_phase1, + on_done=self.on_accept, + on_error=self.on_error_phase1, + ) + def _ics_provider(self): """Return the .ics content for the current will data.""" from datetime import datetime @@ -2197,3 +2264,49 @@ class WillExecutorDialog(BalDialog, MessageBoxMixin): event.accept() +class HeirsDialog(BalDialog, MessageBoxMixin): + def __init__(self, bal_window, parent=None): + if not parent: + parent = bal_window.window + BalDialog.__init__(self, parent, bal_window.bal_plugin) + self.bal_plugin = bal_window.bal_plugin + self.bal_window = bal_window + + self.setWindowTitle(_("Heirs")) + self.setMinimumSize(800, 300) + + from .lists import HeirListWidget + vbox = QVBoxLayout(self) + self.heir_list_widget = HeirListWidget(bal_window, self) + vbox.addWidget(self.heir_list_widget) + + btn_row = QHBoxLayout() + new_heir_btn = QPushButton(_("New Heir")) + new_heir_btn.clicked.connect(self._add_heir) + btn_row.addWidget(new_heir_btn) + + import_btn = QPushButton(_("Import")) + import_btn.clicked.connect(self._import_heirs) + btn_row.addWidget(import_btn) + + export_btn = QPushButton(_("Export")) + export_btn.clicked.connect(self._export_heirs) + btn_row.addWidget(export_btn) + + btn_row.addStretch(1) + vbox.addLayout(btn_row) + + def _add_heir(self): + self.bal_window.new_heir_dialog() + self.heir_list_widget.update() + + def _import_heirs(self): + self.bal_window.import_heirs() + self.heir_list_widget.update() + + def _export_heirs(self): + self.bal_window.export_heirs() + + def closeEvent(self, event): + event.accept() +