Bump plugin version to 0.3.4 (manifest, __init__, plugin_base, VERSION). GROUP A - A1: remove block-height locktimes; the plugin now uses UNIX timestamps only. The NLOCKTIME_BLOCKHEIGHT_MAX guard is kept on purpose (it forces every locktime to be a timestamp). chk_locktime is now 2-arg; int_locktime and anticipate_locktime no longer accept blocks; RAW input only accepts d/y. Two now-dormant configs (LOCKTIME_BLOCKS, LOCKTIMEDELTA_BLOCKS) are kept with comments to avoid touching persisted keys. - A2: rename PENDING -> MEMPOOL everywhere (label 'Mempool', yellow #ffce30); add new UPDATED status; ANTICIPATED & UPDATED keep VALID; documented set_status rules; backward-compat migration (old PENDING -> MEMPOOL). - A3: clarify that anticipating to a future date only rebuilds (never invalidates), while only a past locktime invalidates (WillExpired). Code was already correct; only the comment and docs were fixed. Colour follow-up: UPDATED lightened from #800080 to #b266b2 (more readable), updated in theme.py, docs and the theme test. GROUP B - B1: verified the 'Create your will' button already opens the guided wizard (no code change needed). - B2: new persisted AUTO_SIGN setting (default ON) with an 'Auto-sign on Check' checkbox in the settings dialog. When enabled, Check signs and broadcasts automatically; the wallet password is requested only for encrypted wallets. B2 follow-up (fixes reported after testing): - Remove the duplicate sign/broadcast cycle in lists.check(); build_will_task() already signs and broadcasts. - Suppress the manual 'press Sign/Broadcast' hint and its popup when AUTO_SIGN is ON (kept when OFF). - Make broadcast one-shot: removed the retry flag and the Exception('retry'); failed will-executors stay PUSH_FAIL and are skipped (no endless retry). PUSHED transactions are already excluded from re-collection. Docs: inheritance-options.md/.html and inheritance-flow.svg updated to v0.3.4. Tests: 206 passing (new test_anticipate_manual_locktime, test_anticipate_past_locktime, test_group_b_auto_sign; updated core/util, core/will_extra, gui/theme, gui/widgets). CHANGELOG.md added with one numbered entry per task.
17 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.pystr_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. Thew(wallet) argument is kept only for call-site compatibility (now unused).int_locktime: removed theblocksargument (and theblocks * 600seconds-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 theblocksargument and the block-height branch; it now only moves a timestamp earlier (by hours/days). The Windows overflow clamp and theout < 1clamp are kept.- Expanded the
LOCKTIME_THRESHOLDcomment to explain the timestamp-only model.
-
bal/core/will.py- Removed the
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAXimport (it was only used by the dead block-height branch). check_will: removed theblock_to_checkparameter.is_will_valid: removed theblock_to_checkparameter.check_will_expired: removed theblock_to_checkparameter and theif locktime <= NLOCKTIME_BLOCKHEIGHT_MAX:block-height branch; expiry is now decided purely by comparing the locktime againsttimestamp_to_check.- Added/expanded docstrings on the three methods above.
- KEPT
utxo.block_heightin the coinbase-maturity check (line ~399): that is a legitimate Electrum UTXO attribute, NOT a BAL locktime concept.
- Removed the
-
bal/gui/qt/window.pycheck_will: stopped passing the removedblock_to_checkargument.init_class_variables: removed the deadlocktime_blocks,current_blockandblock_to_check = 0assignments (replaced by an explanatory comment).
-
bal/gui/qt/widgets.pyLockTimeRawEdit: removed the"b"(block) suffix handling fromreplace_str,numbifyand theisblocksflag; 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(noblocks),test_chk_locktime(2-arg),test_anticipate_locktime(noblocks). Addedimport pytest.tests/test_core_will_extra.py:test_check_willnow calls the 4-argcheck_will.tests/test_anticipate_past_locktime.py:chk_locktimecall updated to 2-arg.tests/test_gui_widgets.py:test_locktime_raw_edit_replace_strnow expects"b"to be left untouched.
Verification:
ruff checkon all modified production files: no new errors introduced (bal/core/util.pyis 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.pystill 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->MEMPOOLeverywhere (STATUS_DEFAULT,set_status, and the threeget_status(...)reads incheck_invalidated/search_rai). Visible label: "Mempool". - Added the new status
UPDATEDtoSTATUS_DEFAULT(label "Updated"). set_status: VALID rules now documented and updated:INVALIDATED,REPLACED,CONFIRMED,MEMPOOL-> clear VALID.ANTICIPATEDandUPDATED-> KEEP VALID (intentionally NOT in the clear-VALID list).CONFIRMED,MEMPOOL-> clear INVALIDATED (unchanged behaviour).
- Added a full docstring to
set_statusexplaining all side effects. - Backward-compatibility migration (owner decision "Modo B"): in
__init__, a will saved by an older plugin version that stores the legacyPENDINGflag is migrated toMEMPOOL, so no state is lost on load. The newMEMPOOLkey wins if both are present.
- Renamed the status key
-
bal/gui/qt/theme.py- Renamed the
PENDINGcolour entry toMEMPOOL(#ffce30 yellow, unchanged). - Added
UPDATED-> #800080 (violet) in the priority list, placed after REPLACED and before CONFIRMED/MEMPOOL.
- Renamed the
-
Tests:
tests/test_gui_theme.py: renamed the pending colour test totest_color_mempool, updated the "overrides lower" test, and addedtest_color_updated(#800080).tests/test_core_will_extra.py: renamedtest_check_invalidated_pending->test_check_invalidated_mempool; updatedtest_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 checkon 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), NEVERWillExpiredException. 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_heirsexplaining 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 bycheck_will_expired. No logic change.
- Added a clarifying comment in
-
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), andtest_a2_past_locktime_is_genuinely_expired(A2 = WillExpired kept).
- Permanent regression tests guaranteeing:
-
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 checkon 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).
- UPDATED colour changed from
-
docs/inheritance-options.md/docs/inheritance-options.html- Updated the colour table and the
.pill.violetCSS to#b266b2("light violet").
- Updated the colour table and the
-
tests/test_gui_theme.pytest_color_updatednow 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 opensBalWizardDialog- 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.
- Added a new persisted setting
-
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.
- Added an "Auto-sign on Check" checkbox bound to
-
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 reusesask_password_and_sign_transactions(callback=...); the existingget_wallet_password()already prompts only for encrypted wallets, so a password-less wallet is handled with no prompt.
- Added
-
bal/gui/qt/lists.py(check)- After the server check, when
AUTO_SIGNis enabled, callsauto_sign_and_broadcast(). When the setting is OFF the behaviour is unchanged (check only).
- After the server check, when
-
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 checkon 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:
- The "Building Will" dialog still showed "Next step (manual): press 'Broadcast'..." even though the will had already been broadcast.
- A second, duplicate popup ("Informazioni") repeated the same manual hint.
- 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 runsBalBuildWillDialog.build_will_task()(which already checks, signs and broadcasts), and then a secondauto_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 aretryflag and raisedException("retry")whenever any will-executor failed.
What changed (Fix A - no duplicate broadcast / no wrong manual hint):
-
bal/gui/qt/lists.pycheck()no longer callsauto_sign_and_broadcast(). Signing and broadcasting are already done bybuild_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.
- Removed the now-unused
-
bal/gui/qt/dialogs.py_show_next_steps_hint()returns early (no in-dialog hint, no popup) whenAUTO_SIGNis ON, because the will has already been signed and broadcast. WhenAUTO_SIGNis 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
retryflag, theretry_flag["value"] = Trueassignments and the finalif 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_transactionsalready excludes PUSHED transactions, so the successful ones are never re-sent on a later run.
- Removed the
-
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 checkon changed Python files: no new errors introduced (new test file is ruff-clean).- Full test suite:
206 passed.
Outcome: DONE.