Add Willexecutors.is_valid + grey italic styling for invalid executors

This commit is contained in:
2026-07-30 17:49:28 -04:00
parent 4a9299d85b
commit 9bf088b7ff
20 changed files with 3826 additions and 66 deletions

View File

@@ -63,7 +63,8 @@ from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
# --- Core (GUI-free) logic layer ---
from ...core.plugin_base import BalPlugin, BalTimestamp
from ...core.heirs import (HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT,
HeirAmountIsDustException, Heirs)
HeirAmountIsDustException, Heirs,
WillExecutorFeeTooHighException)
from ...core.util import Util
from ...core.will import (AmountException, HeirChangeException,
HeirNotFoundException, NoHeirsException,

View File

@@ -648,6 +648,7 @@ class BalBuildWillDialog(BalDialog):
self.bal_window.window.wallet.get_utxos(),
self.bal_window.date_to_check,
self.bal_window.window.wallet.dust_threshold(),
max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
)
_logger.debug("variables ok")
self.msg_set_status("Checking variables", varrow, "Ok", self.COLOR_OK)
@@ -659,6 +660,10 @@ class BalBuildWillDialog(BalDialog):
+ "Your settings require an adjustment of the amounts"
)
)
except WillExecutorFeeTooHighException as e:
self.msg_set_checking(
self.msg_warning(f"Will-executor fee too high: {e}")
)
self.msg_set_checking()
have_to_build = False
@@ -800,6 +805,15 @@ class BalBuildWillDialog(BalDialog):
_("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
)
except NoWillExecutorNotPresent:
_logger.debug("no will-executor selected, build interrupted")
self.msg_set_status(
_("Will-Executor"), None,
_("Not present - select one or enable backup mode"),
self.COLOR_ERROR,
)
return "no_willexecutor", None
except WillExpiredException as e:
# An expired will is an EXPECTED situation (the locktime has
# passed). After adding/changing an heir the will is rebuilt
@@ -1113,7 +1127,14 @@ class BalBuildWillDialog(BalDialog):
selected = {
url: we
for url, we in willexecutors.items()
if Willexecutors.is_selected(self.bal_window.willexecutors.get(url))
if Willexecutors.is_selected(
self.bal_window.willexecutors.get(url),
max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
) and Willexecutors.is_valid(
self.bal_window.willexecutors.get(url),
max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
dust=self.bal_window.window.wallet.dust_threshold(),
)
}
# Servers that report "already present" need their stored tx
@@ -1243,6 +1264,7 @@ class BalBuildWillDialog(BalDialog):
def invalidate_task(self, password, bal_window, tx):
if self._stopping:
return
print("invalidate task")
_logger.debug(f"invalidate tx: {tx}")
# fee_per_byte = bal_window.will_settings.get("baltx_fees", 1)
tx = self.bal_window.wallet.sign_transaction(tx, password)
@@ -1346,6 +1368,10 @@ class BalBuildWillDialog(BalDialog):
QTimer.singleShot(0, self.bal_window.invalidate_will)
return
if self.have_to_sign == "no_willexecutor":
self._add_no_willexecutor_buttons()
return
_logger.debug("have to sign {}".format(self.have_to_sign))
password = None
if self.have_to_sign is None:
@@ -1479,6 +1505,73 @@ class BalBuildWillDialog(BalDialog):
self.vbox.addLayout(button_row)
self._close_button.setFocus()
# ------------------------------------------------------------------ #
# No-willexecutor error handling
# ------------------------------------------------------------------ #
def _add_no_willexecutor_buttons(self):
"""Add "Will-Executor" and "Close" buttons when no executor is
selected and ``no_willexecutor`` is ``False``."""
if getattr(self, "_no_we_buttons_added", False):
return
self._no_we_buttons_added = True
btn_row = QHBoxLayout()
btn_row.addStretch(1)
we_btn = QPushButton(_("Will-Executor"))
we_btn.clicked.connect(self._open_willexecutor_dialog)
btn_row.addWidget(we_btn)
download_btn = QPushButton(_("\U0001f52e Wizard"))
download_btn.clicked.connect(self._open_willexecutor_download_widget)
btn_row.addWidget(download_btn)
close_btn = QPushButton(_("Close"))
close_btn.clicked.connect(self.close)
btn_row.addWidget(close_btn)
self._no_we_layout = btn_row
self.vbox.addLayout(btn_row)
self.resize(self.vbox.sizeHint())
def _open_willexecutor_dialog(self):
"""Open the will-executor management dialog, then auto-retry
the build when it closes."""
d = WillExecutorDialog(self.bal_window, parent=self)
d.exec()
self._retry_build_after_willexecutor()
def _open_willexecutor_download_widget(self):
"""Close the build-will dialog and re-open the wizard at the
will-executor download step so the user can add one."""
self.close()
wizard = BalWizardDialog(self.bal_window)
wizard.on_next_heir()
wizard.on_next_locktimeandfee()
wizard.exec()
def _retry_build_after_willexecutor(self):
"""Remove the no-willexecutor buttons, reset the message panel,
and re-run ``task_phase1`` on the same thread."""
self._no_we_buttons_added = False
if self._no_we_layout:
while self._no_we_layout.count():
item = self._no_we_layout.takeAt(0)
w = item.widget()
if w:
w.setParent(None)
w.deleteLater()
self.vbox.removeItem(self._no_we_layout)
self._no_we_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, timedelta

