Files
bal-electrum-plugin/CHANGELOG.md
donkey-ai 9d2cbc2814 feat(bal): Group C settings-dialog improvements (editable dates, narrower RAW box, warning/reset/support, tooltips, bold locktime)
C2 - 'Editable dates' option (default OFF):
  - new EDITABLE_DATES config; checkbox in settings dialog
  - WillSettingsWidget.apply_editable_dates() re-locks/unlocks the
    delivery-time and check-alive fields, called from update_all() so the
    toggle takes effect immediately (same mechanism as Hide Invalidated)
  - EDITABLE_DATES included in the Reset-to-defaults list

C3 - RAW date box narrowed to ~1/3 (input box 6 chars); the trailing
  label that shows the computed absolute date kept at 10 chars (fixed a
  truncation regression)

C4 - settings dialog:
  (a) bold red warning at the top
  (b) 'Reset setting' button restoring all 7 dialog settings to defaults
      (single source of truth: BalConfig.default), not touching wills
  (c) bold 'Support: bitcoin-after.life' link (opens via webopen)

C5 - tooltips: calendar 'Export reminder dates to your calendar (.ics)',
  fee field 'Mining fee rate in sat/vByte used for the will transactions',
  fee '丰' icon 'Miner fee, click for more information' (tooltip font scoped
  to QPushButton so it matches the other tooltips)

C6 - Locktime column rendered in bold in the will list

tests/test_group_c_settings.py: 4 new tests (EDITABLE_DATES default/toggle,
  reset restores all dialog settings incl. EDITABLE_DATES, reset leaves
  unrelated config untouched). Full suite: 210 passed.
2026-06-28 23:00:30 -04:00

23 KiB

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.


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.