UI batch (TASK A/B/C/D): - (A) Clearer 'Will expired' message: shortened will id (8+8 chars) and readable UTC date instead of raw UNIX timestamp; shown in WARNING colour (orange) instead of ERROR (red) in the wizard, as it is an expected step. - (A2) Split the orange message onto two lines via <br> (rendered as HTML by msg_warning) to avoid an overly long single line. - (B) History tab labels (text only): inheritance txs -> 'BAL Inheritance transaction'; invalidate tx -> 'BAL Invalidate transaction'. Colours left to Electrum defaults. - (C) New 'No will-executor TX' checkbox in plugin settings, bound to the existing NO_WILLEXECUTOR config (default ON, kept in sync with the wizard), with help text, and included in the settings reset action. - (D) Wizard button label 'Create your will' -> 'Build Your Will'. Regression fix (A3): - When an heir was added to an already-expired will via the wizard, the automatic invalidation no longer triggered. Root cause: the inner check_will() after build_will() raised WillExpiredException and the handler returned (False, None), which never invalidated. - Auto-opening the invalidation tx window from the closing wizard proved unreliable (the window ended up behind the main wallet on some window managers; a re-check loop could ask to invalidate repeatedly before the tx reached the mempool). The robust fix: close the wizard and show a clear instruction popup guiding the user to run 'Tools -> Invalidate' and then press 'Check' to finish the will. Reasoning documented in the code. Version bumped to 0.3.9 (manifest.json, __init__.py, plugin_base.py, VERSION). CHANGELOG entry #15 updated. Verified: py_compile OK, ruff clean (no new errors), full test suite 239 passed.
856 lines
41 KiB
Markdown
856 lines
41 KiB
Markdown
# CHANGELOG
|
|
|
|
This file records the work done on the BAL (Bitcoin After Life) Electrum
|
|
inheritance plugin, one numbered entry per task.
|
|
|
|
Each entry lists: task title, date, files changed, and outcome
|
|
(DONE / UNRESOLVED). It is meant to make it easy to review what was done and,
|
|
if needed, to roll back to a previous state.
|
|
|
|
---
|
|
|
|
<!-- New entries are added below, newest last. -->
|
|
|
|
## 1. A1 - Remove block-height locktimes (timestamps only)
|
|
|
|
**Date:** 2026-06-22
|
|
|
|
**Goal (OPUS plan, Group A / A1):** Remove block-height locktimes from the
|
|
codebase so that ALL ordering and comparison use UNIX timestamps only. The
|
|
`NLOCKTIME_BLOCKHEIGHT_MAX` guard is intentionally KEPT, because it forces every
|
|
locktime to be a timestamp (it is a safety "bouncer", not block-height ordering).
|
|
|
|
**What changed:**
|
|
|
|
- `bal/core/util.py`
|
|
- `str_to_locktime`: removed the `"b"` (block) suffix; only `"d"` (days) and
|
|
`"y"` (years) relative suffixes are accepted now.
|
|
- `parse_locktime_string`: removed the block-height (`"<n>b"`) branch. The
|
|
`w` (wallet) argument is kept only for call-site compatibility (now unused).
|
|
- `int_locktime`: removed the `blocks` argument (and the `blocks * 600`
|
|
seconds-per-block conversion). New signature:
|
|
`int_locktime(seconds=0, minutes=0, hours=0, days=0)`.
|
|
- `chk_locktime`: signature changed from
|
|
`(timestamp_to_check, block_height_to_check, locktime)` to
|
|
`(timestamp_to_check, locktime)`; comparison is now purely timestamp-based.
|
|
- `anticipate_locktime`: removed the `blocks` argument and the block-height
|
|
branch; it now only moves a timestamp earlier (by hours/days). The Windows
|
|
overflow clamp and the `out < 1` clamp are kept.
|
|
- Expanded the `LOCKTIME_THRESHOLD` comment to explain the timestamp-only model.
|
|
|
|
- `bal/core/will.py`
|
|
- Removed the `from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX` import
|
|
(it was only used by the dead block-height branch).
|
|
- `check_will`: removed the `block_to_check` parameter.
|
|
- `is_will_valid`: removed the `block_to_check` parameter.
|
|
- `check_will_expired`: removed the `block_to_check` parameter and the
|
|
`if locktime <= NLOCKTIME_BLOCKHEIGHT_MAX:` block-height branch; expiry is
|
|
now decided purely by comparing the locktime against `timestamp_to_check`.
|
|
- Added/expanded docstrings on the three methods above.
|
|
- KEPT `utxo.block_height` in the coinbase-maturity check (line ~399): that is
|
|
a legitimate Electrum UTXO attribute, NOT a BAL locktime concept.
|
|
|
|
- `bal/gui/qt/window.py`
|
|
- `check_will`: stopped passing the removed `block_to_check` argument.
|
|
- `init_class_variables`: removed the dead `locktime_blocks`, `current_block`
|
|
and `block_to_check = 0` assignments (replaced by an explanatory comment).
|
|
|
|
- `bal/gui/qt/widgets.py`
|
|
- `LockTimeRawEdit`: removed the `"b"` (block) suffix handling from
|
|
`replace_str`, `numbify` and the `isblocks` flag; only `"d"` and `"y"` remain.
|
|
- Added a clarifying comment on the kept guard
|
|
`LockTimeDateEdit.min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1`.
|
|
|
|
- Tests updated for the new signatures / behaviour:
|
|
- `tests/test_core_util.py`: `test_str_to_locktime` (now expects `"144b"` to be
|
|
rejected), `test_int_locktime` (no `blocks`), `test_chk_locktime` (2-arg),
|
|
`test_anticipate_locktime` (no `blocks`). Added `import pytest`.
|
|
- `tests/test_core_will_extra.py`: `test_check_will` now calls the 4-arg
|
|
`check_will`.
|
|
- `tests/test_anticipate_past_locktime.py`: `chk_locktime` call updated to 2-arg.
|
|
- `tests/test_gui_widgets.py`: `test_locktime_raw_edit_replace_str` now expects
|
|
`"b"` to be left untouched.
|
|
|
|
**Verification:**
|
|
- `ruff check` on all modified production files: no new errors introduced
|
|
(`bal/core/util.py` is clean; pre-existing baseline warnings unchanged).
|
|
- Full test suite: `190 passed`
|
|
(`tests/test_core_*.py tests/test_gui_*.py tests/test_anticipate_past_locktime.py`).
|
|
|
|
**Dormant block-based config kept on purpose (owner decision: keep, comment why):**
|
|
- `bal/core/plugin_base.py` still defines two block-based stored configs that
|
|
are no longer read anywhere after A1:
|
|
- `LOCKTIME_BLOCKS` (`"bal_locktime_blocks"`)
|
|
- `LOCKTIMEDELTA_BLOCKS` (`"bal_locktimedelta_blocks"`)
|
|
Per the owner's decision they are KEPT in place (dormant) to avoid touching
|
|
persisted config keys that may already exist in some users' saved settings.
|
|
An explanatory comment was added above each line stating they are unused now
|
|
and why they are intentionally retained.
|
|
|
|
**Outcome:** DONE.
|
|
|
|
---
|
|
|
|
## 2. A2 - Status definitions, colours and VALID rules (MEMPOOL rename + UPDATED)
|
|
|
|
**Date:** 2026-06-22
|
|
|
|
**Goal (OPUS plan, Group A / A2):** Define the inheritance statuses, their
|
|
colours and their VALID rules. The fine moment-by-moment assignment of
|
|
ANTICIPATED / UPDATED will be validated by the Group E tests; A2 sets up the
|
|
state machine and colours.
|
|
|
|
**Status meanings (for reference):**
|
|
- **ANTICIPATED** - locktime anticipated by 1 day vs a pre-existing tx with the
|
|
same heirs; the tx STAYS VALID.
|
|
- **REPLACED** - an input is spent by a new tx with a LOWER locktime; loses
|
|
VALID, cascades to children.
|
|
- **INVALIDATED** - an input is spent by a mempool/confirmed tx and the previous
|
|
tx is no longer in the will; loses VALID.
|
|
- **UPDATED** (new) - the tx was spendable AND valid, and a new tx replaces it
|
|
keeping the SAME locktime and SAME heirs; STAYS VALID.
|
|
- **MEMPOOL** (renamed from PENDING) - the tx has been seen in the Electrum
|
|
mempool; loses VALID.
|
|
- **CONFIRMED** - the tx is confirmed in the blockchain; loses VALID.
|
|
|
|
**What changed:**
|
|
|
|
- `bal/core/will.py`
|
|
- Renamed the status key `PENDING` -> `MEMPOOL` everywhere
|
|
(`STATUS_DEFAULT`, `set_status`, and the three `get_status(...)` reads in
|
|
`check_invalidated` / `search_rai`). Visible label: "Mempool".
|
|
- Added the new status `UPDATED` to `STATUS_DEFAULT` (label "Updated").
|
|
- `set_status`: VALID rules now documented and updated:
|
|
- `INVALIDATED`, `REPLACED`, `CONFIRMED`, `MEMPOOL` -> clear VALID.
|
|
- `ANTICIPATED` and `UPDATED` -> KEEP VALID (intentionally NOT in the
|
|
clear-VALID list).
|
|
- `CONFIRMED`, `MEMPOOL` -> clear INVALIDATED (unchanged behaviour).
|
|
- Added a full docstring to `set_status` explaining all side effects.
|
|
- **Backward-compatibility migration (owner decision "Modo B"):** in
|
|
`__init__`, a will saved by an older plugin version that stores the legacy
|
|
`PENDING` flag is migrated to `MEMPOOL`, so no state is lost on load. The new
|
|
`MEMPOOL` key wins if both are present.
|
|
|
|
- `bal/gui/qt/theme.py`
|
|
- Renamed the `PENDING` colour entry to `MEMPOOL` (#ffce30 yellow, unchanged).
|
|
- Added `UPDATED` -> #800080 (violet) in the priority list, placed after
|
|
REPLACED and before CONFIRMED/MEMPOOL.
|
|
|
|
- Tests:
|
|
- `tests/test_gui_theme.py`: renamed the pending colour test to
|
|
`test_color_mempool`, updated the "overrides lower" test, and added
|
|
`test_color_updated` (#800080).
|
|
- `tests/test_core_will_extra.py`: renamed `test_check_invalidated_pending`
|
|
-> `test_check_invalidated_mempool`; updated `test_check_will`. Added new
|
|
tests: `test_legacy_pending_migrates_to_mempool`,
|
|
`test_new_mempool_wins_over_legacy_pending`, `test_updated_status_keeps_valid`,
|
|
`test_anticipated_status_keeps_valid`, `test_mempool_status_clears_valid`.
|
|
|
|
**Verification:**
|
|
- `ruff check` on modified production files: no new errors introduced.
|
|
- Full test suite: `196 passed`
|
|
(`tests/test_core_*.py tests/test_gui_*.py tests/test_anticipate_past_locktime.py`).
|
|
|
|
**Note:** the exact moment ANTICIPATED / UPDATED get assigned during real flows
|
|
is validated by the Group E tests (per the owner's decision).
|
|
|
|
**Outcome:** DONE.
|
|
|
|
---
|
|
|
|
## 3. A3 - "Move date earlier (anticipate)" must not invalidate
|
|
|
|
**Date:** 2026-06-22
|
|
|
|
**Goal (OPUS plan, Group A / A3):** (a) explain the inheritance-options table;
|
|
(b) make "Move date earlier (anticipate)" only anticipate (rebuild), NOT
|
|
invalidate; (c) regenerate a clearer English table.
|
|
|
|
**Owner decisions captured during DISCOVER:**
|
|
- D1 = the case to handle is the user MANUALLY setting a smaller locktime in the
|
|
wizard (e.g. from "90 days" to "30 days").
|
|
- D2 = case A1 (smaller locktime, still in the future): plain REBUILD with the
|
|
new locktime, NEVER invalidate, even if the tx was already signed/sent.
|
|
- D3 = the genuine-expiry case (new date in the past) keeps invalidating; only
|
|
the documentation is wrong and must be fixed.
|
|
- Chosen path = X (fix BOTH code clarity and documentation).
|
|
|
|
**Analysis result (verified with tests, not assumed):**
|
|
The core logic was ALREADY correct for D2. A diagnostic confirmed:
|
|
- Case A1 (smaller, future locktime, signed OR unsigned) ->
|
|
`HeirNotFoundException` (a rebuild signal), NEVER `WillExpiredException`. So no
|
|
on-chain invalidation happens. This already matches the owner's decision.
|
|
- Case A2 (locktime in the past) -> `WillExpiredException` -> invalidation. This
|
|
is a genuine expiry and is intentionally kept.
|
|
|
|
So the real defect was in the DOCUMENTATION (the table wrongly claimed
|
|
"anticipate -> always invalidate"). No behavioural code change was needed.
|
|
|
|
**What changed:**
|
|
|
|
- `bal/core/will.py`
|
|
- Added a clarifying comment in `check_willexecutors_and_heirs` explaining that
|
|
anticipating to a FUTURE date is a plain rebuild that NEVER invalidates (even
|
|
when signed/sent), while only a date in the PAST is a genuine expiry handled
|
|
by `check_will_expired`. No logic change.
|
|
|
|
- `tests/test_anticipate_manual_locktime.py` (new)
|
|
- Permanent regression tests guaranteeing:
|
|
`test_a1_anticipate_unsigned_triggers_rebuild_not_invalidate`,
|
|
`test_a1_anticipate_signed_triggers_rebuild_not_invalidate` (A1 = rebuild, no
|
|
invalidate, signed or not), and
|
|
`test_a2_past_locktime_is_genuinely_expired` (A2 = WillExpired kept).
|
|
|
|
- `docs/inheritance-options.md`
|
|
- Split the "Move date EARLIER" row into two: "still in the future" (rebuild,
|
|
no fee, signed or not) vs "into the past" (invalidate, WillExpired).
|
|
- Rewrote the "why anticipate" notes (anticipate is safe, opposite of postpone).
|
|
- Fixed the quick-reference summary table and the Golden Rules accordingly.
|
|
- Updated the status section to match A2 (MEMPOOL instead of PENDING; added
|
|
ANTICIPATED/UPDATED keep-VALID rows) and rewrote the colour table in the exact
|
|
priority order used by `gui/qt/theme.py` (incl. UPDATED #800080 violet).
|
|
- Updated the Mermaid decision flow to the corrected branching.
|
|
|
|
- `docs/inheritance-options.html`
|
|
- Mirrored all the above .md changes (status table, colour table with new pill
|
|
colours, section 4.1 table + notes, summary table, Golden Rules, Mermaid).
|
|
- Bumped the document version footer to v0.3.4.
|
|
|
|
- `docs/images/inheritance-flow.svg`
|
|
- Reworked the "anticipate" branch: the decision is now "new date in the PAST?"
|
|
-> invalidate (WillExpired); otherwise "moved earlier? (anticipate)" ->
|
|
rebuild only, NEVER invalidates. Bumped subtitle to v0.3.4. Verified the SVG
|
|
is still well-formed XML.
|
|
|
|
**Verification:**
|
|
- `ruff check` on changed Python files: no new errors introduced.
|
|
- Full test suite: `199 passed`
|
|
(`tests/test_core_*.py tests/test_gui_*.py tests/test_anticipate_past_locktime.py
|
|
tests/test_anticipate_manual_locktime.py`).
|
|
|
|
**Outcome:** DONE.
|
|
|
|
---
|
|
|
|
## 4. A2 follow-up - UPDATED status colour lightened
|
|
|
|
**Date:** 2026-06-22
|
|
|
|
**Goal:** After testing the v0.3.4 build, the original UPDATED colour
|
|
(`#800080`, dark violet) was reported as too dark to read in the list.
|
|
Lighten it to a more readable light violet while keeping it distinct from
|
|
MEMPOOL (yellow `#ffce30`).
|
|
|
|
**What changed:**
|
|
|
|
- `bal/gui/qt/theme.py`
|
|
- UPDATED colour changed from `#800080` (dark violet) to `#b266b2`
|
|
(light violet) in `_STATUS_COLOR_PRIORITY`. Priority position unchanged
|
|
(after REPLACED, before CONFIRMED/MEMPOOL).
|
|
|
|
- `docs/inheritance-options.md` / `docs/inheritance-options.html`
|
|
- Updated the colour table and the `.pill.violet` CSS to `#b266b2`
|
|
("light violet").
|
|
|
|
- `tests/test_gui_theme.py`
|
|
- `test_color_updated` now asserts `#b266b2`.
|
|
|
|
**Verification:**
|
|
- Full test suite: see run below.
|
|
|
|
**Outcome:** DONE.
|
|
|
|
---
|
|
|
|
## 5. B1 + B2 - Guided wizard verification and Auto-sign on Check
|
|
|
|
**Date:** 2026-06-22
|
|
|
|
**Goal (OPUS plan, Group B):**
|
|
- **B1:** the "Create your will" button should open the step-by-step guided
|
|
wizard.
|
|
- **B2:** the "Check" action should be able to automatically sign and broadcast
|
|
the will, controlled by an "Auto-sign" checkbox in the settings dialog,
|
|
default ON.
|
|
|
|
**B1 - finding (no code change needed):**
|
|
- The "Create your will" toolbar button is already wired to
|
|
`BalWindow.init_wizard`, which opens `BalWizardDialog` - the step-by-step
|
|
wizard (Heirs -> Locktime & Fee -> Will-Executor download -> Will-Executor ->
|
|
build will). This already matches the requested behaviour, so no code change
|
|
was required for B1.
|
|
|
|
**B2 - what changed:**
|
|
|
|
- `bal/core/plugin_base.py`
|
|
- Added a new persisted setting `AUTO_SIGN`
|
|
(`BalConfig(config, "bal_auto_sign", True)`), default ON, with an
|
|
explanatory comment.
|
|
|
|
- `bal/gui/qt/plugin.py` (`settings_dialog`)
|
|
- Added an "Auto-sign on Check" checkbox bound to `AUTO_SIGN`, with a tooltip
|
|
explaining that the wallet password is requested only if the wallet is
|
|
encrypted. Re-numbered the grid rows so the new checkbox (row 3) does not
|
|
overlap the existing widgets; also fixed the "Event sescription" ->
|
|
"Event description" label typo.
|
|
|
|
- `bal/gui/qt/window.py`
|
|
- Added `auto_sign_and_broadcast()`: signs the will and, only after signing
|
|
succeeds, broadcasts it to the will-executors. It reuses
|
|
`ask_password_and_sign_transactions(callback=...)`; the existing
|
|
`get_wallet_password()` already prompts only for encrypted wallets, so a
|
|
password-less wallet is handled with no prompt.
|
|
|
|
- `bal/gui/qt/lists.py` (`check`)
|
|
- After the server check, when `AUTO_SIGN` is enabled, calls
|
|
`auto_sign_and_broadcast()`. When the setting is OFF the behaviour is
|
|
unchanged (check only).
|
|
|
|
- `tests/test_group_b_auto_sign.py` (new)
|
|
- 5 tests: AUTO_SIGN defaults ON and can be disabled; sign happens before
|
|
broadcast; encrypted wallet prompts for the password; un-encrypted wallet
|
|
signs and broadcasts with no prompt.
|
|
|
|
**Verification:**
|
|
- `ruff check` on changed Python files: no new errors introduced
|
|
(new test file is ruff-clean).
|
|
- Full test suite: `204 passed`.
|
|
|
|
**Outcome:** DONE.
|
|
|
|
---
|
|
|
|
## 6. B2 follow-up - Remove duplicate broadcast and make broadcast one-shot
|
|
|
|
**Date:** 2026-06-22
|
|
|
|
**Problems reported after testing the v0.3.4 Group B build:**
|
|
1. The "Building Will" dialog still showed "Next step (manual): press
|
|
'Broadcast'..." even though the will had already been broadcast.
|
|
2. A second, duplicate popup ("Informazioni") repeated the same manual hint.
|
|
3. When several transactions were broadcast to different will-executors and one
|
|
server failed, the plugin tried to retry the broadcast, which could loop
|
|
forever against a server that never answers.
|
|
|
|
**Root cause:**
|
|
- `lists.py check()` first runs `BalBuildWillDialog.build_will_task()` (which
|
|
already checks, signs and broadcasts), and then a second
|
|
`auto_sign_and_broadcast()` was triggered - a duplicate sign/broadcast cycle.
|
|
- The "Building Will" dialog always printed the manual "press Sign/Broadcast"
|
|
hint and a follow-up popup, which is wrong when broadcasting is automatic.
|
|
- `loop_push()` used a `retry` flag and raised `Exception("retry")` whenever any
|
|
will-executor failed.
|
|
|
|
**What changed (Fix A - no duplicate broadcast / no wrong manual hint):**
|
|
|
|
- `bal/gui/qt/lists.py`
|
|
- `check()` no longer calls `auto_sign_and_broadcast()`. Signing and
|
|
broadcasting are already done by `build_will_task()`; the duplicate cycle
|
|
was removed (replaced by an explanatory comment).
|
|
|
|
- `bal/gui/qt/window.py`
|
|
- Removed the now-unused `auto_sign_and_broadcast()` method.
|
|
|
|
- `bal/gui/qt/dialogs.py`
|
|
- `_show_next_steps_hint()` returns early (no in-dialog hint, no popup) when
|
|
`AUTO_SIGN` is ON, because the will has already been signed and broadcast.
|
|
When `AUTO_SIGN` is OFF the previous manual hints are kept.
|
|
|
|
**What changed (Fix B - one-shot broadcast, no endless retry):**
|
|
|
|
- `bal/gui/qt/dialogs.py` (`loop_push`)
|
|
- Removed the `retry` flag, the `retry_flag["value"] = True` assignments and
|
|
the final `if retry: raise Exception("retry")`. The broadcast now contacts
|
|
each selected will-executor ONCE: successful transactions become PUSHED,
|
|
failed/timed-out ones are left as PUSH_FAIL and simply skipped (no automatic
|
|
retry). The user can broadcast a failed transaction manually later.
|
|
- Note: `get_willexecutor_transactions` already excludes PUSHED transactions,
|
|
so the successful ones are never re-sent on a later run.
|
|
|
|
- `tests/test_group_b_auto_sign.py`
|
|
- Rewritten for the new behaviour: AUTO_SIGN default/disable; manual hint
|
|
suppressed when AUTO_SIGN ON and shown when OFF; PUSHED transactions are not
|
|
re-collected for broadcast (one-shot), while not-yet-PUSHED ones are.
|
|
|
|
**Verification:**
|
|
- `ruff check` on changed Python files: no new errors introduced
|
|
(new test file is ruff-clean).
|
|
- Full test suite: `206 passed`.
|
|
|
|
**Outcome:** DONE.
|
|
|
|
## 7. Group C - Settings-dialog improvements (editable dates, narrower RAW box, warning/reset/support, tooltips, bold locktime)
|
|
|
|
**Context / request:**
|
|
Group C bundles several usability improvements to the BAL settings dialog and
|
|
the will views, implemented together and delivered in a single ZIP.
|
|
|
|
**What changed (C2 - "Editable dates" option):**
|
|
|
|
- `bal/core/plugin_base.py`
|
|
- New persisted config `EDITABLE_DATES = BalConfig(config, "bal_editable_dates",
|
|
False)` (default OFF).
|
|
|
|
- `bal/gui/qt/widgets.py`
|
|
- `WillSettingsWidget.__init__` reads `EDITABLE_DATES`. When OFF (default) the
|
|
delivery-time and check-alive date fields stay display-only outside the
|
|
"Build your will" wizard; when ON they become editable in the toolbar /
|
|
Heirs tab. The fee field remains read-only outside the wizard.
|
|
|
|
- `bal/gui/qt/plugin.py`
|
|
- New "Editable dates" checkbox in the settings dialog, bound to
|
|
`EDITABLE_DATES`, with an explanatory tooltip. Toggling it refreshes the
|
|
open windows so the date fields immediately reflect the new state.
|
|
|
|
**What changed (C3 - narrower RAW date box):**
|
|
|
|
- `bal/gui/qt/widgets.py`
|
|
- `LockTimeRawEdit` width reduced from `12 * char_width_in_lineedit()` to
|
|
`6 *` (roughly a third), enough for short relative values such as `30d` /
|
|
`1y` while still fitting larger day counts.
|
|
- `TimeRawEditWidget`'s trailing (empty) label shrunk from `10 *` to `2 *`
|
|
character widths, removing the wasted blank space.
|
|
|
|
**What changed (C4 - warning, reset, support link):**
|
|
|
|
- `bal/gui/qt/plugin.py`
|
|
- (a) Warning shown in bold red at the TOP of the dialog:
|
|
"Warning: change these settings only if you know what you are doing."
|
|
- (b) "Reset" button that restores the six dialog settings (Hide Replaced,
|
|
Hide Invalidated, Auto-sign, Calendar App, Event summary, Event description)
|
|
to their factory defaults, taking each default from `BalConfig.default`
|
|
(single source of truth) and refreshing the widgets. It does NOT touch
|
|
wills, will-executors or any other configuration.
|
|
- (c) Clickable support link to `https://bitcoin-after.life`, opened via
|
|
Electrum's `webopen` helper.
|
|
- The dialog now uses an outer vertical layout (warning -> settings grid ->
|
|
Reset/support row); the settings grid is unchanged apart from the new row.
|
|
|
|
- `bal/gui/qt/common.py`
|
|
- Imported `webopen` from `electrum.gui.qt.util` (re-exported for the dialog).
|
|
|
|
**What changed (C5 - clearer tooltips):**
|
|
|
|
- `bal/gui/qt/widgets.py`
|
|
- Calendar button tooltip: "Export reminder dates to your calendar (.ics)".
|
|
- Fee field tooltip: "Mining fee rate in sat/vByte used for the will
|
|
transactions".
|
|
|
|
**What changed (C6 - bold Locktime column):**
|
|
|
|
- `bal/gui/qt/lists.py`
|
|
- In `PreviewList.replace()` the Locktime column item is now rendered in bold
|
|
(via its own `QFont`), so the delivery time stands out in the list. The rest
|
|
of the list is intentionally left unchanged.
|
|
|
|
- `tests/test_group_c_settings.py` (new)
|
|
- Verifies `EDITABLE_DATES` defaults OFF and can be toggled/persisted, and
|
|
that the Reset logic restores all six dialog settings to their defaults
|
|
without touching unrelated configuration.
|
|
|
|
**Verification:**
|
|
- `ruff check` on changed Python files: no new errors introduced (the new test
|
|
file is ruff-clean; the only added import flagged by ruff is the re-export
|
|
`webopen`, matching the existing star-import pattern in `common.py`).
|
|
- Full test suite: `210 passed` (206 previous + 4 new Group C tests).
|
|
|
|
**Outcome:** DONE (delivered as a ZIP for user testing before commit).
|
|
|
|
### Group C - follow-up fixes (after first user test)
|
|
|
|
- **C2 (Editable dates) now takes effect immediately and is reset.**
|
|
- `bal/gui/qt/widgets.py`: extracted the date-locking logic into
|
|
`WillSettingsWidget.apply_editable_dates()`, which re-reads `EDITABLE_DATES`
|
|
and locks/unlocks the delivery-time and check-alive fields (fee stays
|
|
read-only). Called from `__init__` and re-callable afterwards.
|
|
- `bal/gui/qt/window.py`: `update_all()` now calls `apply_editable_dates()` on
|
|
the Heirs-tab and Will-tab settings widgets. Since the "Editable dates"
|
|
checkbox already triggers `update_all()`, toggling it now updates the date
|
|
fields instantly (same mechanism as the "Hide Invalidated" filter).
|
|
- `bal/gui/qt/plugin.py`: added `EDITABLE_DATES` (and its checkbox) to the
|
|
"Reset setting" list, so Reset also returns it to its default (OFF).
|
|
|
|
- **C3: fixed the broken date next to the RAW box.**
|
|
- `bal/gui/qt/widgets.py`: the trailing label in `TimeRawEditWidget` shows the
|
|
ABSOLUTE date computed from the RAW value (e.g. "30d" -> "2027-06-23").
|
|
Its width was wrongly shrunk to 2 characters, truncating that date; it is
|
|
restored to 10 characters. Only the RAW input box stays narrowed (6 chars).
|
|
|
|
- **C4b:** the reset button label is now "Reset setting".
|
|
- **C4c:** the support link text `bitcoin-after.life` is now shown in bold.
|
|
|
|
- `tests/test_group_c_settings.py`: the reset test now also covers
|
|
`EDITABLE_DATES` (seven settings restored to default, flag back OFF).
|
|
|
|
**Verification (follow-up):** `ruff` clean on changed files; full suite
|
|
`210 passed`.
|
|
|
|
### Group C - second follow-up (after second user test)
|
|
|
|
- **Fee icon tooltip (C5):** the small "丰" help icon next to the fee field now
|
|
shows the hover tooltip "Miner fee, click for more information" (the longer
|
|
explanation still appears on click via the HelpButton).
|
|
- **C4c:** the word "Support:" before the link is now also shown in bold (not
|
|
only the link text).
|
|
- **"Editable inheritance" Raw/Date default:** confirmed the existing behaviour
|
|
is "remember the user's last choice" (the Raw/Date combo selection is
|
|
persisted in `WILL_SETTINGS` whenever it changes), so no code change was
|
|
needed - the dialog reopens on whichever mode the user last used.
|
|
|
|
**Verification (second follow-up):** `ruff` clean on changed files; full suite
|
|
`210 passed`.
|
|
|
|
### Group C - third follow-up (tooltip font fix)
|
|
|
|
- **Fee icon tooltip font:** the "丰" button used an unscoped
|
|
`font-size: 16px` stylesheet that also enlarged its tooltip, making it bigger
|
|
than the other tooltips (e.g. the calendar one). The rule is now scoped to
|
|
`QPushButton{...}`, so only the glyph stays large while the tooltip uses the
|
|
default font size like the others.
|
|
|
|
**Verification (third follow-up):** full suite `210 passed`.
|
|
|
|
## 8. Group D / D1 - Configurable, distributed calendar reminders and save-only .ics
|
|
|
|
**Context / request:**
|
|
The exported calendar (.ics) used to add one reminder (VALARM) for every single
|
|
day of the check-alive period (potentially hundreds), and tried to open the file
|
|
with a calendar app. Group D D1 makes the number of reminders configurable
|
|
(default 3, max 5), spreads them across the period before the deadline, and
|
|
changes the calendar button to simply SAVE the .ics file (asking the user where,
|
|
starting on the Desktop) instead of opening it. (D2 was intentionally skipped.)
|
|
|
|
**What changed (D1 - number of reminders):**
|
|
|
|
- `bal/core/plugin_base.py`
|
|
- New persisted config `NUM_REMINDERS = BalConfig(config, "bal_num_reminders",
|
|
3)` (default 3).
|
|
|
|
- `bal/gui/qt/widgets.py`
|
|
- New `BalSpinBox` widget: an integer spin box bound to a `BalConfig`
|
|
(mirrors `BalCheckBox` / `BalLineEdit`), with a clamped range.
|
|
- New pure helper `compute_reminder_offsets(days, count)`: returns the
|
|
reminder offsets (in days before the deadline) spread uniformly across the
|
|
period. Every offset is >= 1 (reminders always fall before the deadline),
|
|
at most one reminder per available day (`min(count, days)`), de-duplicated,
|
|
sorted earliest-first. Examples: `(30, 3) -> [30, 16, 1]`,
|
|
`(2, 3) -> [2, 1]`, `(1, 3) -> [1]`, `(0, 3) -> []`.
|
|
- `create_alarms()` rewritten to read `NUM_REMINDERS`, use
|
|
`compute_reminder_offsets`, and emit one VALARM per offset (with a
|
|
DESCRIPTION reminder text, previously commented out).
|
|
|
|
- `bal/gui/qt/plugin.py`
|
|
- New "Number of reminders" spin box in the settings dialog (range 1..5,
|
|
default 3), with an explanatory tooltip, also included in the "Reset
|
|
setting" list (resets back to 3).
|
|
|
|
**What changed (D1b - save-only .ics, ask where, default to Desktop):**
|
|
|
|
- `bal/gui/qt/calendar.py`
|
|
- New `BalCalendar.desktop_dir()` helper returning the user's Desktop
|
|
(`~/Desktop` when present, otherwise the home directory).
|
|
|
|
- `bal/gui/qt/widgets.py`
|
|
- `open_or_save_calendar()` no longer tries to open the file with a calendar
|
|
app. It always opens a "save as" dialog (via `getSaveFileName`) with an
|
|
`.ics` filter, default filename `will_event.ics`, starting on the Desktop,
|
|
then copies the generated file to the chosen path and shows a confirmation
|
|
message. The unused `save_to_cwd` was replaced by `save_ics_to(target)`.
|
|
- The "Calendar App" setting is left in place (now unused) as requested.
|
|
|
|
- `tests/test_group_d_alarms.py` (new)
|
|
- Verifies `NUM_REMINDERS` default/change and all the distribution rules of
|
|
`compute_reminder_offsets` (spread, before-deadline, one-per-day cap, empty
|
|
when no room, never exceeding the requested count, single-reminder case).
|
|
|
|
**Verification:**
|
|
- `ruff check` on changed files: no new errors (new test file is ruff-clean).
|
|
- Full test suite: `217 passed` (210 previous + 7 new Group D tests).
|
|
|
|
**Outcome:** DONE (delivered as a ZIP for user testing before commit).
|
|
|
|
### Group D - follow-up ("Calendar App" removed from settings)
|
|
|
|
- `bal/gui/qt/plugin.py`: removed the "Calendar App" field from the settings
|
|
dialog (and from the "Reset setting" list). Since the calendar button now only
|
|
SAVES the .ics file (it no longer opens it with an external app), the setting
|
|
was no longer needed. The `CALENDAR_APP` config and the `open_with_default_app`
|
|
helper are left in the codebase (unused, harmless) to avoid touching unrelated
|
|
code.
|
|
- The .ics "save as" behaviour is identical on Windows, Linux and macOS (always
|
|
starts on the user's Desktop, with a home-directory fallback); no per-OS
|
|
branching.
|
|
|
|
**Verification (follow-up):** full suite `217 passed`.
|
|
|
|
## 9. Group E - Mock tests with fake wallet "giovanna7"
|
|
|
|
**Goal:** add automated, GUI-free mock tests covering four behaviour areas of
|
|
the plugin, all driven by a single self-contained fake wallet named
|
|
"giovanna7" (no real Electrum wallet file is needed).
|
|
|
|
**What was added:**
|
|
|
|
- `tests/test_group_e_mock_giovanna7.py` (new) - 22 tests in four sections:
|
|
- A fake wallet model (`GiovannaWallet`, `FakeDB`) whose `str(wallet)` is
|
|
`"giovanna7"`, pre-loaded with two heirs (alice, bob).
|
|
- **E1 - calendar / .ics:** reminder-offset distribution rules
|
|
(`compute_reminder_offsets`), VALARM/TRIGGER shape
|
|
(`TRIGGER;RELATED=END:-P{n}D`), iCalendar escaping of the event text, and
|
|
writing a temporary `.ics` file (`BalCalendar.write_temp_ics`).
|
|
- **E2 - inheritance / states:** loading/adding/removing giovanna7's heirs
|
|
(`Heirs`), and `WillItem` status transitions (VALID -> COMPLETE,
|
|
INVALIDATED clears VALID, PUSHED clears PUSH_FAIL), `Will.only_valid`, and
|
|
heir-change detection (`HeirNotFoundException`).
|
|
- **E3 - connectivity:** stubbing `Willexecutors.get_info_task` to prove that
|
|
`ping_servers_parallel` contacts giovanna7's servers concurrently (total
|
|
time far below the sequential sum), fires the per-server `on_each` callback
|
|
once with the correct ok flag, and writes results back into the mapping;
|
|
plus the empty-mapping no-op.
|
|
- **E4 - will-executor:** `is_selected` default/set behaviour, the
|
|
`get_willexecutor_transactions` filtering rule (only VALID + COMPLETE +
|
|
not-PUSHED + selected wills are pushed; `force=True` re-includes PUSHED
|
|
ones), and `compute_id`.
|
|
|
|
**Verification:**
|
|
- `ruff check` on the new test file: clean (all checks passed).
|
|
- Full test suite: `239 passed` (217 previous + 22 new Group E tests).
|
|
|
|
**Outcome:** DONE (delivered as a ZIP for user testing before commit).
|
|
|
|
## 10. Fix - black bar when cancelling the "invalidate old will" signature
|
|
|
|
**Bug:** when the user postpones a will's delivery date and the plugin asks to
|
|
invalidate the previous (earlier-locktime) will first, cancelling the signature
|
|
prompt left a black/undrawn rectangle at the bottom of the "Building Will"
|
|
dialog.
|
|
|
|
**Root cause:** in `bal/gui/qt/dialogs.py`, `on_success_phase1` handled the
|
|
cancelled-invalidation case with `self.wait(3)` followed by `self.close()`.
|
|
`wait()` uses `time.sleep()`, but `on_success_phase1` runs in the GUI thread, so
|
|
the sleep froze the interface for several seconds. While frozen, the dialog
|
|
could not repaint the area it had just resized, leaving an undrawn (black)
|
|
region at the bottom of the window.
|
|
|
|
**Fix (Variant A):**
|
|
- `bal/gui/qt/dialogs.py`
|
|
- In `on_success_phase1`, the cancelled-invalidation branch no longer calls
|
|
the blocking `self.wait(3)` + `self.close()`. It now marks the step as
|
|
"Aborted" and shows the existing non-blocking "Close" button
|
|
(`_add_close_button()`), so the GUI is never blocked and the bottom of the
|
|
dialog repaints correctly. The user reads the outcome and dismisses the
|
|
dialog when ready, consistent with the other end-of-flow branches.
|
|
|
|
**Verification:**
|
|
- `py_compile` on the changed file: OK.
|
|
- `ruff check`: no new errors (only the pre-existing F401/F403/F405 star-import
|
|
noise and an unrelated F841 at line 615).
|
|
- Full test suite: `239 passed`.
|
|
|
|
**Outcome:** DONE (delivered as a ZIP for user testing before commit).
|
|
|
|
## 11. Fee field now follows the "Editable dates" setting
|
|
|
|
**Request:** the "Editable dates" checkbox in the plugin settings (added in
|
|
Group C / C2) lets the user edit the delivery-time and check-alive dates
|
|
outside the wizard. The mining-fee field next to them, however, stayed always
|
|
read-only. The user asked for the fee to follow the same rule as the dates.
|
|
|
|
**What changed:**
|
|
- `bal/gui/qt/widgets.py`
|
|
- In `WillSettingsWidget.apply_editable_dates()`, the fee widget
|
|
(`baltx_fees`) is now locked/unlocked with `set_read_only(not
|
|
editable_dates)`, exactly like the locktime and threshold widgets, instead
|
|
of being forced to `set_read_only(True)`. The method docstring was updated
|
|
to state that the fee follows the same "Editable dates" rule.
|
|
|
|
**Effect:** with "Editable dates" ticked, the delivery time, check-alive date
|
|
AND the fee become editable outside the wizard; with it unticked, all three go
|
|
back to read-only. Inside the wizard everything stays editable as before. The
|
|
change takes effect immediately (the method is already re-run from
|
|
`BalWindow.update_all()`), with no need to reopen the window.
|
|
|
|
**Verification:**
|
|
- `py_compile` on the changed file: OK.
|
|
- `ruff check`: no new errors (only pre-existing star-import noise and unrelated
|
|
F841 warnings at lines 547 / 792).
|
|
- Full test suite: `239 passed`.
|
|
|
|
**Outcome:** DONE (delivered together with fix #10 in a single ZIP for user
|
|
testing before commit).
|
|
|
|
## 12. Translated the refactoring docs to English and renamed two of them
|
|
|
|
**Request:** per rule R1 (all output, including documentation, must be in
|
|
English), translate the remaining Italian documentation files. The user also
|
|
asked to translate the file *names* of two of them.
|
|
|
|
**What changed:**
|
|
- `CHANGELOG_REFACTOR.md`
|
|
- Translated sections 1-16 (the whole report header plus §1-§16) from Italian
|
|
to English. Sections 17-18 were already in English and were left untouched.
|
|
- All technical content was preserved verbatim: code blocks, commit hashes,
|
|
tables, file/class/method names, version numbers and structure.
|
|
- Updated the §12 history line to mention the new name of the diagnosis file
|
|
(`GUI_DIAGNOSIS.md`, originally `DIAGNOSI_GUI.md`).
|
|
- `DIAGNOSI_GUI.md` → renamed to `GUI_DIAGNOSIS.md` (via `git mv`) and fully
|
|
translated to English, **title included**. All code blocks, line references,
|
|
severity emojis, tables and structure were preserved.
|
|
- `REPORT_NETWORKING_PARALLELO.md` → renamed to `PARALLEL_NETWORKING_REPORT.md`
|
|
(via `git mv`). Its content was already in English, so only the file name was
|
|
translated.
|
|
|
|
**Verification:**
|
|
- Scanned the translated regions for leftover Italian words: none found (only
|
|
English false-positives from the regex).
|
|
- Confirmed sections 17-18 of `CHANGELOG_REFACTOR.md` are unchanged
|
|
(the diff hunks stop before §17).
|
|
- Confirmed no other file in the repo still references the old file names.
|
|
|
|
**Outcome:** DONE (documentation-only change; no plugin code touched, so the
|
|
zip-first step does not apply).
|
|
|
|
## 13. Calendar (.ics): separate reminder events instead of one event with alarms
|
|
|
|
**Request:** the exported `.ics` added a single calendar entry (on the locktime)
|
|
with internal VALARM reminders, which most calendars show as just one
|
|
appointment. The user asked for **N separate events** (default 3), each with its
|
|
own visible date, with the **last one one day before the inheritance locktime**.
|
|
|
|
**What changed (option A):**
|
|
- `bal/gui/qt/widgets.py`
|
|
- Rewrote `WillSettingsWidget.open_or_save_calendar()` to emit **one VEVENT
|
|
per reminder**, each placed on `locktime - offset` days, instead of one
|
|
VEVENT carrying VALARM blocks. The offsets come from the existing
|
|
`compute_reminder_offsets()`, so they are spread uniformly across the
|
|
check-alive period and the **last event is always one day before the
|
|
locktime**.
|
|
- Each event gets a **unique UID** (`bal-<wallet>-<offset>d`) so calendars do
|
|
not merge them, and its summary is suffixed with **" (reminder N/total)"**
|
|
to tell the events apart. The description still reuses `EVENT_DESCRIPTION`
|
|
with the usual `$wallet_name` / `$heirs_complete` substitutions.
|
|
- Removed the now-unused `create_alarms()` method (no more VALARM blocks).
|
|
- Changed the default save filename from `will_event.ics` to
|
|
**`BAL_will_event.ics`** (still defaulting to the Desktop).
|
|
- `bal/gui/qt/common.py`
|
|
- Added `timedelta` to the `datetime` import (needed for the per-event date
|
|
arithmetic; re-exported via the GUI star-import).
|
|
- `bal/core/plugin_base.py`
|
|
- Updated the `NUM_REMINDERS` comment to describe the new "separate events"
|
|
behaviour.
|
|
- `tests/test_group_e_mock_giovanna7.py`
|
|
- Replaced the old VALARM-shape E1 test with
|
|
`test_e1_build_separate_events_for_giovanna`, which asserts the new
|
|
structure: N distinct VEVENTs, no VALARM, unique UIDs, numbered summaries,
|
|
and the last event one day before the locktime.
|
|
|
|
**Effect:** with the default of 3 reminders over, say, a 30-day period, the
|
|
`.ics` now produces 3 separate calendar appointments (e.g. 30, 16 and 1 day
|
|
before the deadline) instead of a single one. Short periods automatically
|
|
produce fewer events (at most one per day).
|
|
|
|
**Verification:**
|
|
- `py_compile` on the changed files: OK.
|
|
- `ruff check`: no new errors.
|
|
- Full test suite: see run below.
|
|
|
|
**Outcome:** DONE (delivered as a test ZIP v0.3.7 for the user to try before
|
|
commit).
|
|
|
|
## 14. Check-alive help text: DATA/RAW explanation and two typo fixes
|
|
|
|
**Request:** rewrite the text inside the CHECK ALIVE help popup to explain both
|
|
the DATA mode and the RAW mode, while keeping the **CHECK ALIVE** title in bold.
|
|
|
|
**What changed:**
|
|
- `bal/gui/qt/widgets.py`
|
|
- Updated `ThresholdTimeWidget.help_text` with the new wording:
|
|
- kept `<b>CHECK ALIVE</b>` bold;
|
|
- added a **DATA mode** paragraph (set the check-alive date; on wallet open,
|
|
if the date has passed, the plugin asks whether to postpone the
|
|
inheritance, assuming the user is still alive and in control);
|
|
- added a **RAW mode** paragraph (when less than this time is missing, ask
|
|
to invalidate; failing to invalidate in time delivers the transactions to
|
|
the heirs);
|
|
- kept the existing "d / y suffix" explanation.
|
|
- Fixed two typos in this help text: "less then" -> "less **than**", and
|
|
"currrent" -> "**current**" (only within the CHECK ALIVE help text).
|
|
|
|
**Verification:**
|
|
- `py_compile` on the changed file: OK.
|
|
- `ruff check`: no new errors (only the pre-existing star-import noise and the
|
|
pre-existing F841 at line 547).
|
|
- Full test suite: `239 passed`.
|
|
|
|
**Outcome:** DONE (delivered as a test ZIP v0.3.8 for the user to try before
|
|
commit).
|
|
|
|
## 15. UI batch (v0.3.9): clearer "will expired" message, History labels, settings checkbox, wizard button
|
|
|
|
**Context:** A batch of four small UI improvements requested by the user
|
|
(internally tracked as TASK A/B/C/D).
|
|
|
|
**Changes:**
|
|
- **(A) "Will expired" message — clearer and not alarming.** Rewrote the message
|
|
raised by `Will.check_will_expired()` (`bal/core/will.py`): the will id is now
|
|
shortened (first 8 + last 8 chars, e.g. `9f1b0a75…fed9ae1b`) and the locktime
|
|
is shown as a readable UTC date (e.g. `2026-06-22 11:00 UTC`) instead of a raw
|
|
UNIX timestamp, with an explanation that the will will be invalidated and
|
|
re-signed. Two small helpers were added (`_short_will_id`, `_format_locktime`)
|
|
and `datetime`/`timezone` are now imported at module top. In the "Build your
|
|
will" wizard (`bal/gui/qt/dialogs.py`) an expired will is now shown in the
|
|
WARNING colour (orange) instead of the ERROR colour (red), because it is an
|
|
expected part of the flow, not an error.
|
|
- **(B) History tab labels (text only).** Renamed the labels written to
|
|
Electrum's History tab: inheritance transactions now read
|
|
`BAL Inheritance transaction` (was `BAL Transaction`, updated in both
|
|
`dialogs.py` and `window.py`) and the invalidate transaction now reads
|
|
`BAL Invalidate transaction` (was `BAL Invalidate`, in `window.py`). Colours
|
|
are left to Electrum's defaults (outgoing transactions are shown in red by
|
|
Electrum itself) to keep the change simple and robust.
|
|
- **(C) "No will-executor TX" checkbox in plugin settings.** Added a checkbox to
|
|
the settings dialog (`bal/gui/qt/plugin.py`) bound to the existing
|
|
`NO_WILLEXECUTOR` config (default ON), the same config used by the wizard's
|
|
will-executor download window, so the two stay in sync. Its help button reads:
|
|
"Create a will that does not require a Will-executor; it can be saved, for
|
|
example, on a USB stick, and a copy can be given to the heirs." It is also
|
|
included in the "Reset setting" action so a reset restores it to ON.
|
|
- **(D) Wizard button label.** The main wizard button now reads `Build Your Will`
|
|
(was `Create your will`, `bal/gui/qt/lists.py`), matching the tooltip and the
|
|
rest of the codebase which already call it the "Build your will" wizard.
|
|
|
|
- **(A2) Two-line expired message.** Following user feedback that the orange
|
|
message was too long on a single line, a `<br>` line break was added in
|
|
`Will.check_will_expired()` so the second sentence ("too late to anticipate,
|
|
the will will be invalidated and re-signed.") is shown on its own line. The
|
|
message is rendered as HTML by `msg_warning()`, so the `<br>` tag is honoured.
|
|
- **(A3) Auto-invalidate regression fix when adding an heir to an expired will.**
|
|
While verifying TASK A a regression was found: when an heir was added to an
|
|
already-expired will through the wizard, the automatic invalidation window no
|
|
longer opened (the user had to restart Electrum or press Check). Root cause:
|
|
in `dialogs.py::task_phase1`, adding an heir first raises
|
|
`HeirNotFoundException` (so the will is rebuilt by `build_will()`), and the
|
|
freshly rebuilt transactions are themselves expired, raising
|
|
`WillExpiredException` on the INNER `check_will()`. The original handler there
|
|
did `return False, None`, which never triggered invalidation. The fix makes
|
|
the wizard, in that case, behave exactly like the "Tools -> Invalidate" menu:
|
|
instead of trying to auto-open the invalidation transaction window (which
|
|
proved unreliable — depending on the OS window manager the transaction window
|
|
ended up BEHIND the main wallet window when the wizard closed, and an
|
|
automatic re-check loop could ask to invalidate repeatedly before the
|
|
invalidation tx reached the mempool), the wizard now closes and shows a clear
|
|
instruction popup telling the user to run `Tools -> Invalidate` themselves and
|
|
then press `Check`. That menu path is already known to work perfectly (its
|
|
transaction window stays in front and sets the `BAL Invalidate transaction`
|
|
history label) and it makes the user consciously aware of this deliberate,
|
|
important action. The reasoning behind this choice is documented in the code.
|
|
|
|
**Verification:**
|
|
- `py_compile` on all changed files: OK.
|
|
- `ruff check`: no new errors (only the pre-existing star-import noise and
|
|
pre-existing F841 warnings, none in the changed lines).
|
|
- Full test suite: `239 passed`.
|
|
|
|
**Outcome:** DONE (delivered as test ZIP v0.3.9, confirmed OK by the user before
|
|
commit).
|