View File

@@ -888,7 +888,7 @@ class WillExecutorListWidget(MyTreeView):
# are shown unchanged.
display_url = url if len(url) <= 40 else url[:37] + "\u2026"
labels[self.Columns.URL] = display_url
if Willexecutors.is_selected(value):
if Willexecutors.is_selected(value, max_fee=float("inf")):
labels[self.Columns.SELECTED] = [
read_QIcon_from_bytes(
@@ -929,6 +929,17 @@ class WillExecutorListWidget(MyTreeView):
pass
else:
items.append(QStandardItem(e))
max_fee = self._bal_parent.bal_plugin.MAX_WILLEXECUTOR_FEE.get()
dust = self._bal_parent.bal_window.window.wallet.dust_threshold()
if not Willexecutors.is_valid(value, max_fee=max_fee, dust=dust):
grey = QColor("#808080")
for item in items:
font = item.font()
font.setItalic(True)
item.setFont(font)
item.setForeground(grey)
items[self.Columns.SELECTED].setEditable(False)
items[self.Columns.URL].setEditable(True)
items[self.Columns.ADDRESS].setEditable(True)

View File

@@ -436,6 +436,13 @@ class Plugin(BalPlugin):
# persisted NUM_REMINDERS config (default 3), with a range of 1..5.
heir_num_reminders = BalSpinBox(self.NUM_REMINDERS, minimum=1, maximum=5)
# Max willexecutor fee spin box. Maximum fee (in satoshi) allowed for
# a single will-executor. If a will-executor charges more, the will
# will not be built. Default 500,000 satoshi (0.005 BTC).
heir_max_willexecutor_fee = BalSpinBox(
self.MAX_WILLEXECUTOR_FEE, minimum=0, maximum=10000000
)
# "No will-executor TX" checkbox. Bound to the persisted NO_WILLEXECUTOR
# config (default ON, see plugin_base.py), the SAME config used by the
# checkbox inside the "Build your will" wizard's will-executor download
@@ -636,13 +643,28 @@ class Plugin(BalPlugin):
),
)
grid.addWidget(_make_reset_btn(self.NO_WILLEXECUTOR, heir_no_willexecutor, "check"), 4, 3)
# Max willexecutor fee: maximum fee (in satoshi) allowed for a single
# will-executor. Visible to all users (BASIC and ADVANCED).
add_widget(
grid,
"Max Will-Executor Fee (satoshi)",
heir_max_willexecutor_fee,
5,
(
"Maximum fee (in satoshi) allowed to be paid to a single "
"will-executor. If a will-executor charges more than this, "
"the will will not be built.\n"
"Default: 500,000 satoshi (0.005 BTC)."
),
)
grid.addWidget(_make_reset_btn(self.MAX_WILLEXECUTOR_FEE, heir_max_willexecutor_fee, "spin"), 5, 3)
# User Type selector placed BEFORE the advanced-only settings so the
# user chooses basic/advanced first, then sees the relevant options.
add_widget(
grid,
"User Type",
user_type_combo,
5,
6,
(
"Choose how much detail the plugin shows.\n\n"
"BASIC: simplified interface, safe configuration for most "
@@ -653,7 +675,7 @@ class Plugin(BalPlugin):
"editable."
),
)
grid.addWidget(_make_reset_btn(self.USER_TYPE, user_type_combo, "user_type"), 5, 3)
grid.addWidget(_make_reset_btn(self.USER_TYPE, user_type_combo, "user_type"), 6, 3)
# Number of reminders, event summary and event description are visible
# only in ADVANCED mode. In BASIC mode the factory defaults are always
# used and these settings are hidden.
@@ -662,11 +684,11 @@ class Plugin(BalPlugin):
"How many reminder alarms the exported calendar (.ics) event "
"contains. Range: 1 to 5 (default 3). Only used in ADVANCED mode."
)
grid.addWidget(_hide_if_basic(lbl_num_reminders), 6, 0)
grid.addWidget(_hide_if_basic(heir_num_reminders), 6, 1)
grid.addWidget(_hide_if_basic(help_num_reminders), 6, 2)
grid.addWidget(_hide_if_basic(lbl_num_reminders), 7, 0)
grid.addWidget(_hide_if_basic(heir_num_reminders), 7, 1)
grid.addWidget(_hide_if_basic(help_num_reminders), 7, 2)
reset_btn_6 = _make_reset_btn(self.NUM_REMINDERS, heir_num_reminders, "spin")
grid.addWidget(_hide_if_basic(reset_btn_6), 6, 3)
grid.addWidget(_hide_if_basic(reset_btn_6), 7, 3)
lbl_event_summary = QLabel(_("Event summary"))
help_event_summary = HelpButton(
@@ -676,11 +698,11 @@ class Plugin(BalPlugin):
" $heirs_complete: list of heirs name,address,amount\n"
"Only used in ADVANCED mode."
)
grid.addWidget(_hide_if_basic(lbl_event_summary), 7, 0)
grid.addWidget(_hide_if_basic(edit_event_summary), 7, 1)
grid.addWidget(_hide_if_basic(help_event_summary), 7, 2)
grid.addWidget(_hide_if_basic(lbl_event_summary), 8, 0)
grid.addWidget(_hide_if_basic(edit_event_summary), 8, 1)
grid.addWidget(_hide_if_basic(help_event_summary), 8, 2)
reset_btn_7 = _make_reset_btn(self.EVENT_SUMMARY, edit_event_summary, "line")
grid.addWidget(_hide_if_basic(reset_btn_7), 7, 3)
grid.addWidget(_hide_if_basic(reset_btn_7), 8, 3)
lbl_event_description = QLabel(_("Event description"))
help_event_description = HelpButton(
@@ -690,11 +712,11 @@ class Plugin(BalPlugin):
" $heirs_complete: list of heirs name,address,amount\n"
"Only used in ADVANCED mode."
)
grid.addWidget(_hide_if_basic(lbl_event_description), 8, 0)
grid.addWidget(_hide_if_basic(edit_event_description), 8, 1)
grid.addWidget(_hide_if_basic(help_event_description), 8, 2)
grid.addWidget(_hide_if_basic(lbl_event_description), 9, 0)
grid.addWidget(_hide_if_basic(edit_event_description), 9, 1)
grid.addWidget(_hide_if_basic(help_event_description), 9, 2)
reset_btn_8 = _make_reset_btn(self.EVENT_DESCRIPTION, edit_event_description, "text")
grid.addWidget(_hide_if_basic(reset_btn_8), 8, 3)
grid.addWidget(_hide_if_basic(reset_btn_8), 9, 3)
# 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"))
@@ -702,11 +724,11 @@ class Plugin(BalPlugin):
"URL of the server that provides the will-executor list. "
"Only available in ADVANCED mode."
)
grid.addWidget(_hide_if_basic(lbl_welist_server), 9, 0)
grid.addWidget(_hide_if_basic(edit_welist_server), 9, 1)
grid.addWidget(_hide_if_basic(help_welist_server), 9, 2)
grid.addWidget(_hide_if_basic(lbl_welist_server), 10, 0)
grid.addWidget(_hide_if_basic(edit_welist_server), 10, 1)
grid.addWidget(_hide_if_basic(help_welist_server), 10, 2)
reset_btn_9 = _make_reset_btn(self.WELIST_SERVER, edit_welist_server, "line")
grid.addWidget(_hide_if_basic(reset_btn_9), 9, 3)
grid.addWidget(_hide_if_basic(reset_btn_9), 10, 3)
lbl_calendar_app = QLabel(_("Calendar app command"))
help_calendar_app = HelpButton(
@@ -714,11 +736,11 @@ class Plugin(BalPlugin):
"Leave empty to use the system default (xdg-open/open/start).\n"
"Only used in ADVANCED mode."
)
grid.addWidget(_hide_if_basic(lbl_calendar_app), 10, 0)
grid.addWidget(_hide_if_basic(edit_calendar_app), 10, 1)
grid.addWidget(_hide_if_basic(help_calendar_app), 10, 2)
grid.addWidget(_hide_if_basic(lbl_calendar_app), 11, 0)
grid.addWidget(_hide_if_basic(edit_calendar_app), 11, 1)
grid.addWidget(_hide_if_basic(help_calendar_app), 11, 2)
reset_btn_10 = _make_reset_btn(self.CALENDAR_APP, edit_calendar_app, "line")
grid.addWidget(_hide_if_basic(reset_btn_10), 10, 3)
grid.addWidget(_hide_if_basic(reset_btn_10), 11, 3)
# NOTE: the ADVANCED-only widgets above have ALREADY been given their
# correct initial visibility inline (via _hide_if_basic) BEFORE being
@@ -727,12 +749,12 @@ class Plugin(BalPlugin):
# the Windows relayout flicker. Do NOT reintroduce a post-hoc
# setVisible() loop here.
grid.addWidget(heir_repush, 11, 0)
grid.addWidget(heir_repush, 12, 0)
grid.addWidget(
HelpButton(
"Broadcast all transactions to willexecutors including those already pushed"
),
11,
12,
2,
)
@@ -761,6 +783,7 @@ class Plugin(BalPlugin):
(self.EDITABLE_DATES, heir_editable_dates, "check"),
(self.NUM_REMINDERS, heir_num_reminders, "spin"),
(self.NO_WILLEXECUTOR, heir_no_willexecutor, "check"),
(self.MAX_WILLEXECUTOR_FEE, heir_max_willexecutor_fee, "spin"),
(self.EVENT_SUMMARY, edit_event_summary, "line"),
(self.EVENT_DESCRIPTION, edit_event_description, "text"),
(self.WELIST_SERVER, edit_welist_server, "line"),

View File

@@ -318,7 +318,12 @@ class BalWindow:
f = False
for _u, w in self.willexecutors.items():
if Willexecutors.is_selected(w):
if Willexecutors.is_selected(
w, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get()
) and Willexecutors.is_valid(
w, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
dust=self.window.wallet.dust_threshold()
):
f = True
if not f:
_logger.error("No Will-Executor or backup transaction selected")
@@ -532,6 +537,7 @@ class BalWindow:
self.window.wallet.get_utxos(),
self.date_to_check,
self.window.wallet.dust_threshold(),
max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
)
except AmountException as e:
self.show_warning(
@@ -539,6 +545,11 @@ class BalWindow:
f"In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{e}"
)
)
except WillExecutorFeeTooHighException as e:
self.show_error(
_(f"Will-executor fee too high: {e}")
)
return
except CheckAliveError:
self.show_error(
_(
@@ -553,7 +564,12 @@ class BalWindow:
if not self.no_willexecutor:
f = False
for _k, we in self.willexecutors.items():
if Willexecutors.is_selected(we):
if Willexecutors.is_selected(
we, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get()
) and Willexecutors.is_valid(
we, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
dust=self.window.wallet.dust_threshold()
):
f = True
if not f:
self.show_error(