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).
This commit is contained in:
@@ -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
|
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.
|
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;
|
### 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.
|
### then DISCOVER -> PLAN -> wait OK (R4) -> zip-first.
|
||||||
|
|
||||||
|
|||||||
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1424,3 +1424,73 @@ confirms the ZIP works).
|
|||||||
|
|
||||||
**Outcome:** DONE (delivered as test ZIP v0.4.7; commit only after the user
|
**Outcome:** DONE (delivered as test ZIP v0.4.7; commit only after the user
|
||||||
confirms the ZIP works).
|
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).
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0.4.7
|
0.4.8
|
||||||
@@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing
|
|||||||
``json_db.register_dict``) and PyQt6.
|
``json_db.register_dict``) and PyQt6.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.4.7"
|
__version__ = "0.4.8"
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_version = None
|
_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.
|
# Command used to open an .ics calendar file, per operating system.
|
||||||
default_app = {
|
default_app = {
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ from PyQt6.QtGui import (QColor, QPainter, QPalette, QStandardItem,
|
|||||||
QStandardItemModel)
|
QStandardItemModel)
|
||||||
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
|
from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
|
||||||
QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout,
|
QComboBox, QDateTimeEdit, QGridLayout, QHBoxLayout,
|
||||||
QLabel, QLineEdit, QTextEdit, QMenu, QMenuBar,
|
QInputDialog, QLabel, QLineEdit, QTextEdit, QMenu,
|
||||||
QPushButton, QScrollArea, QSizePolicy, QSpinBox,
|
QMenuBar, QPushButton, QScrollArea, QSizePolicy,
|
||||||
QStackedWidget, QStyle, QStyleOptionFrame,
|
QSpinBox, QStackedWidget, QStyle, QStyleOptionFrame,
|
||||||
QVBoxLayout, QWidget, QDialog)
|
QVBoxLayout, QWidget, QDialog)
|
||||||
|
|
||||||
# --- Core (GUI-free) logic layer ---
|
# --- Core (GUI-free) logic layer ---
|
||||||
|
|||||||
@@ -555,11 +555,12 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.scroll_area = QScrollArea(self)
|
self.scroll_area = QScrollArea(self)
|
||||||
self.scroll_area.setWidgetResizable(True)
|
self.scroll_area.setWidgetResizable(True)
|
||||||
self.scroll_area.setWidget(self.message_label)
|
self.scroll_area.setWidget(self.message_label)
|
||||||
# Open the report area already ~500px tall (owner request, allegato1:
|
# Open the report area ~450px tall (owner request: 500px left too much
|
||||||
# the previous ~140px area was far too short). The dialog may still grow
|
# empty space below the short report; 450px lines up with the desired
|
||||||
# up to 700px to fit a few more lines; beyond that the vertical
|
# window height). The dialog may still grow up to 700px to fit a few more
|
||||||
# scrollbar takes over and the bottom buttons stay reachable.
|
# lines; beyond that the vertical scrollbar takes over and the bottom
|
||||||
self.scroll_area.setMinimumHeight(500)
|
# buttons stay reachable.
|
||||||
|
self.scroll_area.setMinimumHeight(450)
|
||||||
self.scroll_area.setMaximumHeight(700)
|
self.scroll_area.setMaximumHeight(700)
|
||||||
self.vbox.addWidget(self.scroll_area, 1)
|
self.vbox.addWidget(self.scroll_area, 1)
|
||||||
|
|
||||||
@@ -701,6 +702,38 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
_logger.debug(f"not complete {e} true")
|
_logger.debug(f"not complete {e} true")
|
||||||
message = False
|
message = False
|
||||||
have_to_build = True
|
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):
|
if isinstance(e, HeirChangeException):
|
||||||
message = _("Heirs changed:")
|
message = _("Heirs changed:")
|
||||||
elif isinstance(e, WillExecutorNotPresent):
|
elif isinstance(e, WillExecutorNotPresent):
|
||||||
@@ -741,8 +774,15 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
txs = self.bal_window.build_will()
|
txs = self.bal_window.build_will()
|
||||||
if not txs:
|
if not txs:
|
||||||
self.msg_set_building(
|
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
|
return False, None
|
||||||
|
|
||||||
@@ -1546,6 +1586,42 @@ class BalBuildWillDialog(BalDialog):
|
|||||||
self.msg_edit_row(self.msg_error(f"Error: {b}"))
|
self.msg_edit_row(self.msg_error(f"Error: {b}"))
|
||||||
_logger.error(f"error phase2: {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):
|
def msg_set_checking(self, status="Waiting", row=None):
|
||||||
row = self.check_row if row is None else row
|
row = self.check_row if row is None else row
|
||||||
self.check_row = self.msg_set_status(_("Checking your will"), row, status)
|
self.check_row = self.msg_set_status(_("Checking your will"), row, status)
|
||||||
|
|||||||
@@ -438,6 +438,29 @@ class Plugin(BalPlugin):
|
|||||||
def on_user_type_change(idx):
|
def on_user_type_change(idx):
|
||||||
# Persist "basic"/"advanced" and refresh the open windows so the
|
# Persist "basic"/"advanced" and refresh the open windows so the
|
||||||
# advanced controls appear/disappear right away.
|
# 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.USER_TYPE.set("advanced" if idx == 1 else "basic")
|
||||||
self.update_all()
|
self.update_all()
|
||||||
|
|
||||||
@@ -473,40 +496,29 @@ class Plugin(BalPlugin):
|
|||||||
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
|
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
|
||||||
# have made the grid the dialog's only layout, leaving no room for them.
|
# have made the grid the dialog's only layout, leaving no room for them.
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
add_widget(
|
# NOTE: the "User Type" row used to be the first row (row 0). It was moved
|
||||||
grid,
|
# to the BOTTOM of the grid (just above "Rebroadcast transactions") at the
|
||||||
"User Type",
|
# owner's request; see the add_widget call further below. The remaining
|
||||||
user_type_combo,
|
# rows were renumbered up by one accordingly.
|
||||||
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."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
add_widget(
|
add_widget(
|
||||||
grid,
|
grid,
|
||||||
"Hide Replaced",
|
"Hide Replaced",
|
||||||
heir_hide_replaced,
|
heir_hide_replaced,
|
||||||
1,
|
0,
|
||||||
"Hide replaced transactions from will detail and list",
|
"Hide replaced transactions from will detail and list",
|
||||||
)
|
)
|
||||||
add_widget(
|
add_widget(
|
||||||
grid,
|
grid,
|
||||||
"Hide Invalidated",
|
"Hide Invalidated",
|
||||||
heir_hide_invalidated,
|
heir_hide_invalidated,
|
||||||
2,
|
1,
|
||||||
"Hide invalidated transactions from will detail and list",
|
"Hide invalidated transactions from will detail and list",
|
||||||
)
|
)
|
||||||
add_widget(
|
add_widget(
|
||||||
grid,
|
grid,
|
||||||
"Auto-sign on Check",
|
"Auto-sign on Check",
|
||||||
heir_auto_sign,
|
heir_auto_sign,
|
||||||
3,
|
2,
|
||||||
(
|
(
|
||||||
"When checking, automatically sign and broadcast the will "
|
"When checking, automatically sign and broadcast the will "
|
||||||
"transactions to their will-executors.\n"
|
"transactions to their will-executors.\n"
|
||||||
@@ -518,7 +530,7 @@ class Plugin(BalPlugin):
|
|||||||
grid,
|
grid,
|
||||||
"Panel editable Date and Fee",
|
"Panel editable Date and Fee",
|
||||||
heir_editable_dates,
|
heir_editable_dates,
|
||||||
4,
|
3,
|
||||||
(
|
(
|
||||||
"When enabled, the delivery-time and check-alive date fields "
|
"When enabled, the delivery-time and check-alive date fields "
|
||||||
"can be edited everywhere (toolbar / Heirs tab), not only in "
|
"can be edited everywhere (toolbar / Heirs tab), not only in "
|
||||||
@@ -540,7 +552,7 @@ class Plugin(BalPlugin):
|
|||||||
grid,
|
grid,
|
||||||
"Add transaction without willexecutor",
|
"Add transaction without willexecutor",
|
||||||
heir_no_willexecutor,
|
heir_no_willexecutor,
|
||||||
5,
|
4,
|
||||||
(
|
(
|
||||||
"Create a will that does not require a Will-executor; it can be "
|
"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 "
|
"saved, for example, on a USB stick, and a copy can be given to "
|
||||||
@@ -551,7 +563,7 @@ class Plugin(BalPlugin):
|
|||||||
grid,
|
grid,
|
||||||
"Number of reminders",
|
"Number of reminders",
|
||||||
heir_num_reminders,
|
heir_num_reminders,
|
||||||
6,
|
5,
|
||||||
(
|
(
|
||||||
"How many reminder alarms the exported calendar (.ics) event "
|
"How many reminder alarms the exported calendar (.ics) event "
|
||||||
"contains.\n\n"
|
"contains.\n\n"
|
||||||
@@ -568,7 +580,7 @@ class Plugin(BalPlugin):
|
|||||||
grid,
|
grid,
|
||||||
"Event summary",
|
"Event summary",
|
||||||
edit_event_summary,
|
edit_event_summary,
|
||||||
7,
|
6,
|
||||||
(
|
(
|
||||||
"Default message to be used in event summary\n"
|
"Default message to be used in event summary\n"
|
||||||
"Variables:\n"
|
"Variables:\n"
|
||||||
@@ -581,7 +593,7 @@ class Plugin(BalPlugin):
|
|||||||
grid,
|
grid,
|
||||||
"Event description",
|
"Event description",
|
||||||
edit_event_description,
|
edit_event_description,
|
||||||
8,
|
7,
|
||||||
(
|
(
|
||||||
"Default message to be used in event description\n"
|
"Default message to be used in event description\n"
|
||||||
"Variables:\n"
|
"Variables:\n"
|
||||||
@@ -607,6 +619,24 @@ class Plugin(BalPlugin):
|
|||||||
# "Ask before to ping willexecutor",
|
# "Ask before to ping willexecutor",
|
||||||
# )
|
# )
|
||||||
# add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ")
|
# 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(heir_repush, 9, 0)
|
||||||
grid.addWidget(
|
grid.addWidget(
|
||||||
HelpButton(
|
HelpButton(
|
||||||
@@ -668,7 +698,7 @@ class Plugin(BalPlugin):
|
|||||||
# reflects the reset values.
|
# reflects the reset values.
|
||||||
self.update_all()
|
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.setToolTip(_("Reset these settings to their default values"))
|
||||||
btn_reset.clicked.connect(on_reset_defaults)
|
btn_reset.clicked.connect(on_reset_defaults)
|
||||||
|
|
||||||
|
|||||||
@@ -388,6 +388,43 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
|||||||
self.combo.setCurrentIndex(index)
|
self.combo.setCurrentIndex(index)
|
||||||
#self.on_current_index_changed(index, force)
|
#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(
|
def set_value(
|
||||||
self,
|
self,
|
||||||
x: Any,
|
x: Any,
|
||||||
@@ -918,6 +955,18 @@ class WillSettingsWidget(QWidget):
|
|||||||
# Hidden in BASIC, visible in ADVANCED.
|
# Hidden in BASIC, visible in ADVANCED.
|
||||||
threshold.setVisible(not basic)
|
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):
|
def open_or_save_calendar(self):
|
||||||
"""Build and save an .ics calendar file with SEPARATE reminder events.
|
"""Build and save an .ics calendar file with SEPARATE reminder events.
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "bal",
|
"name": "bal",
|
||||||
"fullname": "Bitcoin After Life",
|
"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.",
|
"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",
|
"author": "Svatantrya",
|
||||||
"licence": "MIT",
|
"licence": "MIT",
|
||||||
|
|||||||
127
tests/test_group_h_v048.py
Normal file
127
tests/test_group_h_v048.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user