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

@@ -14,6 +14,8 @@ One :class:`bal.gui.qt.window.BalWindow` is created per top-level wallet window
and cached in ``self.bal_windows``.
"""
from electrum.fee_policy import FeeMethod, FeePolicy
from electrum.gui.qt.fee_slider import FeeComboBox, FeeSlider
from electrum.gui.qt.main_window import StatusBarButton
from electrum.plugin import hook
from electrum.util import EventListener, event_listener
@@ -594,7 +596,9 @@ class Plugin(BalPlugin, EventListener):
lbl_save_history, heir_save_history, help_save_history,
lbl_history_label, edit_history_label, help_history_label,
reset_btn_6, reset_btn_7, reset_btn_8, reset_btn_9, reset_btn_10,
reset_btn_11, reset_btn_12, reset_btn_auto_sign):
reset_btn_11, reset_btn_12, reset_btn_auto_sign,
lbl_invalidation_fee, invalidation_fee_box,
help_invalidation_fee, btn_reset_invalidation_fee):
w.setVisible(not basic)
# Opzione 2: apply the per-mode Raw/Date editor default ONLY here, on
# a real USER TYPE change (not inside update_all/CHECK), so pressing
@@ -911,6 +915,78 @@ class Plugin(BalPlugin, EventListener):
)
grid.addWidget(reset_btn_auto_rebuild, 15, 3)
# ----------------------------------------------------------------- #
# Invalidation fee policy (ADVANCED only): which fee rate the on-chain #
# invalidation transaction pays. It reuses Electrum's standard fee #
# widget (the same Feerate / ETA / Mempool slider used in the send #
# tab), so the user picks a fixed rate, a block target or a mempool #
# depth. The chosen policy is persisted as an Electrum "FeePolicy" #
# descriptor ("<method>:<value>"). #
# ----------------------------------------------------------------- #
invalidation_fee_policy = FeePolicy(self.INVALIDATION_FEE_POLICY.get())
settings_network = (
getattr(window, "network", None) if window is not None else None
)
def on_invalidation_fee_change(_fee_rate):
"""Persist the fee policy descriptor chosen with the slider."""
self.INVALIDATION_FEE_POLICY.set(
invalidation_fee_policy.get_descriptor()
)
invalidation_fee_slider = FeeSlider(
parent=d,
network=settings_network,
fee_policy=invalidation_fee_policy,
callback=on_invalidation_fee_change,
)
invalidation_fee_combo = FeeComboBox(invalidation_fee_slider)
# Do NOT call invalidation_fee_slider.update() here: the slider and combo
# already initialise themselves before their signals are connected, and
# update() fires moved() -> on_invalidation_fee_change, which would
# persist (and snap) the descriptor just for opening the dialog.
invalidation_fee_box = QWidget()
invalidation_fee_box_layout = QHBoxLayout(invalidation_fee_box)
invalidation_fee_box_layout.setContentsMargins(0, 0, 0, 0)
invalidation_fee_box_layout.addWidget(invalidation_fee_slider, 1)
invalidation_fee_box_layout.addWidget(invalidation_fee_combo)
def reset_invalidation_fee():
"""Restore the fee policy to its default ("eta:2").
The live ``FeePolicy`` object is mutated in place (method + value)
so the slider keeps working with the new defaults; the stored
descriptor and the combo are then re-synced.
"""
default_policy = FeePolicy(self.INVALIDATION_FEE_POLICY.default)
invalidation_fee_policy.method = default_policy.method
invalidation_fee_policy.value = default_policy.value
self.INVALIDATION_FEE_POLICY.set(self.INVALIDATION_FEE_POLICY.default)
invalidation_fee_combo.setCurrentIndex(
FeeMethod.slider_index_of_method(default_policy.method)
)
invalidation_fee_slider.update()
lbl_invalidation_fee = QLabel(_("Invalidation fee policy"))
help_invalidation_fee = HelpButton(_(
"Fee rate the on-chain invalidation transaction pays.\n"
"The invalidation is sent TODAY (it spends the will's coins so the "
"old inheritance transactions become unusable), so it should pay "
"the current network fee - not the will's mining fee rate.\n\n"
"Feerate: a fixed rate (sat/vByte).\n"
"ETA: fee based on the estimated confirmation time (blocks).\n"
"Mempool: fee targeting a depth in the memory pool.\n"
"Only used in ADVANCED mode."
))
btn_reset_invalidation_fee = QPushButton("\u21ba")
btn_reset_invalidation_fee.setFixedWidth(24)
btn_reset_invalidation_fee.setToolTip(_("Reset to default"))
btn_reset_invalidation_fee.clicked.connect(reset_invalidation_fee)
grid.addWidget(_hide_if_basic(lbl_invalidation_fee), 16, 0)
grid.addWidget(_hide_if_basic(invalidation_fee_box), 16, 1)
grid.addWidget(_hide_if_basic(help_invalidation_fee), 16, 2)
grid.addWidget(_hide_if_basic(btn_reset_invalidation_fee), 16, 3)
# ----------------------------------------------------------------- #
# Group C / C4b: "Reset" button that restores the dialog settings to #
# their factory defaults. It only resets the settings exposed by THIS #
@@ -968,6 +1044,8 @@ class Plugin(BalPlugin, EventListener):
# reset restores SAVE_HISTORY to its default, so the field must
# follow the (default) checkbox state again.
edit_history_label.setEnabled(bool(self.SAVE_HISTORY.get()))
# Reset the invalidation fee policy to its default ("eta:2") too.
reset_invalidation_fee()
# Refresh the open BAL windows so any dependent view (e.g. the
# editable-dates state is not in this list, but hide filters are)
# reflects the reset values.