feat(bal): UI batch v0.3.9 + expired-will invalidate regression fix

UI batch (TASK A/B/C/D):
- (A) Clearer 'Will expired' message: shortened will id (8+8 chars) and
  readable UTC date instead of raw UNIX timestamp; shown in WARNING colour
  (orange) instead of ERROR (red) in the wizard, as it is an expected step.
- (A2) Split the orange message onto two lines via <br> (rendered as HTML by
  msg_warning) to avoid an overly long single line.
- (B) History tab labels (text only): inheritance txs -> 'BAL Inheritance
  transaction'; invalidate tx -> 'BAL Invalidate transaction'. Colours left
  to Electrum defaults.
- (C) New 'No will-executor TX' checkbox in plugin settings, bound to the
  existing NO_WILLEXECUTOR config (default ON, kept in sync with the wizard),
  with help text, and included in the settings reset action.
- (D) Wizard button label 'Create your will' -> 'Build Your Will'.

Regression fix (A3):
- When an heir was added to an already-expired will via the wizard, the
  automatic invalidation no longer triggered. Root cause: the inner
  check_will() after build_will() raised WillExpiredException and the handler
  returned (False, None), which never invalidated.
- Auto-opening the invalidation tx window from the closing wizard proved
  unreliable (the window ended up behind the main wallet on some window
  managers; a re-check loop could ask to invalidate repeatedly before the tx
  reached the mempool). The robust fix: close the wizard and show a clear
  instruction popup guiding the user to run 'Tools -> Invalidate' and then
  press 'Check' to finish the will. Reasoning documented in the code.

Version bumped to 0.3.9 (manifest.json, __init__.py, plugin_base.py, VERSION).
CHANGELOG entry #15 updated. Verified: py_compile OK, ruff clean (no new
errors), full test suite 239 passed.
This commit is contained in:
2026-06-28 23:02:11 -04:00
parent ce3a463a5e
commit 5b707e647b
10 changed files with 249 additions and 22 deletions

View File

