core+gui+cli: network-fee invalidation, will-tree fixes, i18n refresh

- #64: the on-chain invalidation now pays the current network fee through a
  configurable fee-policy setting (default eta:2); baltx_fees is only the
  offline fallback. The change output is dust-guarded: coins are appended until
  the change clears the wallet dust threshold.
- #65: a will item whose father is missing/stale is handled defensively in
  check_invalidated and discarded by add_willtree instead of raising KeyError.
- #66: children are stored as a list (dict accepted for older data/tests).
- Refresh the translation catalogs and fix stale source line references.
- New tests for the invalidation fee policy, the dust guard and the father
  link handling.
This commit is contained in:
2026-10-12 15:16:50 -04:00
parent 346aeee62b
commit 3d9757fe2d
14 changed files with 2296 additions and 974 deletions

View File

@@ -3612,3 +3612,150 @@ deselected executor) are not part of it and will be proposed separately.
none new.
**Outcome:** DONE (pending Truman's review).
---
## 64. Invalidation pays the current network fee, configurable via a fee-policy setting
**Date:** 2026-10-10
**Goal (owner request):** the will's `baltx_fees` (default 20 sat/vbyte) is
deliberately high because the inheritance transactions may be broadcast by the
will-executors years from now, when network fees may be much higher. The
on-chain INVALIDATION, instead, is sent TODAY: it spends the will's coins back
to the wallet so the old pre-signed inheritance transactions become unusable.
It must therefore pay the current network fee, not the inheritance rate. After
the invalidation the plugin rebuilds the new inheritance with `baltx_fees` as
before (unchanged). The fee policy is exposed as an ADVANCED-only setting.
**What changed:**
- `bal/core/will.py`
- New constants `INVALIDATION_FEE_ETA_BLOCKS = 2` and
`INVALIDATION_FEE_POLICY_DEFAULT = "eta:2"` (owner decision: confirm within
2 blocks, Electrum's own default target).
- New `Will.get_invalidation_fee_rate(wallet, fallback_fee_per_byte,
fee_policy_descriptor=None)`: builds an Electrum `FeePolicy` from the
descriptor (Feerate / ETA / Mempool) and returns its rate in sat/vbyte,
together with the `FeeMethod` name (`"feerate"` / `"eta"` / `"mempool"`) or
`"fallback"`. The fallback (the will's `baltx_fees`, i.e. the previous
behaviour) is used when no usable rate is available: offline wallet,
estimates not received yet, network error or an invalid rate value. A
malformed stored descriptor is handled by Electrum's `FeePolicy`, which
falls back to the default `eta:2` policy (network rate), not to
`baltx_fees`. Never raises.
- `Will.invalidate_will` gained a `fee_policy_descriptor` argument and uses
that rate; the total fee is rounded UP to whole satoshis (network rates can
be fractional); it logs the chosen rate and its source. `fees_per_byte` is
now only the fallback, so every caller keeps working. The change output must
stay at or above the wallet dust threshold: coins are appended until the
change clears dust, and if it never does the invalidation returns `None`
(instead of an unrelayable sub-dust output). Docstrings added.
- `bal/core/plugin_base.py`: new `INVALIDATION_FEE_POLICY` BalConfig
(`bal_invalidation_fee_policy`, default `"eta:2"`).
- `bal/gui/qt/plugin.py`: new ADVANCED-only row 16 in the settings dialog,
reusing Electrum's standard fee widget (`FeeSlider` + `FeeComboBox`) for the
Feerate / ETA / Mempool choice, with a Help button and a per-row reset. The
chosen descriptor is persisted; the dialog-wide "Reset to Default Setting"
also restores it.
- `bal/gui/qt/window.py`, `bal/gui/qt/dialogs.py`, `bal/cli/controller.py`:
every `Will.invalidate_will` call site passes
`fee_policy_descriptor=...INVALIDATION_FEE_POLICY.get()`, so the Tools →
Invalidate action, the wizard postpone/expired paths, the automatic
invalidation and the CLI (`bal_will_invalidate`) all get the new behaviour.
- `bal/locale/bal.pot` and the `es_ES` / `it_IT` catalogs refreshed
(`pybabel extract` / `update`, Babel 2.10.3 to match the existing template) so
the new settings-dialog strings are translatable.
**Why no special case for an already-expired will:** discussed with the owner
and intentionally left out. Before its locktime the old inheritance tx cannot
enter a block or the mempool, so the invalidation has no competitor. RBF stays
enabled on the invalidation, so its fee can be raised from Electrum's History
("Increase fee") if it ever gets stuck.
**Verification:**
- `tests/test_core_invalidation_fee.py` (new, 17 tests): rate selection
(2-block target, network value used as is also above `baltx_fees`, fractional
rates, fallback when offline / no estimate / error / invalid values / invalid
descriptor / no wallet) and the fee actually deducted by `invalidate_will`
(network vs fallback, fixed feerate offline, rounding up).
- `tests/test_settings_invalidation_fee.py` (new, 7 tests): the row is hidden
in BASIC and visible in ADVANCED; choosing a Feerate / ETA policy persists
the descriptor; "Reset to Default Setting" restores `"eta:2"`.
- `tests/test_core_will_invalidate.py`: its mock wallet now has `network=None`
so the pure fee arithmetic (`fee = size * fees_per_byte`) still exercises the
fallback path; the policy-based selection is covered by the new file. Four new
tests cover the dust guard: sub-dust change returns `None`, change exactly at
the threshold is accepted, the fallback stops when the change is still dust,
and it succeeds once the change clears dust.
- Selected suite (core + GUI + settings + translations): 423 passed.
- `ruff check` on the touched files: no new violations (only the pre-existing
I001 in `dialogs.py`).
**Outcome:** DONE (pending the owner's test of the release ZIP).
---
## 65. Check crashed with `KeyError: '<txid>'` (stale "father" link in the will tree)
**Date:** 2026-10-10
**Symptom (owner's mainnet wallet, Electrum 4.8.x):** the Check wizard stopped
at "Checking your will" with `Error: 'a65a682f…'`, and the window refresh
logged "error while updating window" with the same txid:
```
File "bal/core/will.py", line 657, in check_invalidated
or willtree[w.father].get_status("CONFIRMED")
KeyError: 'a65a682f2975f0dffd9eccce9392833f227badf617fc67815674736d594d4115'
```
**Root cause:** `father` is persisted in the wallet (the `"Father"` field of
`WillItem.to_dict`) and was only ever set, never cleared. When the father tx was
later removed from the will (rebuild, merge, replacement) the child kept
pointing to a txid that no longer exists, and `check_invalidated` crashed with a
`KeyError` on that txid (the Check wizard and the window refresh both failed).
**What changed:**
- `bal/core/will.py`
- `Will.add_willtree`: clears `father` on every item before rebuilding the
links, so the tree is always recomputed from scratch from the transactions
themselves. Docstring added explaining WHY.
- `Will.check_invalidated`: a father that is not (or no longer) part of the
will is treated as "no father" instead of raising `KeyError`.
- `tests/test_core_willtree_father.py` (new, 7 tests): stale father discarded /
replaced by the real parent, no crash in `check_invalidated` (with and
without the tree rebuild), real parent/child link built, child of an
unconfirmed parent still skipped, and the `set_invalidate` cascade (see #66).
**Verification:** the new test file plus the core/CLI suites pass; `ruff check`
clean on the touched files.
**Outcome:** DONE (pending the owner's test of the ZIP).
---
## 66. Invalidation cascade crashed on real will trees (`children` is a list)
**Date:** 2026-10-10
**Problem (found while fixing #65, reproduced on real data):**
`Will.set_invalidate` iterated `children.items()`, but `Will.add_willtree`
builds `children` as a LIST of `[child_id, input_idx, prevout_idx]`, so
invalidating a will tx that has child will txs raised
`AttributeError: 'list' object has no attribute 'items'`. The existing tests in
`test_core_will_invalidate.py` set `children` to dicts by hand, so they never
saw it.
**What changed:** `bal/core/will.py` - `set_invalidate` accepts both shapes
(the list from `add_willtree`, a dict from older data/tests) and only recurses
into children that are still present in the will.
`tests/test_core_willtree_father.py`: new test
`test_set_invalidate_cascades_to_children_built_by_add_willtree`.
**Verification:** `tests/test_core_willtree_father.py` passes; `ruff check`
clean on the touched files.
**Outcome:** DONE.