Read the plugin version at runtime from bal/manifest.json via importlib.resources (zip-safe, works from inside a zip on Windows). Removes the four-file duplication (VERSION, __init__.py, plugin_base.py, manifest.json) and deletes the bal/VERSION file. - bal/core/plugin_base.py: new get_version() function, BalPlugin.version is now a @property, removed hardcoded __version__ - bal/__init__.py: removed __version__, replaced with a comment - bal/core/willexecutors.py: user-agent uses get_version() - bal/gui/qt/dialogs.py, widgets.py: call sites use .version property - tests/test_version_source.py: 7 tests incl. zip-safe subprocess check - HANDOFF.md: updated to single-source-of-truth documentation - CHANGELOG.md: entries 43-46 - bal/manifest.json: version bumped to 0.6.1 PR #4 (branch bitcoinafterlife-patch-5).
18 KiB
HANDOFF — BAL (Bitcoin After Life) Electrum plugin
Purpose: let ANY future AI assistant (Claude or another model, more advanced or cheaper) resume work on this project with full context, without having to re-discover the codebase. Read this file FIRST, then
CHANGELOG.mdand.agent_memory_tasks.md.
0. TL;DR — what this project is
- Product: BAL ("Bitcoin After Life") — an inheritance plugin for the Electrum 4.7.2 Bitcoin wallet (Qt / PyQt6).
- Form: external ZIP plugin (not bundled in Electrum). The user installs the ZIP from Electrum's plugin manager.
- What it does: lets a wallet owner pre-build, sign and (later) broadcast
Bitcoin transactions that pay one or more heirs after a chosen date
(a future UNIX-timestamp
nLockTime). Optional will-executors (remote services) can be paid a fee to broadcast the inheritance when due. The owner periodically proves they are alive ("check-alive"); if the deadline passes, the inheritance becomes spendable. - Current version: see the
"version"field ofbal/manifest.json(the single source of truth; read at runtime viaget_version()inbal/core/plugin_base.py).
1. MANDATORY working rules (the owner set these — always follow them)
These are non-negotiable. They come from the owner directly.
- R1 — LANGUAGE. The CHAT language with the owner is Italian. But ALL
output — source code, comments, docstrings, UI strings, docs,
CHANGELOG.md, commit messages, this handoff — must be in ENGLISH. - R2 — DOCUMENTED CODE. Every method/class gets a docstring + explanatory comments. Always explain WHY for any non-obvious decision.
- R3 — NEVER INVENT. If something is missing or unclear, STOP and ask the owner clear, simple questions. The owner is NOT a programmer — explain in plain language, avoid jargon. Be "100% sure" before acting.
- R4 — HUMAN CHECKPOINT. Before writing/modifying code, present the PLAN and WAIT for an explicit "OK" from the owner.
- METHOD: DISCOVER → PLAN (wait for OK) → EXECUTE → VERIFY → ITERATE (max ~8 attempts per problem, then step back and ask).
- LOG: keep a single
CHANGELOG.md, in English, one numbered entry per task (newest entry appended at the END of the file). - ZIP-FIRST. Deliver a test ZIP and let the owner test it BEFORE committing. Commit ONLY after the owner explicitly confirms the ZIP works.
- ALWAYS run
ruff+ the official test suite before committing / reporting / zipping. - CREDIT-SAVING (important). The owner is low on funds. Minimize token / credit usage: report brief summaries (do NOT paste whole modified code blocks back), and batch work into a single ZIP/test cycle where possible.
2. Repository layout (what lives where)
bal/ <- the plugin package (this is what ships in the ZIP)
__init__.py <- package docstring (no version here anymore)
manifest.json <- plugin manifest, "version" field (SINGLE SOURCE OF TRUTH for the version)
core/
plugin_base.py <- get_version() reads the version from manifest.json (zip-safe)
heirs.py <- HEIRS + transaction building (prepare_lists,
prepare_transactions, buildTransactions). CORE LOGIC.
will.py <- Will/WillItem, validation (check_amounts, check_will),
exceptions (AmountException, WillExpiredException, ...).
willexecutors.py <- remote will-executor services handling.
util.py <- locktime parsing/most helpers (timestamps only).
gui/qt/
common.py <- shared imports; every gui module does
`from .common import *`. Add new shared imports HERE.
dialogs.py <- the big build/sign/broadcast dialog
(BalBuildWillDialog, task_phase1/2), wizard glue.
widgets.py <- WillSettingsWidget + wizard widgets/labels.
window.py <- BalWalletWindow (build_will, check_will, get_transactions).
lists.py, calendar.py, theme.py, window_utils.py, ...
tests/ <- pytest suite (see run command below).
electrum-src/ <- a copy of Electrum source, used ONLY for tests
(PYTHONPATH=electrum-src). NOT shipped in the ZIP.
build_zip.py <- builds the shippable ZIP (37 files).
CHANGELOG.md <- numbered task log (English).
.agent_memory_tasks.md <- terse internal memory notes per task batch.
HANDOFF.md <- this file.
3. How to build, test and lint
Run everything from /home/user/webapp.
Full test suite (expected: 266 passed as of v0.4.8):
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 -m pytest \
tests/test_core_*.py tests/test_gui_*.py \
tests/test_anticipate_past_locktime.py tests/test_anticipate_manual_locktime.py \
tests/test_group_b_auto_sign.py tests/test_group_c_settings.py \
tests/test_group_d_alarms.py tests/test_group_e_mock_karen7.py \
tests/test_group_f_heir_change_rebuild.py tests/test_group_g_basic_calendar.py \
tests/test_group_h_v048.py -q
Lint (only NEW errors matter; ignore pre-existing noise):
ruff check <files> | grep -oE "^[^ ]+\.py:[0-9]+:[0-9]+: [A-Z][0-9]+" \
| grep -vE "F401|F403|F405|F841"
Pre-existing, KNOWN-OK ruff noise: F401/F403/F405 (star-imports via
from .common import *) and 2× F841 (an unused e in two except blocks).
Do NOT "fix" these unless asked — they are intentional / out of scope.
Build the ZIP (always clear caches first so zipimport doesn't ship stale .pyc):
find bal -name "__pycache__" -type d -exec rm -rf {} + ; find bal -name "*.pyc" -delete
python3 build_zip.py bal-electrum-plugin-vX.Y.Z.zip # produces 37 files
Bump version — ONE file only (single source of truth):
bal/manifest.json -> "version": "X.Y.Z",
The code reads this at runtime via get_version() in bal/core/plugin_base.py (exposed as the BalPlugin.version property), so there is nothing else to keep in sync. There is no longer a bal/VERSION file nor a hardcoded __version__.
IMPORTANT for the owner when testing: after installing a ZIP, the owner
must fully restart Electrum (not just reload the plugin) — Electrum's
zipimport caches modules, so a partial reload runs stale code.
4. Key technical knowledge (hard-won — saves you hours)
- Locktimes are UNIX timestamps only. Block-height locktimes were removed (CHANGELOG #1). Ordering/expiry compare timestamps.
heirs.pydata shape. An heir is a list indexed by constants (heirs.pytop):HEIR_ADDRESS=0,HEIR_AMOUNT=1(sats or"<n>%"),HEIR_LOCKTIME=2,HEIR_REAL_AMOUNT=3(resolved sats, or the string"DUST: <n>"when below the dust limit),HEIR_DUST_AMOUNT=4(raw dust sats).- Will-executor pseudo-heirs. Internally, each selected will-executor is
injected as a fake "heir" whose NAME starts with the reserved marker
w!ll3x3c"(i.e.'w!ll3x3c"' + url + '"' + str(locktime)). Its amount is the executorbase_fee(always non-dust). When you count/iterate "real" heirs you MUST skip names starting withw!ll3x3c". - Transaction-building pipeline:
window.build_will()→Heirs.get_transactions()(recursive over locktimes) →Heirs.buildTransactions()→Heirs.prepare_lists()(builds thelocktimesdict for ALL future locktimes, resolves amounts, marks dust) andprepare_transactions()(builds ONE tx for the LOWEST locktime only; the recursion handles the others via leftoveravailable_utxos). - DUST logic (v0.4.7 — verify before touching):
- The "all heirs are dust" guard lives at the END of
prepare_lists(NOT inprepare_transactions). Reason:prepare_transactionsonly sees the single lowest locktime, so a guard there would FALSE-POSITIVE block a will whose later locktimes still have valid heirs.prepare_listsis the only place that sees ALL heirs across ALL locktimes with their final dust state (fixed AND percentage). - Guard: count real heirs (skip
w!ll3x3c"); if there are real heirs but NONE has a valid (non-"DUST")HEIR_REAL_AMOUNT, raiseHeirAmountIsDustException(defined inheirs.py). A mix of dust + valid heirs keeps building normally. - Critical nuance: with FIXED amounts and a LARGE balance, leftover funds
are REDISTRIBUTED (
normalize_perc(..., real=True)), so small fixed amounts end up with a VALIDHEIR_REAL_AMOUNT(not dust). The real all-dust case is small balance + percentage heirs (matches the owner's log: shares of 214 / 316 / 3 sat). Tests reproduce this withprepare_lists(800, 100, wallet)and"40%"/"60%"heirs. - The exception is NOT a
WillExecutorFeeException, so it skips that handler inbuildTransactionsand propagates cleanly to the GUI. - GUI:
dialogs.py task_phase1has a dedicatedexcept HeirAmountIsDustExceptionBEFORE the genericexcept Exception. It shows a RED message and stops (return False, None) — no signing/checking, no empty will in the list.HeirAmountIsDustExceptionis imported incommon.pyand re-exported viafrom .common import *.
- The "all heirs are dust" guard lives at the END of
broadcast_transactionreturnsNone(Electrumnetwork.py). To get a txid, usetx.txid()— do NOT rely on the broadcast return value (this was the root cause of the missing "BAL Invalidate transaction" label, CHANGELOG #21 / v0.4.5).- Qt label truncation gotcha (CHANGELOG #22). A
QLabeladded withalignment=Qt.AlignmentFlag.AlignLeftis NOT stretched by Qt, so word-wrap computes on a narrow sizeHint and the text gets truncated. Fix: drop the alignment flag, addsetSizePolicy(Expanding, Minimum)+setMinimumWidth. WithsetWordWrap(True), an explicit\nin the text forces a line break. BalBuildWillDialogreport area. Messages are accumulated as HTML inself.labelsand joined bymsg_update("<br><br>".join(...),\n→<br>). The report is inside aQScrollArea(v0.4.8:setMinimumHeight(450),setMaximumHeight(700)); the Close button sits BELOW the scroll area so it stays reachable. Each report row is set viamsg_set_status(label, row, status, color); passingrow=NoneAPPENDS a new line (used to add an extra note without overwriting an existing row), passing the saved row id OVERWRITES it (used bymsg_set_checking, which reusesself.check_row).- BASIC vs ADVANCED (USER TYPE). A global setting
USER_TYPE(plugin_base.py, config keybal_user_type, default"basic"). Read it viabal_plugin.is_basic_mode(). ADVANCED reveals the Raw/Date selector and the Check-Alive field; BASIC hides them and disables the check-alive postpone behaviour. Toggling it callsBalWindow.update_all(), which callsWillSettingsWidget.apply_user_type_visibility()on each open settings widget. - Raw/Date selector visibility (v0.4.8 / task #04 — important gotcha). The
WILL/HEIR tab toolbars are built ONCE and REUSED for the whole session; they
are NOT rebuilt when USER TYPE changes. So any per-widget state decided only in
__init__(like the Raw/Date combo's visibility) gets "stuck". The fix pattern: give the widget anapply_user_type_visibility()method that re-readsis_basic_mode()and re-applies visibility WITHOUT changing the value/editor, and call it fromWillSettingsWidget.apply_user_type_visibility()(already wired forlocktimeandthreshold). The wizard avoids the bug only because it is recreated on every open. Keep this in mind for any future per-widget ADVANCED-dependent UI. - Will-item status flags (CONFIRMED / MEMPOOL).
Will.check_will(core/will.py) sets each will item's status toCONFIRMED/MEMPOOL(read withwitem.get_status("CONFIRMED")etc.). After an inheritance is executed the wallet is fully EMPTIED, so a later CHECK makescheck_willexecutors_and_heirsraiseNotCompleteWillException(heirs no longer match the empty wallet). v0.4.8 addsdialogs.py::_executed_inheritance_status()(CONFIRMED wins over MEMPOOL) to show a reassuring note instead of an alarming "the will must be rebuilt" only. - Dates shown in the plugin are LOCAL time, not UTC. The Date editor
(
LockTimeDateEdit, aQDateTimeEdit) usesdatetime.fromtimestamp(ts)with NO timezone, i.e. the user's local time. So an Italian user sees Italian time; the on-chainnLockTimeis the same instant expressed in UTC (e.g. ts1782370800= 2026-06-25 09:00 Italy = 07:00 UTC). There is a helperWill._format_locktimethat renders a timestamp in UTC. (A planned "(UTC)" label in the wizard is currently SUSPENDED — see suspended item below.)
5. Git / delivery workflow
- Branch: work on
genspark_ai_developer. Open PRs intomain. - Commit policy: ZIP-FIRST — build a test ZIP, let the owner confirm it works, THEN commit. (This differs from "commit after every change"; the owner explicitly prefers ZIP-first because they manually test each build.)
- Before opening/updating a PR:
git fetch origin main, rebase, resolve conflicts preferring remotemainunless a local change is essential, squash local commits into ONE comprehensive commit, push (force if needed), then create/update the PR and SHARE the PR URL with the owner. - ZIPs are NOT committed (
.gitignoreexcludes*.zip). They are distributed via GitHub Releases (gh release create vX.Y.Z file.zip ...). The newest release is the "Latest" and is the owner's convenient download. - Deliverable ZIPs are ALSO uploaded with the file-wrapper tool so the owner can download them directly from chat.
- Auth note: if
git push/ghfails with "Invalid username or token", re-run the GitHub environment setup, then retry. - PR history for this line of work: #13 (v0.4.7), #14 (docs/DUST section +
translation), #15 (v0.4.8). All merged into
main. - Releases: latest is v0.4.8 (asset
bal-electrum-plugin-v0.4.8.zip); v0.4.7 kept in history.
6. Version history (short — full detail in CHANGELOG.md)
- v0.4.5 — fix invalidation loop; add "BAL Invalidate transaction" label on
the automatic path (root cause:
broadcast_transactionreturns None → usetx.txid()); fix wizard text truncation. - v0.4.6 — DUST one-line-per-heir report; heirs on one line; scrollable
report area; wizard final check (
on_next_wenow callscheck_transactions); wizard truncation fix (remove AlignLeft); anticipated- date notice styling. - v0.4.7 — report area opens 500px tall (max 700); heirs reverted to ONE
per line (green/bold); explicit
\nline breaks in two wizard texts (after "(or backup)" and after "miner fees"); ALL-DUST guard inprepare_liststhat blocks (clear RED message) only when EVERY heir is dust; 3 new tests pinning the dust behaviour. 258 tests pass. - v0.4.8 — seven UX fixes: (#04) Raw/Date selector now reappears on the
WILL/HEIR tabs when switching to ADVANCED (and on a raw value from the wizard),
via a new
BalTimeEditWidget.apply_user_type_visibility(); (#3) Building Will report min height 500→450; (#5) "User Type" setting moved to the bottom (above "Rebroadcast transactions"); (#6) enabling ADVANCED now requires typing "at My Risk" (case-insensitive); (#7a) on CHECK with an emptied wallet, an extra note on "Checking your will": "Inheritance already executed (on blockchain)" GREEN / "Inheritance in mempool (waiting confirmation)" ORANGE; (#7b) "Balance is too low… Skipped" recoloured ORANGE + space fix; Reset button renamed "Reset to Default Setting". 8 new tests (test_group_h_v048.py). 266 tests pass.
Open / suspended / backlog items (see .agent_memory_tasks.md for detail)
- SUSPENDED — "(UTC)" label in the wizard. The owner asked to show an "(UTC - Greenwich Time)" hint next to the date with a tooltip. We discovered the date field shows LOCAL time, not UTC, so the label would be misleading. Options proposed (A: label it "Local time"; B: also show the UTC equivalent; C: convert the field to UTC). The owner suspended it ("salta le modifiche UTC per ora"). Decide the approach with the owner before implementing.
- Backlog (NOT started, analysis saved): #01 misleading "heir not found" message when the date was only anticipated; #02 will-executor list should green-check only servers that responded + green ping, re-evaluated each download; #03 missing "BAL Invalidate transaction" history label when invalidating from the AUTO-opened window (linked to a "unify invalidate procedure" task). Task #04 is DONE (v0.4.8).
7. How to resume (checklist for the next AI)
- Read this file, then
CHANGELOG.md(last entries) and.agent_memory_tasks.md. - Confirm the environment:
git status, current branch, and the"version"field ofbal/manifest.json. - Run the full test suite (Section 3) — expect all green (266 as of v0.4.8).
- Talk to the owner in Italian, write everything else in English.
- For any change: present a PLAN, wait for "OK" (R4), then implement, test, build a ZIP, let the owner test, and only commit after explicit confirmation.
- Keep credit usage low: summarize, don't paste big code blocks; batch work.
- When the owner confirms a ZIP works: commit (ZIP-first), sync with
main, squash to one commit, push, open a PR, merge it, then create/refresh a GitHub Release with the ZIP attached (it becomes the owner's "Latest" download). Always give the owner the PR URL and the Release URL.