From 33e8a7533d934e9ce860b0a7f9fb0d741cc3f31c Mon Sep 17 00:00:00 2001 From: donkey-ai Date: Sun, 28 Jun 2026 23:02:40 -0400 Subject: [PATCH] v0.4.8: seven UX fixes for the BAL inheritance plugin - #04: Raw/Date selector now reappears on the WILL/HEIR tabs when switching to ADVANCED (and when a raw value is pushed from the wizard), keeping the current value/editor. New BalTimeEditWidget.apply_user_type_visibility() wired into WillSettingsWidget for both the locktime and check-alive boxes. - #3: Building Will report window minimum height 500 -> 450 px. - #5: 'User Type' setting moved to the bottom, above 'Rebroadcast transactions'. - #6: enabling ADVANCED now requires typing 'at My Risk' (case-insensitive); wrong phrase or cancel reverts to BASIC. QInputDialog added to common import. - #7a: on CHECK with an emptied wallet, an extra reassuring line is shown on 'Checking your will': 'Inheritance already executed (on blockchain)' in GREEN (CONFIRMED) or 'Inheritance in mempool (waiting confirmation)' in ORANGE (MEMPOOL). New _executed_inheritance_status() helper. - #7b: 'Balance is too low, or CheckAlive is in the past. Skipped' (space added) recoloured to ORANGE instead of red. - Reset button renamed 'Reset setting' -> 'Reset to Default Setting'. - Added tests/test_group_h_v048.py (8 tests). Version 0.4.7 -> 0.4.8. Full suite: 266 passed; ruff clean; CHANGELOG #24; memory updated (#04 done). --- .agent_memory_tasks.md | 35 ++++++++++ CHANGELOG.md | 70 ++++++++++++++++++++ bal/VERSION | 2 +- bal/__init__.py | 2 +- bal/core/plugin_base.py | 2 +- bal/gui/qt/common.py | 6 +- bal/gui/qt/dialogs.py | 90 ++++++++++++++++++++++++-- bal/gui/qt/plugin.py | 78 ++++++++++++++++------- bal/gui/qt/widgets.py | 49 ++++++++++++++ bal/manifest.json | 2 +- tests/test_group_h_v048.py | 127 +++++++++++++++++++++++++++++++++++++ 11 files changed, 425 insertions(+), 38 deletions(-) create mode 100644 tests/test_group_h_v048.py diff --git a/.agent_memory_tasks.md b/.agent_memory_tasks.md index 5272f54..db9bba0 100644 --- a/.agent_memory_tasks.md +++ b/.agent_memory_tasks.md @@ -355,6 +355,41 @@ the label) would also fix TASK #03. Keep them linked: solving "unify invalidate" classic window (PROCEDURE 1) resolves #03 automatically. If implemented separately, the minimal fix is to add set_label("BAL Invalidate transaction") in the PROCEDURE 2 broadcast path. +### TASK #04 — Raw/Date selector combo not shown on the WILL/HEIR tabs (only inside the wizard) +**User (translated from Italian):** "when I select Advanced in the plugin settings, the Raw mode of the +locktime and check-alive boxes only appears inside the wizard, and no longer on the Will and Heir tabs, +even if I tick the 'panel editable fee and date' setting. Inside the wizard it works perfectly; outside the +wizard only the DATE works." Plus: "if I set Raw inside the wizard for check-alive/locktime, then on the +Will/Heir tabs the raw number appears, but NOT the box to choose Raw or Date." +**ANALYSIS (already done, confirmed by reading the code):** +- Each date/locktime field is `BalTimeEditWidget` (widgets.py). Its `__init__` reads `is_basic_mode()` ONCE + (line ~293) and, in BASIC, hides the Raw/Date combo (line ~336 `self.combo.setVisible(False)`). +- The WILL/HEIR tab toolbars are created ONCE and REUSED for the whole session (not rebuilt on USER TYPE + change), so the combo stays in its initial (hidden) state. +- `apply_user_type_visibility()` (widgets.py ~888), called from `BalWindow.update_all()` (window.py ~1343) + on a BASIC<->ADVANCED switch, ONLY toggles the Check-Alive ROW visibility (`threshold.setVisible(not basic)`, + line ~919). It NEVER touches `self.combo.setVisible(...)` nor the editor. +- Raw value showing but combo hidden: when the wizard saves a raw value, `update_setting_widgets` -> + `update_widget_value` -> `set_value(raw)` writes the value into the active editor (so the raw NUMBER shows), + and `update_combo_setting_widgets` -> `set_index` sets the combo INDEX to Raw "behind the scenes" — but the + combo is still `setVisible(False)`, so the selector box stays invisible. +- The wizard works only because it is RECREATED on every open (re-reads the current mode). +**ROOT CAUSE (unified):** the Raw/Date combo on the toolbars is hidden permanently at `__init__` and nothing +ever makes it visible again, neither on ADVANCED switch nor when a raw value arrives. +**PROPOSED FIX (to detail in PLAN):** add a method to `BalTimeEditWidget` (e.g. `apply_user_type_visibility()`) +that re-reads `is_basic_mode()` and does `self.combo.setVisible(not basic)` WITHOUT changing the current value +or editor (per user req #1: keep current value to avoid confusion between date and inheritance). Call it from +`WillSettingsWidget.apply_user_type_visibility()` for BOTH `locktime` AND `threshold` (per user req #2: applies +to both boxes). So toggling BASIC<->ADVANCED, and a raw value pushed from the wizard, both correctly reveal the +combo on the already-existing WILL/HEIR toolbars without restarting Electrum. +**USER CONFIRMATIONS:** (1) on ADVANCED, show the combo but KEEP the current value/editor (do not reset) to +avoid confusing date vs inheritance; (2) applies to BOTH locktime AND check-alive boxes. + +### TASK #04 STATUS: DONE in v0.4.8 (user approved, implemented this session). +### Fix: added BalTimeEditWidget.apply_user_type_visibility() (shows/hides the Raw/Date +### combo, keeps current value/editor) and wired it into WillSettingsWidget.apply_user_type_visibility() +### for BOTH locktime and threshold. Delivered together with the other v0.4.8 UX changes. + ### DO NOT ACT on #01/#02/#03. Saved to to-do list only. Wait for user to add more or to choose one; ### then DISCOVER -> PLAN -> wait OK (R4) -> zip-first. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b3166d..2d5d5bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1424,3 +1424,73 @@ confirms the ZIP works). **Outcome:** DONE (delivered as test ZIP v0.4.7; commit only after the user confirms the ZIP works). + +--- + +## 24. v0.4.8 - Raw/Date selector on WILL/HEIR tabs, shorter report window, USER TYPE moved + "at My Risk" gate, executed/mempool inheritance note, "balance too low" recoloured, Reset button renamed + +**Date:** 2026-06-25 + +**Goal (owner requests, this session):** seven small, independent UX fixes, +delivered together in one version. + +**What changed:** + +- **#04 - Raw/Date selector now reappears on the WILL/HEIR tabs.** + `bal/gui/qt/widgets.py` + - Added `BalTimeEditWidget.apply_user_type_visibility()`: re-reads + `is_basic_mode()` and shows/hides ONLY the Raw/Date combo, WITHOUT changing + the current value or active editor (owner request: keep the value to avoid + confusing the delivery date with the inheritance). + - `WillSettingsWidget.apply_user_type_visibility()` now also calls the new + method on BOTH the `locktime` and `threshold` boxes. The reused toolbars used + to hide the combo forever after construction; now switching to ADVANCED (or a + raw value pushed from the wizard) reveals it without restarting Electrum. + +- **#3 - Building Will report window shorter.** + `bal/gui/qt/dialogs.py`: report scroll area minimum height `500 -> 450` px. + +- **#5 - "User Type" moved to the bottom of the settings.** + `bal/gui/qt/plugin.py`: the "User Type" row moved from the first grid row to + the bottom, just above "Rebroadcast transactions"; the other rows were + renumbered up by one. + +- **#6 - "at My Risk" gate before enabling ADVANCED.** + `bal/gui/qt/plugin.py` (`on_user_type_change`): selecting ADVANCED now prompts + "Type 'at My Risk' to enable ADVANCED mode"; the phrase is accepted + case-insensitively. A wrong phrase or a cancel reverts to BASIC. + `bal/gui/qt/common.py`: `QInputDialog` added to the shared Qt import. + +- **#7a - Reassuring note when the inheritance was already executed.** + `bal/gui/qt/dialogs.py`: added `_executed_inheritance_status()` (reads the + will items' CONFIRMED / MEMPOOL flags, CONFIRMED wins). In the + `NotCompleteWillException` branch of `task_phase1`, when the wallet is empty + because the inheritance went through, an extra line is shown on the "Checking + your will" row: "Inheritance already executed (on blockchain)" in GREEN + (CONFIRMED) or "Inheritance in mempool (waiting confirmation)" in ORANGE + (MEMPOOL). The original "changes" message is still shown afterwards. + +- **#7b - "balance too low" message recoloured.** + `bal/gui/qt/dialogs.py`: the text is now + "Balance is too low, or CheckAlive is in the past. Skipped" (a space added + before "Skipped") and rendered in ORANGE (`COLOR_WARNING`) instead of red, + since an empty wallet after execution is normal, not an error. + +- **Reset button renamed.** + `bal/gui/qt/plugin.py`: "Reset setting" -> "Reset to Default Setting". + +- Added `tests/test_group_h_v048.py` with 8 GUI-free tests pinning the + executed-inheritance detection rule (CONFIRMED > MEMPOOL > None) and the + "at My Risk" case-insensitive gate. + +- Version bumped 0.4.7 -> 0.4.8 (`plugin_base.py`, `__init__.py`, `VERSION`, + `manifest.json`). + +**Verification:** + +- `py_compile`: `widgets.py`, `dialogs.py`, `plugin.py`, `common.py` compile OK. +- Full test suite: `266 passed` (258 previous + 8 new v0.4.8 tests). +- `ruff`: no new errors (only pre-existing star-import / F841 noise). + +**Outcome:** DONE (delivered as test ZIP v0.4.8; commit only after the user +confirms the ZIP works). diff --git a/bal/VERSION b/bal/VERSION index f905682..c650d5a 100644 --- a/bal/VERSION +++ b/bal/VERSION @@ -1 +1 @@ -0.4.7 +0.4.8 \ No newline at end of file diff --git a/bal/__init__.py b/bal/__init__.py index 69a7836..dcda43f 100644 --- a/bal/__init__.py +++ b/bal/__init__.py @@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing ``json_db.register_dict``) and PyQt6. """ -__version__ = "0.4.7" +__version__ = "0.4.8" diff --git a/bal/core/plugin_base.py b/bal/core/plugin_base.py index d523b5a..2845e4d 100644 --- a/bal/core/plugin_base.py +++ b/bal/core/plugin_base.py @@ -91,7 +91,7 @@ class BalPlugin(BasePlugin): """ _version = None - __version__ = "0.4.7" # AUTOMATICALLY GENERATED DO NOT EDIT + __version__ = "0.4.8" # AUTOMATICALLY GENERATED DO NOT EDIT # Command used to open an .ics calendar file, per operating system. default_app = { diff --git a/bal/gui/qt/common.py b/bal/gui/qt/common.py index 7a9b537..7bb43c2 100644 --- a/bal/gui/qt/common.py +++ b/bal/gui/qt/common.py @@ -55,9 +55,9 @@ from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem, QStandardItemModel) from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox, QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout, - QLabel, QLineEdit, QTextEdit, QMenu, QMenuBar, - QPushButton, QScrollArea, QSizePolicy, QSpinBox, - QStackedWidget, QStyle, QStyleOptionFrame, + QInputDialog, QLabel, QLineEdit, QTextEdit, QMenu, + QMenuBar, QPushButton, QScrollArea, QSizePolicy, + QSpinBox, QStackedWidget, QStyle, QStyleOptionFrame, QVBoxLayout, QWidget, QDialog) # --- Core (GUI-free) logic layer --- diff --git a/bal/gui/qt/dialogs.py b/bal/gui/qt/dialogs.py index f091622..69c473b 100644 --- a/bal/gui/qt/dialogs.py +++ b/bal/gui/qt/dialogs.py @@ -555,11 +555,12 @@ class BalBuildWillDialog(BalDialog): self.scroll_area = QScrollArea(self) self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidget(self.message_label) - # Open the report area already ~500px tall (owner request, allegato1: - # the previous ~140px area was far too short). The dialog may still grow - # up to 700px to fit a few more lines; beyond that the vertical - # scrollbar takes over and the bottom buttons stay reachable. - self.scroll_area.setMinimumHeight(500) + # Open the report area ~450px tall (owner request: 500px left too much + # empty space below the short report; 450px lines up with the desired + # window height). The dialog may still grow up to 700px to fit a few more + # lines; beyond that the vertical scrollbar takes over and the bottom + # buttons stay reachable. + self.scroll_area.setMinimumHeight(450) self.scroll_area.setMaximumHeight(700) self.vbox.addWidget(self.scroll_area, 1) @@ -701,6 +702,38 @@ class BalBuildWillDialog(BalDialog): _logger.debug(f"not complete {e} true") message = False have_to_build = True + # Task #7a: if the will was already executed, the wallet is empty and + # this exception is expected. Before showing the alarming "Found + # CHANGES ... a NEW WILL must be prepared" message, add a clear, + # reassuring note on the "Checking your will" row telling the user + # the inheritance is already on its way (mempool) or done (on-chain). + # The original message is still shown afterwards (owner request: the + # red/"changes" message stays, this is only an extra, more precise + # informative line). + # NOTE: we add the informative note as its OWN extra row (not via + # msg_set_checking, which reuses self.check_row and would be + # overwritten by the "Found CHANGES" line set below). Passing row=None + # to msg_set_status appends a new line, so the executed/mempool note + # and the original message are BOTH visible. + executed_status = self._executed_inheritance_status() + if executed_status == "CONFIRMED": + # Green: the inheritance transaction is confirmed on the + # blockchain, so it has been executed correctly. + self.msg_set_status( + _("Checking your will"), + None, + _("Inheritance already executed (on blockchain)"), + self.COLOR_OK, + ) + elif executed_status == "MEMPOOL": + # Orange (warning colour): the transaction is in the mempool, + # waiting to be confirmed. Not an error, just "in progress". + self.msg_set_status( + _("Checking your will"), + None, + _("Inheritance in mempool (waiting confirmation)"), + self.COLOR_WARNING, + ) if isinstance(e, HeirChangeException): message = _("Heirs changed:") elif isinstance(e, WillExecutorNotPresent): @@ -741,8 +774,15 @@ class BalBuildWillDialog(BalDialog): txs = self.bal_window.build_will() if not txs: self.msg_set_building( - _("Balance is too low, or CheckAlive is in the past.Skipped"), - color = self.COLOR_ERROR, + _( + "Balance is too low, or CheckAlive is in the " + "past. Skipped" + ), + # Orange (warning) instead of red (error): an empty + # wallet after the inheritance was executed is a NORMAL + # situation, not a failure, so the colour should not + # alarm the user (owner request). + color=self.COLOR_WARNING, ) return False, None @@ -1546,6 +1586,42 @@ class BalBuildWillDialog(BalDialog): self.msg_edit_row(self.msg_error(f"Error: {b}")) _logger.error(f"error phase2: {b}") + def _executed_inheritance_status(self): + """Return the on-chain status of an already-executed inheritance. + + Task #7a (owner request). After an inheritance is executed the wallet is + fully emptied (the plugin always empties the wallet). Pressing CHECK on + such an empty wallet makes ``check_will`` raise NotCompleteWillException + (the heirs no longer match the empty wallet), which previously showed the + alarming "Found CHANGES ... a NEW WILL must be prepared" message even + though the inheritance was, in fact, correctly executed. + + To reassure the user we look at the will items, whose status is already + set to CONFIRMED / MEMPOOL by ``Will.check_will`` (see core/will.py), and + return a short code describing the real situation: + + Returns: + "CONFIRMED" if at least one will transaction is confirmed on-chain + (the inheritance is already executed); + "MEMPOOL" if none is confirmed but at least one is in the mempool + (waiting for confirmation); + None if neither (the change really has to be rebuilt). + + CONFIRMED takes precedence over MEMPOOL, since a confirmed transaction is + the strongest evidence that the inheritance has gone through. + """ + has_mempool = False + try: + for _wid, witem in self.bal_window.willitems.items(): + if witem.get_status("CONFIRMED"): + return "CONFIRMED" + if witem.get_status("MEMPOOL"): + has_mempool = True + except Exception as _err: + # Never let this purely informational check break the flow. + _logger.debug(f"_executed_inheritance_status error: {_err}") + return "MEMPOOL" if has_mempool else None + def msg_set_checking(self, status="Waiting", row=None): row = self.check_row if row is None else row self.check_row = self.msg_set_status(_("Checking your will"), row, status) diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py index 7ac521f..b1ffe8d 100644 --- a/bal/gui/qt/plugin.py +++ b/bal/gui/qt/plugin.py @@ -438,6 +438,29 @@ class Plugin(BalPlugin): def on_user_type_change(idx): # Persist "basic"/"advanced" and refresh the open windows so the # advanced controls appear/disappear right away. + # + # SAFETY GATE (owner request): enabling ADVANCED exposes powerful, + # easy-to-misuse controls. Before switching to ADVANCED the user must + # type a confirmation phrase ("at My Risk", case-insensitive). If the + # phrase is wrong or the dialog is cancelled, we revert the combo to + # BASIC and do NOT enable ADVANCED. Switching back to BASIC needs no + # confirmation. + if idx == 1: + text, ok = QInputDialog.getText( + d, + _("Enable ADVANCED mode"), + _("Type 'at My Risk' to enable ADVANCED mode"), + ) + # Accept any capitalisation (e.g. "at my risk", "AT MY RISK"). + if not ok or text.strip().lower() != "at my risk": + # Revert to BASIC. Block the signal while we reset the combo + # index so this handler is not called again recursively. + user_type_combo.blockSignals(True) + user_type_combo.setCurrentIndex(0) + user_type_combo.blockSignals(False) + self.USER_TYPE.set("basic") + self.update_all() + return self.USER_TYPE.set("advanced" if idx == 1 else "basic") self.update_all() @@ -473,40 +496,29 @@ class Plugin(BalPlugin): # Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would # have made the grid the dialog's only layout, leaving no room for them. grid = QGridLayout() - add_widget( - grid, - "User Type", - user_type_combo, - 0, - ( - "Choose how much detail the plugin shows.\n\n" - "BASIC (default): a simpler interface. It hides the advanced " - "controls (the Raw/Date selector and the 'Check Alive' field) " - "and turns off the 'check alive' postpone behaviour, so you " - "only set the delivery date.\n\n" - "ADVANCED: shows every control, including the 'Check Alive' " - "field and the Raw/Date selector." - ), - ) + # NOTE: the "User Type" row used to be the first row (row 0). It was moved + # to the BOTTOM of the grid (just above "Rebroadcast transactions") at the + # owner's request; see the add_widget call further below. The remaining + # rows were renumbered up by one accordingly. add_widget( grid, "Hide Replaced", heir_hide_replaced, - 1, + 0, "Hide replaced transactions from will detail and list", ) add_widget( grid, "Hide Invalidated", heir_hide_invalidated, - 2, + 1, "Hide invalidated transactions from will detail and list", ) add_widget( grid, "Auto-sign on Check", heir_auto_sign, - 3, + 2, ( "When checking, automatically sign and broadcast the will " "transactions to their will-executors.\n" @@ -518,7 +530,7 @@ class Plugin(BalPlugin): grid, "Panel editable Date and Fee", heir_editable_dates, - 4, + 3, ( "When enabled, the delivery-time and check-alive date fields " "can be edited everywhere (toolbar / Heirs tab), not only in " @@ -540,7 +552,7 @@ class Plugin(BalPlugin): grid, "Add transaction without willexecutor", heir_no_willexecutor, - 5, + 4, ( "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 " @@ -551,7 +563,7 @@ class Plugin(BalPlugin): grid, "Number of reminders", heir_num_reminders, - 6, + 5, ( "How many reminder alarms the exported calendar (.ics) event " "contains.\n\n" @@ -568,7 +580,7 @@ class Plugin(BalPlugin): grid, "Event summary", edit_event_summary, - 7, + 6, ( "Default message to be used in event summary\n" "Variables:\n" @@ -581,7 +593,7 @@ class Plugin(BalPlugin): grid, "Event description", edit_event_description, - 8, + 7, ( "Default message to be used in event description\n" "Variables:\n" @@ -607,6 +619,24 @@ class Plugin(BalPlugin): # "Ask before to ping willexecutor", # ) # add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ") + # "User Type" placed here (row 8), at the BOTTOM of the settings just + # above "Rebroadcast transactions" (owner request). It used to be the very + # first row; the other rows were renumbered up by one when it was moved. + add_widget( + grid, + "User Type", + user_type_combo, + 8, + ( + "Choose how much detail the plugin shows.\n\n" + "BASIC (default): a simpler interface. It hides the advanced " + "controls (the Raw/Date selector and the 'Check Alive' field) " + "and turns off the 'check alive' postpone behaviour, so you " + "only set the delivery date.\n\n" + "ADVANCED: shows every control, including the 'Check Alive' " + "field and the Raw/Date selector." + ), + ) grid.addWidget(heir_repush, 9, 0) grid.addWidget( HelpButton( @@ -668,7 +698,7 @@ class Plugin(BalPlugin): # reflects the reset values. self.update_all() - btn_reset = QPushButton(_("Reset setting")) + btn_reset = QPushButton(_("Reset to Default Setting")) btn_reset.setToolTip(_("Reset these settings to their default values")) btn_reset.clicked.connect(on_reset_defaults) diff --git a/bal/gui/qt/widgets.py b/bal/gui/qt/widgets.py index 1f28d5c..25aa520 100644 --- a/bal/gui/qt/widgets.py +++ b/bal/gui/qt/widgets.py @@ -388,6 +388,43 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor): self.combo.setCurrentIndex(index) #self.on_current_index_changed(index, force) + def apply_user_type_visibility(self): + """Re-apply the BASIC/ADVANCED visibility of the Raw/Date selector. + + WHY this is needed (bug reported by the owner, task #04): the Raw/Date + combo is hidden in BASIC mode and shown in ADVANCED mode. That visibility + used to be decided ONLY in ``__init__`` (see the ``self._basic_mode`` + block there). The WILL and HEIR tab toolbars are created once and then + REUSED for the whole session (they are not rebuilt when the user switches + USER TYPE), so after switching from BASIC to ADVANCED the combo stayed + hidden there and the user could only use the "Date" editor. The same + happened when a RAW value was pushed from the wizard: the raw number + showed (the active editor was updated) but the selector box stayed hidden + because nothing ever re-showed it. The wizard worked only because it is + recreated every time it is opened. + + This method re-reads ``is_basic_mode()`` and shows/hides ONLY the combo. + It deliberately does NOT change the current value or the active editor + (owner request: keep the current value to avoid confusing the delivery + date with the inheritance). It is called from + ``WillSettingsWidget.apply_user_type_visibility()`` (triggered by + ``BalWindow.update_all()`` on a BASIC<->ADVANCED switch), so toggling the + mode takes effect immediately on the already-existing WILL/HEIR toolbars + without restarting Electrum. + + It is safe to call repeatedly and on either layout (horizontal toolbar or + vertical wizard): it only flips the visibility of the Raw/Date combo. + """ + try: + basic = self.bal_window.bal_plugin.is_basic_mode() + except Exception: + # If the mode cannot be read, show the combo (the safe, most + # capable default for an existing widget). + basic = False + # Hidden in BASIC, visible in ADVANCED. The current value/editor is left + # untouched on purpose (see the docstring). + self.combo.setVisible(not basic) + def set_value( self, x: Any, @@ -918,6 +955,18 @@ class WillSettingsWidget(QWidget): # Hidden in BASIC, visible in ADVANCED. threshold.setVisible(not basic) + # Task #04: also re-apply the Raw/Date selector visibility on BOTH the + # delivery-time (locktime) and the check-alive (threshold) boxes. The + # combo was hidden once at construction (BASIC) and the reused WILL/HEIR + # toolbars never re-showed it after switching to ADVANCED. Each composite + # decides its own combo visibility from is_basic_mode() WITHOUT touching + # its current value or editor. + for field in ("locktime", "threshold"): + widget = self.widgets.get(field) + apply_combo = getattr(widget, "apply_user_type_visibility", None) + if callable(apply_combo): + apply_combo() + def open_or_save_calendar(self): """Build and save an .ics calendar file with SEPARATE reminder events. diff --git a/bal/manifest.json b/bal/manifest.json index adc92d5..6698e09 100644 --- a/bal/manifest.json +++ b/bal/manifest.json @@ -1,7 +1,7 @@ { "name": "bal", "fullname": "Bitcoin After Life", - "version": "0.4.7", + "version": "0.4.8", "description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.", "author": "Svatantrya", "licence": "MIT", diff --git a/tests/test_group_h_v048.py b/tests/test_group_h_v048.py new file mode 100644 index 0000000..a45a0f8 --- /dev/null +++ b/tests/test_group_h_v048.py @@ -0,0 +1,127 @@ +""" +Tests for the v0.4.8 changes (Group H). + +These tests cover the small, GUI-free DECISION LOGIC introduced in v0.4.8, +without importing the Qt widgets (which need PyQt6 + an Electrum window). For +each behaviour we reproduce the exact rule the production code uses, so the +tests stay fast and headless while still pinning the contract. + +Covered behaviour: + + * #7a - "executed inheritance" detection used to show a reassuring note on the + "Checking your will" row when the wallet is empty because the inheritance was + already executed (CONFIRMED) or is on its way (MEMPOOL). The rule: + CONFIRMED takes precedence over MEMPOOL, and only when neither is present is + the will treated as "really changed" (None). + * #6 - the "at My Risk" confirmation gate for enabling ADVANCED mode. The + typed phrase is accepted case-insensitively; anything else (or a cancel) + must keep the user in BASIC. + +Run: + PYTHONPATH=electrum-src python3 -m pytest tests/test_group_h_v048.py -q +""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) + + +# ------------------------------------------------------------------ # +# Mocks +# ------------------------------------------------------------------ # + +class FakeWillItem: + """Minimal will item exposing only ``get_status`` like the real WillItem.""" + + def __init__(self, statuses): + # ``statuses`` is a set of status names that are True for this item. + self._statuses = set(statuses) + + def get_status(self, status): + return status in self._statuses + + +def executed_inheritance_status(willitems): + """GUI-free copy of ``BalDialog._executed_inheritance_status``. + + Mirrors the production rule exactly (see bal/gui/qt/dialogs.py): + * return "CONFIRMED" as soon as any item is confirmed on-chain; + * otherwise return "MEMPOOL" if any item is in the mempool; + * otherwise return None. + """ + has_mempool = False + for witem in willitems.values(): + if witem.get_status("CONFIRMED"): + return "CONFIRMED" + if witem.get_status("MEMPOOL"): + has_mempool = True + return "MEMPOOL" if has_mempool else None + + +def advanced_phrase_ok(text, accepted): + """GUI-free copy of the ADVANCED gate check (see on_user_type_change). + + Returns True only when ``text`` equals the confirmation phrase ignoring + surrounding whitespace and letter case. ``accepted`` is False when the input + dialog was cancelled, which must never enable ADVANCED. + """ + if not accepted: + return False + return text.strip().lower() == "at my risk" + + +# ------------------------------------------------------------------ # +# #7a - executed-inheritance detection +# ------------------------------------------------------------------ # + +def test_executed_status_confirmed_wins(): + """A confirmed transaction reports CONFIRMED even if a mempool one exists.""" + willitems = { + "a": FakeWillItem({"MEMPOOL"}), + "b": FakeWillItem({"CONFIRMED"}), + } + assert executed_inheritance_status(willitems) == "CONFIRMED" + + +def test_executed_status_mempool_only(): + """With only a mempool transaction the status is MEMPOOL.""" + willitems = {"a": FakeWillItem({"MEMPOOL"})} + assert executed_inheritance_status(willitems) == "MEMPOOL" + + +def test_executed_status_none_when_neither(): + """No confirmed/mempool item -> None (the will really has to be rebuilt).""" + willitems = {"a": FakeWillItem({"VALID"})} + assert executed_inheritance_status(willitems) is None + + +def test_executed_status_empty(): + """No will items at all -> None.""" + assert executed_inheritance_status({}) is None + + +# ------------------------------------------------------------------ # +# #6 - "at My Risk" ADVANCED gate +# ------------------------------------------------------------------ # + +def test_advanced_phrase_exact(): + """The exact phrase as shown in the prompt is accepted.""" + assert advanced_phrase_ok("at My Risk", True) is True + + +def test_advanced_phrase_case_insensitive(): + """Any capitalisation is accepted (case-insensitive).""" + for variant in ("at my risk", "AT MY RISK", "At My Risk", " at my risk "): + assert advanced_phrase_ok(variant, True) is True + + +def test_advanced_phrase_wrong_text(): + """A wrong phrase keeps the user in BASIC.""" + assert advanced_phrase_ok("at your risk", True) is False + assert advanced_phrase_ok("", True) is False + + +def test_advanced_phrase_cancelled(): + """Cancelling the dialog (accepted=False) never enables ADVANCED.""" + assert advanced_phrase_ok("at My Risk", False) is False