Files
bal-electrum-plugin/HANDOFF.md
kaibot 6d568bf304 Fix: NoneType error in normalize_will when others_inputs is None
When deleting old transactions and clicking Check, task_phase1 would fail
with 'NoneType object has no attribute get' error in WillItem.normalize_locktime.

Root cause: In Will.normalize_will (will.py:150), the parameter others_inputs
defaults to None. At line 151, a local variable 'others_input' is created with
a safe default value (empty dict). However, lines 179 and 184 still used the
original 'others_inputs' parameter instead of the safe local variable, causing
the error when calling .get() on None.

Fix: Use the safe 'others_input' variable instead of the raw parameter in
lines 179 and 184.

Also added:
- Traceback logging to on_error_phase1 so future errors include full call stack
- Debug logging in update_all to log willitems state before processing
- Comprehensive test (test_e5_build_with_real_wallet_heirs_and_utxos) that
  reproduces the exact scenario from the user's karen7 wallet
2026-06-29 12:03:24 -04:00

305 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.md` and
> `.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 `bal/VERSION` (last shipped: **0.4.8**).
---
## 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 <- __version__ (one of 4 version files)
VERSION <- plain-text version (one of 4 version files)
manifest.json <- plugin manifest, "version" field (one of 4)
core/
plugin_base.py <- __version__ "AUTOMATICALLY GENERATED" (one of 4)
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):**
```bash
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):**
```bash
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):**
```bash
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 — there are FOUR files, keep them in sync:**
```
bal/core/plugin_base.py -> __version__ = "X.Y.Z" # AUTOMATICALLY GENERATED DO NOT EDIT
bal/__init__.py -> __version__ = "X.Y.Z"
bal/VERSION -> X.Y.Z
bal/manifest.json -> "version": "X.Y.Z",
```
**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.py` data shape.** An heir is a list indexed by constants
(`heirs.py` top): `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 executor `base_fee` (always non-dust). When you count/iterate "real"
heirs you MUST skip names starting with `w!ll3x3c"`.
- **Transaction-building pipeline:**
`window.build_will()``Heirs.get_transactions()` (recursive over locktimes)
`Heirs.buildTransactions()``Heirs.prepare_lists()` (builds the
`locktimes` dict for ALL future locktimes, resolves amounts, marks dust)
and `prepare_transactions()` (builds ONE tx for the LOWEST locktime only;
the recursion handles the others via leftover `available_utxos`).
- **DUST logic (v0.4.7 — verify before touching):**
- The "all heirs are dust" guard lives at the END of `prepare_lists`
(NOT in `prepare_transactions`). Reason: `prepare_transactions` only sees
the single lowest locktime, so a guard there would FALSE-POSITIVE block a
will whose later locktimes still have valid heirs. `prepare_lists` is 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`, raise
`HeirAmountIsDustException` (defined in `heirs.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 VALID `HEIR_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 with
`prepare_lists(800, 100, wallet)` and `"40%"/"60%"` heirs.
- The exception is NOT a `WillExecutorFeeException`, so it skips that handler
in `buildTransactions` and propagates cleanly to the GUI.
- GUI: `dialogs.py task_phase1` has a dedicated `except
HeirAmountIsDustException` BEFORE the generic `except Exception`. It shows a
RED message and stops (`return False, None`) — no signing/checking, no
empty will in the list. `HeirAmountIsDustException` is imported in
`common.py` and re-exported via `from .common import *`.
- **`broadcast_transaction` returns `None`** (Electrum `network.py`). To get a
txid, use `tx.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 `QLabel` added with
`alignment=Qt.AlignmentFlag.AlignLeft` is NOT stretched by Qt, so word-wrap
computes on a narrow sizeHint and the text gets truncated. Fix: drop the
alignment flag, add `setSizePolicy(Expanding, Minimum)` + `setMinimumWidth`.
With `setWordWrap(True)`, an explicit `\n` in the text forces a line break.
- **`BalBuildWillDialog` report area.** Messages are accumulated as HTML in
`self.labels` and joined by `msg_update` (`"<br><br>".join(...)`, `\n`→`<br>`).
The report is inside a `QScrollArea` (v0.4.8: `setMinimumHeight(450)`,
`setMaximumHeight(700)`); the Close button sits BELOW the scroll area so it
stays reachable. Each report row is set via `msg_set_status(label, row,
status, color)`; passing `row=None` APPENDS a new line (used to add an extra
note without overwriting an existing row), passing the saved row id OVERWRITES
it (used by `msg_set_checking`, which reuses `self.check_row`).
- **BASIC vs ADVANCED (USER TYPE).** A global setting `USER_TYPE`
(`plugin_base.py`, config key `bal_user_type`, default `"basic"`). Read it via
`bal_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 calls `BalWindow.update_all()`, which calls
`WillSettingsWidget.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 an `apply_user_type_visibility()` method that re-reads
`is_basic_mode()` and re-applies visibility WITHOUT changing the value/editor,
and call it from `WillSettingsWidget.apply_user_type_visibility()` (already
wired for `locktime` and `threshold`). 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 to `CONFIRMED` / `MEMPOOL`
(read with `witem.get_status("CONFIRMED")` etc.). After an inheritance is
executed the wallet is fully EMPTIED, so a later CHECK makes
`check_willexecutors_and_heirs` raise `NotCompleteWillException` (heirs no
longer match the empty wallet). v0.4.8 adds
`dialogs.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`, a `QDateTimeEdit`) uses `datetime.fromtimestamp(ts)` with
NO timezone, i.e. the user's local time. So an Italian user sees Italian time;
the on-chain `nLockTime` is the same instant expressed in UTC (e.g. ts
`1782370800` = 2026-06-25 09:00 Italy = 07:00 UTC). There is a helper
`Will._format_locktime` that 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 into `main`.
- **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 remote `main` unless 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** (`.gitignore` excludes `*.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` / `gh` fails 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_transaction` returns None → use
`tx.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_we` now calls
`check_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 `\n` line breaks in two wizard texts
(after "(or backup)" and after "miner fees"); **ALL-DUST guard** in
`prepare_lists` that 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)
1. Read this file, then `CHANGELOG.md` (last entries) and `.agent_memory_tasks.md`.
2. Confirm the environment: `git status`, current branch, `bal/VERSION`.
3. Run the full test suite (Section 3) — expect all green (266 as of v0.4.8).
4. Talk to the owner in **Italian**, write everything else in **English**.
5. 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.
6. Keep credit usage low: summarize, don't paste big code blocks; batch work.
7. 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.