forked from bitcoinafterlife/bal-electrum-plugin
v0.5.4
This commit is contained in:
256
CHANGELOG.md
256
CHANGELOG.md
@@ -1541,3 +1541,259 @@ delivered together in one version.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.4.8; commit only after the user
|
||||
confirms the ZIP works).
|
||||
|
||||
---
|
||||
|
||||
## 26. v0.5.1 - Dynamic Check Alive in BASIC mode + Windows dialog-flicker fix
|
||||
|
||||
**Date:** 2026-07-01
|
||||
|
||||
**Goal:** Fix two owner-reported bugs: (1) in BASIC mode, anticipating the
|
||||
delivery time (locktime) to a date earlier than the hidden, stale "Check
|
||||
Alive" (threshold) default could incorrectly mark the will as expired /
|
||||
blocked; (2) on Windows only, BAL dialogs (Settings, waiting dialogs) briefly
|
||||
flashed 1-2 blank "ghost" windows before rendering their real content.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- **Bug 1 - BASIC mode Check Alive now tracks the delivery time.**
|
||||
`bal/gui/qt/window.py`
|
||||
- Added `BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS` (2 hours,
|
||||
owner-approved) and a new pure `BalWindow.compute_date_to_check(
|
||||
is_basic_mode, locktime_setting, threshold_setting)` static method.
|
||||
- In BASIC mode, `date_to_check` (the "Check Alive" reference timestamp) is
|
||||
now derived LIVE from the current delivery time (`will_settings
|
||||
["locktime"]`), placed 2 hours before it, instead of reading the hidden,
|
||||
possibly stale stored `threshold`. This guarantees `date_to_check` is
|
||||
always earlier than the delivery time, so anticipating/postponing the
|
||||
delivery date in the wizard never triggers the "locktime is lower than
|
||||
threshold" guard (`build_inheritance_transaction`) nor
|
||||
`Will.check_will_expired`/`WillExpiredException` anymore.
|
||||
- In ADVANCED mode nothing changes: the stored `threshold` is used exactly
|
||||
as before.
|
||||
- Root-cause note: `Util.parse_locktime_string` never raises on invalid
|
||||
input, it silently returns `0`; `compute_date_to_check` now explicitly
|
||||
treats that sentinel as "unparsable" and falls back to the stored
|
||||
threshold, so BASIC mode can never compute a bogus near-epoch value.
|
||||
- `init_class_variables()` now simply calls `compute_date_to_check(...)`.
|
||||
|
||||
- **Bug 2 - Windows dialog flicker fixed.**
|
||||
`bal/gui/qt/window_utils.py` (`show_modal`) and `bal/gui/qt/dialogs.py`
|
||||
(`BalWaitingDialog.exe`): `bring_to_front()` (`raise_()` +
|
||||
`activateWindow()`) was being called on dialogs BEFORE `exec()` actually
|
||||
showed them. On Windows this forces Qt to create the dialog's native window
|
||||
handle immediately, empty and unpositioned, causing a brief flash of 1-2
|
||||
blank windows before the real content was drawn (reproduced from a
|
||||
screen recording; Linux does not show this, since native window creation
|
||||
there is lazier). Fixed by scheduling `bring_to_front()` via
|
||||
`QTimer.singleShot(0, ...)` so it runs on the next event-loop iteration,
|
||||
i.e. after the dialog is already visible with its real content - the
|
||||
focus/raise behaviour is unchanged, only the timing of when it fires.
|
||||
|
||||
- Added `tests/test_group_i_basic_checkalive.py` with 6 tests calling the
|
||||
real `BalWindow.compute_date_to_check` directly (no GUI/wallet needed):
|
||||
the 2-hour offset constant, a fresh "1y" default, the exact reported
|
||||
"anticipate to 1 month" scenario, a sweep of delivery dates always staying
|
||||
before locktime, ADVANCED mode being unaffected, and the unparsable-input
|
||||
fallback (which caught and fixed a real edge case during development: a
|
||||
0-timestamp sentinel instead of an exception).
|
||||
|
||||
- Version bumped 0.5.0 -> 0.5.1 (`plugin_base.py`, `__init__.py`, `VERSION`,
|
||||
`manifest.json`).
|
||||
|
||||
**Verification:**
|
||||
|
||||
- Full test suite: `272 passed` (266 previous + 6 new), plus the same 2
|
||||
pre-existing, unrelated failures already present before this change
|
||||
(`test_core_plugin_base.py::test_default_will_settings` /
|
||||
`test_validate_will_settings`, expecting the old `baltx_fees=100` default
|
||||
that v0.5.0 already changed to `20`).
|
||||
- `ruff`: no new errors (only pre-existing star-import / F841 noise).
|
||||
- The Check Alive fix (Bug 1) is fully covered by automated tests. The
|
||||
Windows flicker fix (Bug 2) was diagnosed from a screen recording and
|
||||
reasoned from the Qt/Windows native-window-creation behaviour; it cannot be
|
||||
reproduced or automatically verified on this Linux environment, so the
|
||||
owner needs to confirm on Windows that the flicker is gone.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.5.1; commit only after the owner
|
||||
confirms both fixes work, especially the Windows flicker one).
|
||||
|
||||
---
|
||||
|
||||
## 27. v0.5.2 - Real fix for the Windows Settings-dialog flicker (root cause found by diffing against v0.4.8)
|
||||
|
||||
**Date:** 2026-07-01
|
||||
|
||||
**Goal:** The v0.5.1 attempt at fixing the Windows-only "ghost window" flicker
|
||||
on the BAL Settings dialog (deferring `bring_to_front()` via
|
||||
`QTimer.singleShot`) did NOT fix it, per owner confirmation after testing on
|
||||
Windows. This entry documents the corrected root cause and the actual fix.
|
||||
|
||||
**Root cause (confirmed by diff, not guesswork):** the owner provided the
|
||||
last known-good release (v0.4.8, no flicker) for direct comparison.
|
||||
`bal/gui/qt/window_utils.py` (`show_modal`/`bring_to_front`) turned out to be
|
||||
byte-identical between v0.4.8 and v0.5.0 (aside from the v0.5.1 attempt),
|
||||
proving that code was never the cause. Diffing `bal/gui/qt/plugin.py`
|
||||
instead showed that `settings_dialog()` gained, between v0.4.8 and v0.5.0:
|
||||
(a) ~17 ADVANCED-only setting rows that are shown/hidden via `setVisible()`
|
||||
right at dialog construction time (to reflect the current BASIC/ADVANCED
|
||||
mode before the dialog is ever shown), and (b)
|
||||
`outer.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)`, which did
|
||||
**not** exist anywhere in v0.4.8's GUI code. `SetFixedSize` forces Qt to
|
||||
recompute and enforce the dialog's exact size on every layout invalidation -
|
||||
including while those ~17 rows are being hidden for the first time, before
|
||||
the dialog has ever been shown. On Windows, that live geometry renegotiation
|
||||
overlapping with native window creation is what produced the blank "ghost"
|
||||
window flash (reproduced from the owner's screen recording).
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/gui/qt/plugin.py` (`settings_dialog`): removed
|
||||
`outer.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize)`, restoring
|
||||
the sizing behaviour v0.4.8 already had (no forced constraint). Added an
|
||||
explicit `d.adjustSize()` right before `show_modal(d)` so the dialog still
|
||||
opens at its natural, correctly laid-out size (now computed once, instead
|
||||
of being continuously re-enforced). The dynamic BASIC/ADVANCED row
|
||||
visibility feature itself is fully preserved - only the size-constraint
|
||||
coupling that caused the flicker was removed.
|
||||
- Removed the now-unused `QLayout` import.
|
||||
|
||||
**Verification:**
|
||||
|
||||
- Full test suite: `272 passed`, same 2 pre-existing, unrelated failures as
|
||||
before (stale `baltx_fees=100` expectation vs the v0.5.0 default of `20`).
|
||||
- `ruff`: no new errors (only pre-existing star-import noise).
|
||||
- Diagnosis confirmed by direct code comparison against v0.4.8 (provided by
|
||||
the owner) rather than inference alone. The fix itself still cannot be
|
||||
verified on this Linux environment (Windows-only symptom): the owner needs
|
||||
to confirm on Windows that the flicker is gone.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.5.2; commit only after the owner
|
||||
confirms the flicker is actually gone this time).
|
||||
|
||||
---
|
||||
|
||||
## 28. v0.5.3 - Windows Settings-dialog flicker: corrected fix (drop bring_to_front before exec(), not just defer it)
|
||||
|
||||
**Date:** 2026-07-01
|
||||
|
||||
**Goal:** v0.5.2 (removing `SetFixedSize`) did NOT fix the Windows flicker
|
||||
either, per owner confirmation ("è come prima" - unchanged). This entry
|
||||
documents the corrected fix, based on new evidence from the owner: the
|
||||
flicker only ever happens with the Settings dialog in ADVANCED mode (many
|
||||
more visible rows, much larger dialog) and never in BASIC mode (few rows,
|
||||
small dialog) - always with the same intensity, not intermittent.
|
||||
|
||||
**Root cause (corrected):** `show_modal()` (`bal/gui/qt/window_utils.py`)
|
||||
called `bring_to_front(dialog)` (`raise_()` + `activateWindow()`) BEFORE
|
||||
`dialog.exec()`. The v0.5.1 attempt only *deferred* this call by one event
|
||||
loop tick (`QTimer.singleShot(0, ...)`) instead of removing it, which turned
|
||||
out to still fire too early relative to Windows finishing the dialog's
|
||||
native window layout/paint - reproducing the same flicker. The owner's
|
||||
BASIC-vs-ADVANCED observation is the key evidence: `raise_()`/
|
||||
`activateWindow()` forcing native window creation before the layout has
|
||||
settled produces a flash whose visible severity scales with how different
|
||||
the dialog's final size is from its premature/unlaid-out state - which is
|
||||
exactly why a much bigger ADVANCED-mode dialog flickers noticeably while the
|
||||
small BASIC-mode one does not.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/gui/qt/window_utils.py` (`show_modal`): removed the `bring_to_front()`
|
||||
call (deferred or otherwise) entirely. `QDialog.exec()` already shows,
|
||||
raises and activates a **modal** dialog on its own as part of entering its
|
||||
modal event loop, so the call was always redundant for this case - and, on
|
||||
Windows, actively harmful. `bring_to_front()` itself is untouched and is
|
||||
still used (correctly) by `show_on_top()`, for the few genuinely
|
||||
**non-modal** dialogs that use `.show()` instead of `.exec()` and do need
|
||||
an explicit raise/activate.
|
||||
- Removed the now-unused `QTimer` import.
|
||||
- `bal/gui/qt/dialogs.py` (`BalWaitingDialog.exe`): applied the same
|
||||
corrected fix for consistency (same redundant-before-exec() pattern, same
|
||||
underlying cause), even though the owner did not report flicker there -
|
||||
removed `bring_to_front()`/`QTimer.singleShot` and kept only `self.exec()`.
|
||||
|
||||
**Verification:**
|
||||
|
||||
- Full test suite: `272 passed`, same 2 pre-existing, unrelated failures as
|
||||
before.
|
||||
- `ruff`: no new errors (only pre-existing star-import / F841 noise).
|
||||
- As with v0.5.1/v0.5.2, this is a Windows-only symptom that cannot be
|
||||
reproduced or verified on this Linux environment; the owner needs to
|
||||
confirm on Windows.
|
||||
|
||||
**Outcome:** DONE (delivered as test ZIP v0.5.3; commit only after the owner
|
||||
confirms the flicker is actually gone).
|
||||
|
||||
---
|
||||
|
||||
## 29. v0.5.4 - REAL fix for the Windows Settings-dialog flicker (found by Windows-side bisection with the owner)
|
||||
|
||||
**Date:** 2026-07-02
|
||||
|
||||
**Goal:** v0.5.3 (dropping `bring_to_front()` before `exec()`) did NOT fix the
|
||||
flicker either, per owner confirmation ("è come prima" - unchanged). This
|
||||
entry documents the actual root cause, found through a structured bisection
|
||||
process where the owner tested a series of isolated diagnostic ZIPs directly
|
||||
on Windows (since the bug cannot be reproduced on Linux), and the
|
||||
corresponding real fix, **confirmed working by the owner on Windows**.
|
||||
|
||||
**Bisection process (each step tested live on Windows by the owner):**
|
||||
1. A dialog stripped down to almost nothing (2 checkboxes, no ADVANCED-only
|
||||
rows, none of the widgets/layout features added since v0.4.8) did **not**
|
||||
flicker → the cause is in the dialog's content, not in how/where it is
|
||||
opened, nor in `show_modal`/`bring_to_front` (already ruled out in
|
||||
v0.5.1/v0.5.3), nor in `SetFixedSize` (already ruled out in v0.5.2).
|
||||
2. Re-adding *only* the "User Type" combo + its dynamic show/hide mechanism
|
||||
flickered (mildly); re-adding *only* the new text/input fields (Event
|
||||
summary/description, Welist Server, Calendar app) did **not** → narrowed
|
||||
to the combo/visibility mechanism.
|
||||
3. A plain `QComboBox` alone (no confirmation dialog, no dynamic show/hide)
|
||||
did **not** flicker; the construction-time `setVisible()` toggle alone
|
||||
(no combo at all) **did** flicker → isolated to the visibility mechanism
|
||||
itself.
|
||||
4. A version of that same toggle that hides each ADVANCED-only widget
|
||||
**before** adding it to the layout (instead of adding it visible and
|
||||
hiding it afterwards) did **not** flicker → fix validated.
|
||||
|
||||
**Root cause:** the ~17 ADVANCED-only rows (Welist Server, Number of
|
||||
reminders, Event summary/description, Calendar app, Auto-sign, and their
|
||||
per-row reset buttons) were added to the settings grid **visible**, then all
|
||||
hidden together with `setVisible(False)` in BASIC mode, in a single loop
|
||||
*after* they were already part of the live layout. On Windows, that
|
||||
visible→hidden transition inside an already-populated layout - happening
|
||||
while the dialog's native window was still being created - forced a
|
||||
re-layout that flashed the dialog on screen. It only showed in ADVANCED mode
|
||||
because BASIC mode is when the widgets get hidden (so ADVANCED is when they
|
||||
stay visible and the dialog is at its largest, making any transient
|
||||
mis-layout most noticeable) - consistent with the owner's observation that
|
||||
severity tracked dialog size.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `bal/gui/qt/plugin.py` (`settings_dialog`): added a `basic_init` flag and a
|
||||
`_hide_if_basic(widget)` helper right after the dialog is created. Every
|
||||
ADVANCED-only widget is now wrapped in `_hide_if_basic(...)` at its
|
||||
`grid.addWidget(...)` call, so its final visibility is set **before** it
|
||||
ever joins the layout - it never transitions visible→hidden inside a live
|
||||
layout. Removed the old post-hoc loop that used to hide all ~17 widgets at
|
||||
once after they were already added. The runtime BASIC/ADVANCED toggle
|
||||
(`on_user_type_change`, used while the dialog is already open and visible)
|
||||
is unaffected and still works exactly as before - only the one-time
|
||||
construction-time visibility set-up changed.
|
||||
- Reverted the ineffective v0.5.2 attempt (`SetFixedSize` removal /
|
||||
`adjustSize()`) and the ineffective v0.5.3 attempt (removing
|
||||
`bring_to_front()` from `show_modal()`/`BalWaitingDialog.exe()`) back to
|
||||
their original v0.5.0 behaviour, since bisection proved neither was the
|
||||
cause; keeping them would only have added unnecessary risk/diff.
|
||||
|
||||
**Verification:**
|
||||
|
||||
- Full test suite: `272 passed`, same 2 pre-existing, unrelated failures as
|
||||
before (stale `baltx_fees=100` expectation vs the v0.5.0 default of `20`).
|
||||
- `ruff`: no new errors (only pre-existing star-import noise).
|
||||
- **Confirmed fixed by the owner on Windows** (the actual reporting
|
||||
environment), after testing the full real Settings dialog with this fix
|
||||
applied - not just the isolated diagnostic build.
|
||||
|
||||
**Outcome:** DONE. Confirmed resolved by the owner on Windows.
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.5.0
|
||||
0.5.4
|
||||
@@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing
|
||||
``json_db.register_dict``) and PyQt6.
|
||||
"""
|
||||
|
||||
__version__ = "0.5.0"
|
||||
__version__ = "0.5.4"
|
||||
|
||||
@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
|
||||
"""
|
||||
|
||||
_version = None
|
||||
__version__ = "0.5.0" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
__version__ = "0.5.4" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
|
||||
# Command used to open an .ics calendar file, per operating system.
|
||||
default_app = {
|
||||
|
||||
@@ -378,6 +378,33 @@ class Plugin(BalPlugin):
|
||||
lbl_logo = QLabel()
|
||||
lbl_logo.setPixmap(qicon)
|
||||
|
||||
# WINDOWS FLICKER FIX (root cause found by bisection with the owner
|
||||
# testing on Windows): the ADVANCED-only rows used to be added to the
|
||||
# grid *visible* and then hidden all at once with setVisible(False)
|
||||
# AFTER they were already in the layout (see the old block near the
|
||||
# end of this function). On Windows that visible->hidden transition,
|
||||
# happening inside an already-populated layout while the dialog's
|
||||
# native window is being created, forced a live re-layout that flashed
|
||||
# the dialog on screen (the "ghost window" the owner saw). It only
|
||||
# showed in ADVANCED because that is the larger dialog. The fix,
|
||||
# validated step by step on Windows, is to set each ADVANCED-only
|
||||
# widget's visibility BEFORE it is ever added to the layout, so it
|
||||
# never transitions visible->hidden inside a live layout.
|
||||
#
|
||||
# ``basic_init`` is the current mode; ``_hide_if_basic(w)`` hides a
|
||||
# widget immediately (at creation time) when in BASIC mode and returns
|
||||
# it, so it can be wrapped around each ADVANCED-only widget inline.
|
||||
basic_init = str(self.USER_TYPE.get()).lower() != "advanced"
|
||||
|
||||
def _hide_if_basic(w):
|
||||
"""Hide *w* now (before it is added to any layout) if in BASIC
|
||||
mode, and return it. Used to give ADVANCED-only widgets their
|
||||
final visibility up-front, avoiding the Windows relayout flicker
|
||||
that a later setVisible(False) inside the populated grid caused."""
|
||||
if basic_init:
|
||||
w.setVisible(False)
|
||||
return w
|
||||
|
||||
# heir_ping_willexecutors = BalCheckBox(self.PING_WILLEXECUTORS)
|
||||
# heir_ask_ping_willexecutors = BalCheckBox(self.ASK_PING_WILLEXECUTORS)
|
||||
# heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)
|
||||
@@ -562,13 +589,13 @@ class Plugin(BalPlugin):
|
||||
"The wallet password is requested only if the wallet is "
|
||||
"encrypted."
|
||||
)
|
||||
grid.addWidget(lbl_auto_sign, 2, 0)
|
||||
grid.addWidget(heir_auto_sign, 2, 1)
|
||||
grid.addWidget(help_auto_sign, 2, 2)
|
||||
grid.addWidget(_hide_if_basic(lbl_auto_sign), 2, 0)
|
||||
grid.addWidget(_hide_if_basic(heir_auto_sign), 2, 1)
|
||||
grid.addWidget(_hide_if_basic(help_auto_sign), 2, 2)
|
||||
reset_btn_auto_sign = _make_reset_btn(
|
||||
self.AUTO_SIGN, heir_auto_sign, "check"
|
||||
)
|
||||
grid.addWidget(reset_btn_auto_sign, 2, 3)
|
||||
grid.addWidget(_hide_if_basic(reset_btn_auto_sign), 2, 3)
|
||||
add_widget(
|
||||
grid,
|
||||
"Panel editable Date and Fee",
|
||||
@@ -630,11 +657,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(lbl_num_reminders, 6, 0)
|
||||
grid.addWidget(heir_num_reminders, 6, 1)
|
||||
grid.addWidget(help_num_reminders, 6, 2)
|
||||
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)
|
||||
reset_btn_6 = _make_reset_btn(self.NUM_REMINDERS, heir_num_reminders, "spin")
|
||||
grid.addWidget(reset_btn_6, 6, 3)
|
||||
grid.addWidget(_hide_if_basic(reset_btn_6), 6, 3)
|
||||
|
||||
lbl_event_summary = QLabel(_("Event summary"))
|
||||
help_event_summary = HelpButton(
|
||||
@@ -644,11 +671,11 @@ class Plugin(BalPlugin):
|
||||
" $heirs_complete: list of heirs name,address,amount\n"
|
||||
"Only used in ADVANCED mode."
|
||||
)
|
||||
grid.addWidget(lbl_event_summary, 7, 0)
|
||||
grid.addWidget(edit_event_summary, 7, 1)
|
||||
grid.addWidget(help_event_summary, 7, 2)
|
||||
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)
|
||||
reset_btn_7 = _make_reset_btn(self.EVENT_SUMMARY, edit_event_summary, "line")
|
||||
grid.addWidget(reset_btn_7, 7, 3)
|
||||
grid.addWidget(_hide_if_basic(reset_btn_7), 7, 3)
|
||||
|
||||
lbl_event_description = QLabel(_("Event description"))
|
||||
help_event_description = HelpButton(
|
||||
@@ -658,11 +685,11 @@ class Plugin(BalPlugin):
|
||||
" $heirs_complete: list of heirs name,address,amount\n"
|
||||
"Only used in ADVANCED mode."
|
||||
)
|
||||
grid.addWidget(lbl_event_description, 8, 0)
|
||||
grid.addWidget(edit_event_description, 8, 1)
|
||||
grid.addWidget(help_event_description, 8, 2)
|
||||
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)
|
||||
reset_btn_8 = _make_reset_btn(self.EVENT_DESCRIPTION, edit_event_description, "text")
|
||||
grid.addWidget(reset_btn_8, 8, 3)
|
||||
grid.addWidget(_hide_if_basic(reset_btn_8), 8, 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"))
|
||||
@@ -670,11 +697,11 @@ class Plugin(BalPlugin):
|
||||
"URL of the server that provides the will-executor list. "
|
||||
"Only available in ADVANCED mode."
|
||||
)
|
||||
grid.addWidget(lbl_welist_server, 9, 0)
|
||||
grid.addWidget(edit_welist_server, 9, 1)
|
||||
grid.addWidget(help_welist_server, 9, 2)
|
||||
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)
|
||||
reset_btn_9 = _make_reset_btn(self.WELIST_SERVER, edit_welist_server, "line")
|
||||
grid.addWidget(reset_btn_9, 9, 3)
|
||||
grid.addWidget(_hide_if_basic(reset_btn_9), 9, 3)
|
||||
|
||||
lbl_calendar_app = QLabel(_("Calendar app command"))
|
||||
help_calendar_app = HelpButton(
|
||||
@@ -682,23 +709,18 @@ class Plugin(BalPlugin):
|
||||
"Leave empty to use the system default (xdg-open/open/start).\n"
|
||||
"Only used in ADVANCED mode."
|
||||
)
|
||||
grid.addWidget(lbl_calendar_app, 10, 0)
|
||||
grid.addWidget(edit_calendar_app, 10, 1)
|
||||
grid.addWidget(help_calendar_app, 10, 2)
|
||||
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)
|
||||
reset_btn_10 = _make_reset_btn(self.CALENDAR_APP, edit_calendar_app, "line")
|
||||
grid.addWidget(reset_btn_10, 10, 3)
|
||||
grid.addWidget(_hide_if_basic(reset_btn_10), 10, 3)
|
||||
|
||||
# Initial visibility: hidden in basic, visible in advanced.
|
||||
basic_init = str(self.USER_TYPE.get()).lower() != "advanced"
|
||||
for w in (lbl_welist_server, edit_welist_server, help_welist_server,
|
||||
lbl_num_reminders, heir_num_reminders, help_num_reminders,
|
||||
lbl_event_summary, edit_event_summary, help_event_summary,
|
||||
lbl_event_description, edit_event_description, help_event_description,
|
||||
lbl_calendar_app, edit_calendar_app, help_calendar_app,
|
||||
lbl_auto_sign, heir_auto_sign, help_auto_sign,
|
||||
reset_btn_6, reset_btn_7, reset_btn_8, reset_btn_9, reset_btn_10,
|
||||
reset_btn_auto_sign):
|
||||
w.setVisible(not basic_init)
|
||||
# NOTE: the ADVANCED-only widgets above have ALREADY been given their
|
||||
# correct initial visibility inline (via _hide_if_basic) BEFORE being
|
||||
# added to the grid. The old code did the opposite - it added them
|
||||
# visible and then hid them here, all at once, which is what caused
|
||||
# the Windows relayout flicker. Do NOT reintroduce a post-hoc
|
||||
# setVisible() loop here.
|
||||
|
||||
grid.addWidget(heir_repush, 11, 0)
|
||||
grid.addWidget(
|
||||
|
||||
@@ -451,11 +451,74 @@ class BalWindow:
|
||||
for heir, value in updates.items():
|
||||
self.heirs[heir] = value
|
||||
|
||||
# How long BEFORE the delivery time (locktime) the auto-computed Check
|
||||
# Alive threshold is placed in BASIC mode (see compute_date_to_check).
|
||||
# Owner-approved value: 2 hours. Kept as a named constant so the offset
|
||||
# is documented in one place and easy to tune later if needed.
|
||||
BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS = 2 * 60 * 60
|
||||
|
||||
@staticmethod
|
||||
def compute_date_to_check(is_basic_mode, locktime_setting, threshold_setting):
|
||||
"""Resolve the "Check Alive" reference timestamp (``date_to_check``).
|
||||
|
||||
SIMPLE / ADVANCED: the "Check Alive" (threshold) field is hidden in
|
||||
BASIC mode, so the user can never keep it in sync with the delivery
|
||||
time (locktime) when they anticipate/postpone it from the wizard.
|
||||
Left untouched, the threshold stays at its old/default value (e.g.
|
||||
"today + 11 months") and, if the user later sets a delivery date
|
||||
EARLIER than that, two checks elsewhere (the "locktime is lower than
|
||||
threshold" guard in ``build_inheritance_transaction``, and
|
||||
``Will.check_will_expired`` via ``date_to_check``) misfire and
|
||||
block/expire the will - even though the postpone/expiry check itself
|
||||
is meant to be inert in BASIC mode (see the ``is_basic_mode()`` guard
|
||||
in ``init_class_variables``).
|
||||
|
||||
FIX: in BASIC mode only, ignore the stored threshold and instead
|
||||
derive ``date_to_check`` LIVE from the current delivery time
|
||||
(``locktime_setting``), placed a small, fixed offset BEFORE it
|
||||
(``BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS`` = 2 hours). This keeps
|
||||
``date_to_check`` always < locktime by construction, so anticipating
|
||||
or postponing the delivery date in BASIC mode never triggers a
|
||||
spurious "expired" / "threshold" error. In ADVANCED mode nothing
|
||||
changes: the stored threshold is used as-is, exactly like before.
|
||||
|
||||
Args:
|
||||
is_basic_mode: ``BalPlugin.is_basic_mode()`` result.
|
||||
locktime_setting: the current ``will_settings["locktime"]`` value
|
||||
(relative string like ``"1y"`` or an absolute timestamp).
|
||||
threshold_setting: the current ``will_settings["threshold"]``
|
||||
value, used as-is in ADVANCED mode and as a fallback if the
|
||||
locktime cannot be parsed.
|
||||
|
||||
Returns:
|
||||
float: the resolved ``date_to_check`` UNIX timestamp.
|
||||
"""
|
||||
if is_basic_mode:
|
||||
try:
|
||||
locktime_ts = Util.parse_locktime_string(locktime_setting)
|
||||
# Util.parse_locktime_string never raises: on anything it
|
||||
# cannot parse it silently returns 0 (see bal/core/util.py).
|
||||
# Treat that sentinel the same as a parse error, otherwise we
|
||||
# would compute a nonsensical date_to_check near the UNIX
|
||||
# epoch instead of falling back to the stored threshold.
|
||||
if locktime_ts:
|
||||
return (
|
||||
locktime_ts
|
||||
- BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return BalTimestamp(threshold_setting).to_timestamp()
|
||||
|
||||
def init_class_variables(self):
|
||||
if not self.heirs:
|
||||
raise NoHeirsException(_("Heirs are not defined"))
|
||||
try:
|
||||
self.date_to_check = BalTimestamp(self.will_settings['threshold']).to_timestamp()
|
||||
self.date_to_check = BalWindow.compute_date_to_check(
|
||||
self.bal_plugin.is_basic_mode(),
|
||||
self.will_settings["locktime"],
|
||||
self.will_settings["threshold"],
|
||||
)
|
||||
# found = False
|
||||
# NOTE: block-height tracking removed (A1) - locktimes are always
|
||||
# UNIX timestamps now, so we no longer read the current block height
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bal",
|
||||
"fullname": "Bitcoin After Life",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.4",
|
||||
"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",
|
||||
|
||||
119
tests/test_group_i_basic_checkalive.py
Normal file
119
tests/test_group_i_basic_checkalive.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
Tests for the "BASIC mode dynamic Check Alive" fix (Group I).
|
||||
|
||||
Background (reported by the owner): with the plugin left in BASIC mode (the
|
||||
default), the "Check Alive" (threshold) field is hidden from the user and
|
||||
stays stuck at its old/default value (roughly "today + 11 months", i.e. one
|
||||
year minus the default 30-day margin). If the user then anticipates the
|
||||
delivery time (locktime) to something earlier than that stale threshold - e.g.
|
||||
"in 1 month" - two different checks that compare locktime against
|
||||
``date_to_check`` (the resolved threshold) would incorrectly treat the will as
|
||||
"expired"/"invalid", even though the whole Check Alive concept is supposed to
|
||||
be inert in BASIC mode.
|
||||
|
||||
The fix (``BalWindow.compute_date_to_check``) makes ``date_to_check`` track
|
||||
the delivery time LIVE in BASIC mode, placed a fixed 2-hour margin before it,
|
||||
so it is always < locktime by construction. In ADVANCED mode nothing changes:
|
||||
the stored threshold is used as-is.
|
||||
|
||||
These tests call the real production method directly (no GUI/Electrum wallet
|
||||
needed - it is a plain ``@staticmethod``), so they exercise the exact code
|
||||
used at runtime rather than a re-implementation.
|
||||
|
||||
Run:
|
||||
PYTHONPATH=electrum-src python3 -m pytest tests/test_group_i_basic_checkalive.py -q
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.gui.qt.window import BalWindow # noqa: E402 (path insert above)
|
||||
|
||||
|
||||
OFFSET = BalWindow.BASIC_MODE_CHECK_ALIVE_OFFSET_SECONDS
|
||||
|
||||
|
||||
def test_offset_is_two_hours():
|
||||
"""Owner-approved margin: exactly 2 hours."""
|
||||
assert OFFSET == 2 * 60 * 60
|
||||
|
||||
|
||||
def _relative_days_to_midnight_timestamp(days):
|
||||
"""Reproduce Util.parse_locktime_string's own normalisation: "now + N
|
||||
days", truncated to midnight. Used to compute the expected value in
|
||||
tests without duplicating the parsing logic itself."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
now = datetime.now()
|
||||
return (
|
||||
(now + timedelta(days=days))
|
||||
.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
.timestamp()
|
||||
)
|
||||
|
||||
|
||||
def test_basic_mode_relative_locktime_one_year():
|
||||
"""BASIC + default "1y" delivery: date_to_check tracks it, 2h earlier."""
|
||||
result = BalWindow.compute_date_to_check(True, "1y", "30d")
|
||||
expected_locktime = _relative_days_to_midnight_timestamp(365)
|
||||
assert abs(result - (expected_locktime - OFFSET)) < 5
|
||||
|
||||
|
||||
def test_basic_mode_anticipated_delivery_stays_consistent():
|
||||
"""BASIC + delivery anticipated to 1 month: date_to_check follows it,
|
||||
NOT the old ~11-month threshold - this is the exact bug scenario
|
||||
reported by the owner."""
|
||||
stale_threshold = "30d" # would resolve close to the OLD 1-year locktime
|
||||
anticipated_locktime = "30d" # user moved delivery to ~1 month from now
|
||||
result = BalWindow.compute_date_to_check(
|
||||
True, anticipated_locktime, stale_threshold
|
||||
)
|
||||
locktime_ts = _relative_days_to_midnight_timestamp(30)
|
||||
# date_to_check must be (delivery - 2h), always strictly before delivery.
|
||||
assert result < locktime_ts
|
||||
assert abs((locktime_ts - OFFSET) - result) < 5
|
||||
|
||||
|
||||
def test_basic_mode_date_to_check_always_before_locktime():
|
||||
"""Regression guard for the reported bug: whatever the delivery date is
|
||||
(even very close to "now"), date_to_check must stay before it."""
|
||||
for relative_locktime in ("1d", "7d", "30d", "90d", "365d"):
|
||||
result = BalWindow.compute_date_to_check(True, relative_locktime, "30d")
|
||||
parsed_locktime = _relative_days_to_midnight_timestamp(
|
||||
int(relative_locktime[:-1])
|
||||
)
|
||||
assert result < parsed_locktime, (
|
||||
f"date_to_check ({result}) should be before locktime "
|
||||
f"({parsed_locktime}) for locktime={relative_locktime}"
|
||||
)
|
||||
|
||||
|
||||
def test_advanced_mode_uses_stored_threshold_unchanged():
|
||||
"""ADVANCED mode: behaviour must stay exactly as before this fix - the
|
||||
stored threshold is used as-is, regardless of the locktime value."""
|
||||
absolute_threshold = time.time() + 5 * 86400 # arbitrary user-chosen value
|
||||
result = BalWindow.compute_date_to_check(False, "30d", absolute_threshold)
|
||||
assert abs(result - absolute_threshold) < 1
|
||||
|
||||
|
||||
def test_basic_mode_falls_back_on_unparsable_locktime():
|
||||
"""If the locktime can't be parsed for any reason, BASIC mode must not
|
||||
crash: it falls back to the stored threshold, same as ADVANCED."""
|
||||
absolute_threshold = time.time() + 5 * 86400
|
||||
result = BalWindow.compute_date_to_check(
|
||||
True, {"not": "a valid locktime"}, absolute_threshold
|
||||
)
|
||||
assert abs(result - absolute_threshold) < 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_offset_is_two_hours()
|
||||
test_basic_mode_relative_locktime_one_year()
|
||||
test_basic_mode_anticipated_delivery_stays_consistent()
|
||||
test_basic_mode_date_to_check_always_before_locktime()
|
||||
test_advanced_mode_uses_stored_threshold_unchanged()
|
||||
test_basic_mode_falls_back_on_unparsable_locktime()
|
||||
print("All test_group_i_basic_checkalive tests passed.")
|
||||
Reference in New Issue
Block a user