@@ -648,13 +648,41 @@ class BalBuildWillDialog(BalDialog):
self.bal_window.check_will()
for wid in Will.only_valid(self.bal_window.willitems):
self.bal_window.wallet.set_label(wid, "BAL Transaction")
# Label shown in Electrum's History tab for inheritance txs.
self.bal_window.wallet.set_label(wid, "BAL Inheritance transaction")
self.msg_set_building(self.msg_ok())
except WillExecutorNotPresent:
self.msg_set_status(
_("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
)
except WillExpiredException as e:
# An expired will is an EXPECTED situation (the locktime has
# passed). After adding/changing an heir the will is rebuilt
# above (build_will), and the freshly rebuilt transactions can
# themselves already be expired.
#
# We must NOT trigger the wizard's automatic invalidation loop
# here (return None, invalidate_tx). That loop re-runs
# task_phase1 right after broadcasting the invalidation, but the
# invalidation tx is not yet visible in the mempool, so the will
# is still detected as expired and the user is asked to
# invalidate again and again (infinite loop). It also never sets
# the "BAL Invalidate transaction" history label.
#
# Instead we reproduce EXACTLY what the "Tools -> invalidate"
# menu does (BalWalletWindow.invalidate_will): open Electrum's
# classic transaction dialog so the user can sign and broadcast
# the invalidation manually, set the proper history label, and
# stop. This is robust regardless of mempool confirmation state.
#
# The actual call to invalidate_will() (which opens GUI windows)
# must run in the GUI thread, so we only RETURN a signal here
# ("invalidate_classic"); on_success_phase1 performs the call.
# We still show the expired notice as a WARNING (orange).
self.msg_set_building(self.msg_warning(e))
return "invalidate_classic", None
except Exception as e:
self.msg_set_building(self.msg_error(e))
return False, None
@@ -906,6 +934,54 @@ class BalBuildWillDialog(BalDialog):
# if not tx:
# self.msg_edit_row(self.msg_error("Error, no tx was built"))
# return
# Special signal raised by task_phase1 when the freshly rebuilt will is
# already expired (e.g. an heir was added to an expired will). Instead of
# running the wizard's automatic invalidation loop (which would re-check
# before the invalidation tx reaches the mempool and loop forever), we
# behave exactly like the "Tools -> invalidate" menu: open Electrum's
# classic transaction dialog so the user signs and broadcasts the
# invalidation manually, with the "BAL Invalidate transaction" label.
# This runs in the GUI thread (on_success callback), so opening windows
# is safe. We then stop and close the wizard.
if self.have_to_sign == "invalidate_classic":
self.thread.stop()
# Design decision (window stacking + user clarity):
#
# When an heir is added to an already-expired will, the rebuilt will
# is itself expired and the old will must be invalidated on-chain
# before the new one can be used. We previously tried to open the
# invalidation transaction window AUTOMATICALLY from here, but doing
# so from within the closing wizard proved fragile: depending on the
# OS window manager and Qt's event ordering, the transaction window
# kept ending up BEHIND the main wallet window (it lost focus when
# the wizard closed). Neither closing-before-opening nor a deferred
# QTimer close() fixed it reliably on every machine.
#
# The robust solution is to NOT auto-open any window here. Instead we
# close the wizard and show a clear instruction telling the user to
# run "Tools -> Invalidate" themselves. That menu path is already
# known to work perfectly (its transaction window always stays in
# front, because no other window is closing at the same time), and
# it also makes the user consciously aware that they are performing a
# deliberate, important action (invalidating their old will).
#
# Close the wizard first so the instruction popup is the only window
# left, then show the guidance message.
self.close()
self.bal_window.show_message(
_(
"Your will has expired and must be invalidated before it "
"can be rebuilt.\n\n"
"Please use the top-right menu Tools -> Invalidate to "
"invalidate your old will: a transaction window will open "
"where you can sign and broadcast the invalidation.\n\n"
"After the invalidation is confirmed, press the Check "
"button near Tools, to finish the will."
)
)
return
_logger.debug("have to sign {}".format(self.have_to_sign))
password = None
if self.have_to_sign is None:

View File

@@ -470,7 +470,7 @@ class PreviewList(MyTreeView, MessageBoxMixin):
# The Wizard is the main entry point to create an inheritance, so make
# it stand out: show a bold label next to a slightly larger icon (the
# plain icon-only button was too easy to overlook).
wizard = QPushButton(" " + _("Create your will"))
wizard = QPushButton(" " + _("Build Your Will"))
wizard.setIcon(
read_QIcon_from_bytes(
self.bal_window.bal_plugin.read_file("icons/wizard.png")

View File

@@ -409,6 +409,14 @@ 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)
# "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
# window, so the two stay in sync automatically. When enabled the plugin
# also builds a will that does not require a will-executor (e.g. it can
# be saved on a USB stick and a copy given to the heirs).
heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)
# Editable line/text widgets are created once and kept in named
# variables so the "Reset" button (Group C / C4b) can refresh the
# displayed values after resetting the underlying config.
@@ -519,6 +527,21 @@ class Plugin(BalPlugin):
)
#add_widget(grid, "Bal Mode", bal_mode, 4, "choose bal mode")
# "No will-executor TX" setting. Mirrors the checkbox shown in the
# wizard's will-executor download window (both bound to NO_WILLEXECUTOR),
# so it can also be toggled from the plugin settings. Default ON.
add_widget(
grid,
"No will-executor TX",
heir_no_willexecutor,
8,
(
"Create a will that does not require a Will-executor; it can be "
"saved, for example, on a USB stick, and a copy can be given to "
"the heirs."
),
)
# add_widget(
# grid,
# "Ping Willexecutors",
@@ -533,32 +556,25 @@ class Plugin(BalPlugin):
# 4,
# "Ask before to ping willexecutor",
# )
# add_widget(
# grid,
# "Backup Transaction",
# heir_no_willexecutor,
# 5,
# "Add transactions without willexecutor",
# )
# add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ")
grid.addWidget(heir_repush, 8, 0)
grid.addWidget(heir_repush, 9, 0)
grid.addWidget(
HelpButton(
"Broadcast all transactions to willexecutors including those already pushed"
),
8,
9,
2,
)
# ----------------------------------------------------------------- #
# Group C / C4b: "Reset" button that restores the dialog settings to #
# their factory defaults. It only resets the settings exposed by THIS #
# dialog (the 6 below) and refreshes the corresponding widgets so the #
# dialog (the ones below) and refreshes the corresponding widgets so #
# change is visible immediately. It deliberately does NOT touch the #
# wills, will-executors or any other configuration. #
# ----------------------------------------------------------------- #
def on_reset_defaults():
"""Reset the six dialog settings to their defaults and refresh widgets.
"""Reset the dialog settings to their defaults and refresh widgets.
The default value of each setting is taken from ``BalConfig.default``
(the third argument used when the config was created in
@@ -573,6 +589,7 @@ class Plugin(BalPlugin):
(self.AUTO_SIGN, heir_auto_sign, "check"),
(self.EDITABLE_DATES, heir_editable_dates, "check"),
(self.NUM_REMINDERS, heir_num_reminders, "spin"),
(self.NO_WILLEXECUTOR, heir_no_willexecutor, "check"),
(self.EVENT_SUMMARY, edit_event_summary, "line"),
(self.EVENT_DESCRIPTION, edit_event_description, "text"),
]

View File

@@ -608,7 +608,8 @@ class BalWindow:
try:
self.check_will()
for wid, _w in self.willitems.items():
self.wallet.set_label(wid, "BAL Transaction")
# Label shown in Electrum's History tab for inheritance txs.
self.wallet.set_label(wid, "BAL Inheritance transaction")
rebuilt_ok = True
except WillExpiredException as e:
self.invalidate_will()
@@ -699,7 +700,8 @@ class BalWindow:
"Please sign and broadcast this transaction to invalidate current will"
)
)
self.wallet.set_label(result.txid(), "BAL Invalidate")
# Label shown in Electrum's History tab for invalidate txs.
self.wallet.set_label(result.txid(), "BAL Invalidate transaction")
self.show_transaction(result)
else:
self.show_message(_("No transactions to invalidate"))