forked from bitcoinafterlife/bal-electrum-plugin
feat(v0.4.7): report area 500px, heirs one-per-line, wizard line breaks, ALL-DUST guard
Owner-approved changes after testing v0.4.6, plus accumulated v0.4.x work, task-tracking notes, and an updated project HANDOFF document. The four v0.4.7 changes: 1. Report area (BalBuildWillDialog) opens 500px tall (min) up to 700px (max), then the scrollbar takes over. Previously it opened ~140px (too short). 2. Heirs are listed ONE per line again (green, bold) in _build_success_report, reverting the v0.4.6 single-line form. Heir names can be long and the report now scrolls, so compression is no longer needed. 3. Two wizard texts get an explicit line break: after "(or backup)" in the date hint and after "miner fees" in the fee note (widgets.py). 4. ALL-DUST guard: when EVERY heir's share is below the Bitcoin dust limit, the inheritance would pay nobody. Heirs.prepare_lists now raises HeirAmountIsDustException at the end (where all heirs across all locktimes are known with their final dust state), and dialogs.task_phase1 shows a clear RED message and stops without building/signing/checking. A mix of dust + valid heirs keeps building normally. The guard is intentionally in prepare_lists, NOT prepare_transactions (which only sees the lowest locktime and would false-positive). HeirAmountIsDustException is imported in common.py. Tests: 3 new tests in test_core_heirs_extra.py pin the dust behaviour (all-dust raises; mixed continues; multi-locktime continues). Full suite: 258 passed. ruff: no new errors. Version bumped 0.4.6 -> 0.4.7 (4 files). CHANGELOG #23. Also adds/updates HANDOFF.md so any future AI (Claude or another model) can resume the project with full context (rules, layout, build/test/lint, dust logic, git flow), and records the task-tracking notes in .agent_memory_tasks.md.
This commit is contained in:
@@ -62,7 +62,8 @@ from PyQt6.QtWidgets import (QAbstractItemView, QAbstractSpinBox, QCheckBox,
|
||||
|
||||
# --- Core (GUI-free) logic layer ---
|
||||
from ...core.plugin_base import BalPlugin, BalTimestamp
|
||||
from ...core.heirs import HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT, Heirs
|
||||
from ...core.heirs import (HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT,
|
||||
HeirAmountIsDustException, Heirs)
|
||||
from ...core.util import Util
|
||||
from ...core.will import (AmountException, HeirChangeException,
|
||||
HeirNotFoundException, NoHeirsException,
|
||||
|
||||
@@ -127,6 +127,21 @@ class BalWizardDialog(BalDialog):
|
||||
def on_next_we(self):
|
||||
close_window = BalBuildWillDialog(self.bal_window)
|
||||
close_window.build_will_task()
|
||||
|
||||
# Run the SAME final server check as the "Check" button (allegato15,
|
||||
# case B): previously the wizard only ran build_will_task() and skipped
|
||||
# the will-executor verification, so the user always had to press
|
||||
# "Check" manually after finishing the wizard. We now replicate exactly
|
||||
# the lists.py check() logic: after building, query every will that
|
||||
# needs a server check (Will.needs_server_check) and run
|
||||
# check_transactions(), which shows the "Checking transactions" dialog.
|
||||
will = {}
|
||||
for wid, w in self.bal_window.willitems.items():
|
||||
if Will.needs_server_check(w):
|
||||
will[wid] = w
|
||||
if will:
|
||||
self.bal_window.check_transactions(will)
|
||||
|
||||
self.close()
|
||||
# self.next_widget(BalWizardLocktimeAndFeeWidget(self.bal_window,self,self.on_next_locktimeandfee,self.on_next_wedonwload,self.on_next_wedonwload.on_cancel_heir))
|
||||
|
||||
@@ -321,10 +336,30 @@ class BalWizardWEDownloadWidget(BalWizardWidget):
|
||||
ping_on_done()
|
||||
|
||||
def ping_on_done():
|
||||
# Task #02 - "Automatically download and select"
|
||||
# (index 0): the green SELECTED tick must follow the
|
||||
# green ping dot, i.e. select ONLY servers that actually
|
||||
# answered the ping (status == 200) and DESELECT every
|
||||
# server that did not (timeout / error / never pinged).
|
||||
#
|
||||
# Why the explicit deselect matters: previously a server
|
||||
# that had been selected on an earlier download but is
|
||||
# now unreachable stayed selected, so the plugin kept
|
||||
# broadcasting to a dead server and got stuck. Forcing
|
||||
# selected=False for non-200 servers discards them at the
|
||||
# source. Re-running this (each "Automatically download"
|
||||
# action) re-evaluates every server: one that failed
|
||||
# before but now answers is selected again.
|
||||
#
|
||||
# We compare the status as a string ("200") to stay
|
||||
# consistent with the will-executor list view
|
||||
# (lists.py uses str(status) == "200"), and use .get()
|
||||
# so a missing "status" key never raises.
|
||||
if index < 1:
|
||||
for we in self.bal_window.willexecutors:
|
||||
if self.bal_window.willexecutors[we]["status"] == 200:
|
||||
self.bal_window.willexecutors[we]["selected"] = True
|
||||
wedict = self.bal_window.willexecutors[we]
|
||||
responded = str(wedict.get("status", "")) == "200"
|
||||
wedict["selected"] = responded
|
||||
Willexecutors.save(
|
||||
self.bal_window.bal_plugin, self.bal_window.willexecutors
|
||||
)
|
||||
@@ -502,10 +537,35 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.bal_window = bal_window
|
||||
self.bal_plugin = bal_window.bal_plugin
|
||||
self.message_label = QLabel(_("Building Will:"))
|
||||
# Allow the long report text to wrap instead of forcing the dialog ever
|
||||
# wider, and let it grow downward inside the scroll area below.
|
||||
self.message_label.setWordWrap(True)
|
||||
self.message_label.setAlignment(
|
||||
Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
|
||||
)
|
||||
self.vbox = QVBoxLayout(self)
|
||||
self.vbox.addWidget(self.message_label, 0)
|
||||
|
||||
# SCROLLABLE message area (allegato14): with many will-executors the
|
||||
# report can reach dozens of lines. Previously the dialog kept resizing
|
||||
# itself taller for every new line (see msg_update's resize), so with
|
||||
# e.g. 50 will-executors the window grew past the screen and the bottom
|
||||
# buttons (Close) became unreachable. We now put the message label in a
|
||||
# QScrollArea with a capped maximum height: once the text exceeds that
|
||||
# height a vertical scrollbar appears and the buttons stay visible.
|
||||
self.scroll_area = QScrollArea(self)
|
||||
self.scroll_area.setWidgetResizable(True)
|
||||
self.scroll_area.setWidget(self.message_label)
|
||||
# Open the report area already ~500px tall (owner request, allegato1:
|
||||
# the previous ~140px area was far too short). The dialog may still grow
|
||||
# up to 700px to fit a few more lines; beyond that the vertical
|
||||
# scrollbar takes over and the bottom buttons stay reachable.
|
||||
self.scroll_area.setMinimumHeight(500)
|
||||
self.scroll_area.setMaximumHeight(700)
|
||||
self.vbox.addWidget(self.scroll_area, 1)
|
||||
|
||||
# Kept for backward compatibility (referenced by old/commented code);
|
||||
# no longer used to lay out the messages.
|
||||
self.qwidget = QWidget(self)
|
||||
self.vbox.addWidget(self.qwidget, 1)
|
||||
self.labelsbox = QVBoxLayout(self.qwidget)
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(100)
|
||||
@@ -519,6 +579,18 @@ class BalBuildWillDialog(BalDialog):
|
||||
# Manual next-steps hint (Sign / Broadcast) shown to the user after the
|
||||
# dialog finishes; None when nothing is left to do.
|
||||
self._next_steps_hint = None
|
||||
# Set to True by _sync_locktime_to_built_txs when the delivery date was
|
||||
# automatically anticipated during a rebuild. Used to explain to the
|
||||
# user WHY signing is being requested (otherwise the sign prompt appears
|
||||
# without any reason, as the owner reported).
|
||||
self._date_was_anticipated = False
|
||||
# Set to True right after we broadcast an automatic invalidation
|
||||
# transaction (the "postpone" path). On the very next phase-1 re-check
|
||||
# Electrum may not have seen the invalidation tx yet, so it would still
|
||||
# report a postpone and the wizard would re-prompt to invalidate over
|
||||
# and over (the reported loop). When this flag is set and a postpone is
|
||||
# STILL detected, we STOP with a clear message instead of re-prompting.
|
||||
self._invalidation_broadcast = False
|
||||
self.network = Network.get_instance()
|
||||
self._stopping = False
|
||||
self.thread = TaskThread(self)
|
||||
@@ -592,12 +664,22 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.bal_window.check_will()
|
||||
self.msg_set_checking(self.msg_ok())
|
||||
except WillExpiredException:
|
||||
# UNIFY INVALIDATE PROCEDURE (+ task #03):
|
||||
#
|
||||
# The will is already expired (e.g. the CHECK button is pressed on an
|
||||
# expired will). Previously this returned (None, invalidate_tx),
|
||||
# which routed to the automatic invalidate path (password prompt +
|
||||
# auto-broadcast) that did NOT set the "BAL Invalidate transaction"
|
||||
# history label.
|
||||
#
|
||||
# We now return the SAME "invalidate_classic" signal used elsewhere,
|
||||
# so on_success_phase1 shows the warning popup and auto-opens
|
||||
# Electrum's classic transaction window (which sets the label). This
|
||||
# makes the CHECK button and the WIZARD behave identically and fixes
|
||||
# the missing-label bug (#03).
|
||||
_logger.debug("expired")
|
||||
self.msg_set_checking("Expired")
|
||||
fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
|
||||
return None, Will.invalidate_will(
|
||||
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
||||
)
|
||||
return "invalidate_classic", None
|
||||
except WillPostponedException as e:
|
||||
# An already signed/sent will is being postponed. Like an expired
|
||||
# will, the previously committed coins must be invalidated on-chain
|
||||
@@ -628,12 +710,30 @@ class BalBuildWillDialog(BalDialog):
|
||||
elif isinstance(e, TxFeesChangedException):
|
||||
message = _("Txfees are changed")
|
||||
elif isinstance(e, HeirNotFoundException):
|
||||
message = _("Heir not found")
|
||||
# Task #01b: the old text "Heir not found" was misleading.
|
||||
# In practice this branch is reached whenever the will is no
|
||||
# longer coherent and must be rebuilt - very often simply
|
||||
# because the delivery date was anticipated, NOT because an heir
|
||||
# is genuinely missing. We therefore show a clear, accurate
|
||||
# message that covers both the DATE and the HEIRS cases.
|
||||
message = _(
|
||||
"Found CHANGES to the DATE or the HEIRS,\n"
|
||||
"a NEW WILL must be prepared."
|
||||
)
|
||||
if message:
|
||||
_logger.debug(f"message: {message}")
|
||||
self.msg_set_checking(message)
|
||||
else:
|
||||
self.msg_set_checking("New")
|
||||
# Task #01b: the old fallback text "New" was unclear. When the
|
||||
# will is incomplete without a more specific reason, it still
|
||||
# means the will has to be rebuilt, so we use the same clear
|
||||
# message as the HeirNotFoundException branch above.
|
||||
self.msg_set_checking(
|
||||
_(
|
||||
"Found CHANGES to the DATE or the HEIRS,\n"
|
||||
"a NEW WILL must be prepared."
|
||||
)
|
||||
)
|
||||
|
||||
if have_to_build:
|
||||
self.msg_set_building()
|
||||
@@ -647,10 +747,7 @@ class BalBuildWillDialog(BalDialog):
|
||||
return False, None
|
||||
|
||||
self.bal_window.check_will()
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
# Label shown in Electrum's History tab for inheritance txs.
|
||||
self.bal_window.wallet.set_label(wid, "BAL Inheritance transaction")
|
||||
self.msg_set_building(self.msg_ok())
|
||||
self._build_success_report()
|
||||
except WillExecutorNotPresent:
|
||||
self.msg_set_status(
|
||||
_("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR
|
||||
@@ -683,21 +780,77 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.msg_set_building(self.msg_warning(e))
|
||||
return "invalidate_classic", None
|
||||
|
||||
except NotCompleteWillException as e:
|
||||
# IMPORTANT (bugs E/F/K): build_will() above has just REBUILT the
|
||||
# whole will because the heirs (or the date) changed. The
|
||||
# post-build re-validation (check_will) then legitimately reports
|
||||
# that the new will differs from the previous one - e.g. it
|
||||
# raises HeirNotFoundException for a heir that is not yet covered
|
||||
# by an already-signed transaction. That is NOT an error: it is
|
||||
# exactly the signal that the freshly built transactions still
|
||||
# need to be SIGNED (and then broadcast).
|
||||
#
|
||||
# Previously this fell through to the generic "except Exception"
|
||||
# below, which (1) printed the heir name in RED and (2) returned
|
||||
# have_to_sign=False, so the rebuilt will was never signed
|
||||
# ("Nothing to do"). We now treat it as a successful rebuild:
|
||||
# show the green "Ok" + the heir list and FALL THROUGH to the
|
||||
# have_to_sign detection, so the new (status "New", not COMPLETE)
|
||||
# transactions are correctly detected and signed/broadcast.
|
||||
_logger.debug(f"will rebuilt, needs signing: {e}")
|
||||
self._build_success_report()
|
||||
|
||||
except HeirAmountIsDustException:
|
||||
# ALL-DUST CASE (owner request): every heir's share is below the
|
||||
# Bitcoin dust limit, so the inheritance would pay nobody. We do
|
||||
# NOT build, sign or check anything - we show a clear message in
|
||||
# red and stop, so no "empty" will ends up in the list. This is
|
||||
# raised by Heirs.prepare_lists only when ALL real heirs are
|
||||
# dust; a mix of dust + valid heirs never reaches here.
|
||||
self.msg_set_building(
|
||||
self.msg_error(
|
||||
_(
|
||||
"All heirs' shares are below the dust limit: "
|
||||
"the inheritance cannot be created. "
|
||||
"Increase the amounts or reduce the number of heirs."
|
||||
)
|
||||
)
|
||||
)
|
||||
return False, None
|
||||
|
||||
except Exception as e:
|
||||
self.msg_set_building(self.msg_error(e))
|
||||
return False, None
|
||||
|
||||
# excluded_heirs = []
|
||||
# DUST report (one line PER HEIR, not per will-executor).
|
||||
#
|
||||
# WHY: the wallet balance can be so small that each heir's share falls
|
||||
# below Bitcoin's dust limit, so the inheritance is not feasible. The
|
||||
# "is DUST" condition depends ONLY on the heir's amount, NOT on which
|
||||
# will-executor transaction we are looking at. The previous code looped
|
||||
# over every valid will (one per will-executor) AND every heir, so with
|
||||
# e.g. 20 will-executors and 10 heirs it printed 20x10 = 200 identical
|
||||
# "is DUST ... Excluded from will <wid>" rows (owner report, allegato13).
|
||||
#
|
||||
# We now collect the dust heirs in a de-duplicated dict (heir id ->
|
||||
# dust amount) across all valid wills and print ONE row per heir,
|
||||
# without the will-executor reference. So N heirs => at most N rows,
|
||||
# regardless of how many will-executors exist.
|
||||
dust_heirs = {}
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
heirs = self.bal_window.willitems[wid].heirs
|
||||
for hid, heir in heirs.items():
|
||||
if "DUST" in str(heir[HEIR_REAL_AMOUNT]):
|
||||
self.msg_set_status(
|
||||
f"{hid},{heir[HEIR_DUST_AMOUNT]} is DUST",
|
||||
None,
|
||||
f"Excluded from will {wid}",
|
||||
self.COLOR_WARNING,
|
||||
)
|
||||
# Keep the first dust amount seen for this heir; it is the
|
||||
# same share in every will-executor copy of the will.
|
||||
dust_heirs.setdefault(hid, heir[HEIR_DUST_AMOUNT])
|
||||
for hid, dust_amount in dust_heirs.items():
|
||||
self.msg_set_status(
|
||||
f"{_('Heir')} {hid}",
|
||||
None,
|
||||
f"{dust_amount} is DUST - excluded (amount below dust limit)",
|
||||
self.COLOR_WARNING,
|
||||
)
|
||||
|
||||
have_to_sign = False
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
@@ -706,6 +859,116 @@ class BalBuildWillDialog(BalDialog):
|
||||
break
|
||||
return have_to_sign, txs
|
||||
|
||||
def _build_success_report(self):
|
||||
"""Mark the build step as done and list every heir in green.
|
||||
|
||||
Called after a successful (re)build of the will. It:
|
||||
1. labels each inheritance transaction in Electrum's history;
|
||||
2. shows the green "Ok" result on the "Building your will" row;
|
||||
3. lists EVERY heir of the freshly built will, one per line, in green
|
||||
(the "Ok" colour), so the user can confirm at a glance who will
|
||||
inherit.
|
||||
|
||||
Previously a heir name could appear in RED (it was the text of a rebuild
|
||||
exception) and the heir list was only shown on the "all clean" path,
|
||||
which is why after changing/deleting an heir the user saw a single red
|
||||
heir name and no green list. This helper is now used both on the clean
|
||||
path and on the "will was rebuilt and needs signing" path, so the green
|
||||
heir list is always shown.
|
||||
|
||||
Internal will-executor pseudo-heirs (reserved ``w!ll3x3c"`` prefix) are
|
||||
skipped, exactly as the core coherence check does. A set keeps the list
|
||||
unique even when an heir appears in several transactions.
|
||||
"""
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
# Label shown in Electrum's History tab for inheritance txs.
|
||||
self.bal_window.wallet.set_label(wid, "BAL Inheritance transaction")
|
||||
# Keep the plugin's stored delivery date in sync with the (possibly
|
||||
# auto-anticipated) transactions, otherwise the next Check would wrongly
|
||||
# ask to invalidate. See _sync_locktime_to_built_txs for the full why.
|
||||
self._sync_locktime_to_built_txs()
|
||||
self.msg_set_building(self.msg_ok())
|
||||
# List EACH heir on its OWN line, green + bold (owner request: revert the
|
||||
# one-line "Heirs: a, b, c" form of v0.4.6). Heir names can be long, and
|
||||
# now that the report area scrolls (allegato1) there is no need to cram
|
||||
# them onto a single line. We de-duplicate (an heir can appear in several
|
||||
# will-executor transactions) and skip the internal will-executor
|
||||
# pseudo-heirs (reserved ``w!ll3x3c"`` prefix), exactly as before.
|
||||
shown_heirs = set()
|
||||
for wid in Will.only_valid(self.bal_window.willitems):
|
||||
for hname in self.bal_window.willitems[wid].heirs:
|
||||
if str(hname)[:9] == 'w!ll3x3c"':
|
||||
continue
|
||||
if hname in shown_heirs:
|
||||
continue
|
||||
shown_heirs.add(hname)
|
||||
self.msg_set_status(_("Heir"), None, str(hname), self.COLOR_OK)
|
||||
|
||||
def _sync_locktime_to_built_txs(self):
|
||||
"""Align the plugin's stored delivery date with the built transactions.
|
||||
|
||||
WHY this is needed (bug reported by the owner):
|
||||
When the will is rebuilt while it still spends the same coins as a
|
||||
previous one (e.g. after deleting an heir WITHOUT changing the date),
|
||||
the core engine AUTOMATICALLY anticipates the transaction locktime by
|
||||
one day (see Will.check_anticipate / Util.anticipate_locktime). This is
|
||||
correct and required so the new transaction can be mined BEFORE the old
|
||||
one it replaces.
|
||||
|
||||
However the plugin's own stored delivery date
|
||||
(WILL_SETTINGS["locktime"]) was NOT updated and stayed at the original
|
||||
date. On the next Check the plugin compared the stored date (original)
|
||||
with the transaction locktime (original minus one day) and, since
|
||||
stored > tx, mistook the automatic anticipation for a user POSTPONE,
|
||||
wrongly asking to invalidate the will.
|
||||
|
||||
Fix: after a (re)build, set the stored delivery date to the MINIMUM
|
||||
locktime among the valid built transactions. We only ever move the date
|
||||
EARLIER (anticipation): if the minimum is not strictly below the current
|
||||
stored date we leave it untouched, so a genuine user-chosen postpone is
|
||||
never silently overwritten. The owner confirmed that, when several
|
||||
transactions carry different locktimes, taking the minimum is the
|
||||
desired behaviour, and that the date shown in the panel/wizard must
|
||||
reflect this anticipated date (so the calendar .ics also uses it).
|
||||
|
||||
We route the update through BalWindow.update_setting_widgets, which is
|
||||
the single place that (1) stores the value in WILL_SETTINGS, (2)
|
||||
persists it to Electrum's database and (3) refreshes the date widgets in
|
||||
every panel/wizard, so the visible date and the .ics export stay
|
||||
consistent.
|
||||
"""
|
||||
# Minimum locktime across the valid (just built) inheritance txs.
|
||||
# Will.get_min_locktime returns None when there is no valid tx.
|
||||
min_locktime = Will.get_min_locktime(self.bal_window.willitems, None)
|
||||
if min_locktime is None:
|
||||
return
|
||||
min_locktime = int(min_locktime)
|
||||
# Current stored delivery date, as a comparable UNIX timestamp.
|
||||
try:
|
||||
current = int(
|
||||
Util.parse_locktime_string(
|
||||
self.bal_window.will_settings["locktime"]
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
# If the stored value cannot be parsed, fall back to syncing.
|
||||
current = None
|
||||
# Only anticipate (move the date EARLIER); never overwrite a postpone.
|
||||
if current is not None and min_locktime >= current:
|
||||
return
|
||||
_logger.debug(
|
||||
f"sync delivery date to anticipated tx locktime: "
|
||||
f"{current} -> {min_locktime}"
|
||||
)
|
||||
# Remember that we anticipated the date, so the later sign prompt can
|
||||
# explain WHY signing is needed (see on_success_phase1).
|
||||
self._date_was_anticipated = True
|
||||
# update_setting_widgets stores the value, persists it and refreshes the
|
||||
# date widgets in all panels/wizard (so the .ics calendar uses it too).
|
||||
self.bal_window.update_setting_widgets(
|
||||
min_locktime, "locktime", update_all=True
|
||||
)
|
||||
|
||||
def on_accept(self):
|
||||
self.bal_window.update_all()
|
||||
pass
|
||||
@@ -733,12 +996,34 @@ class BalBuildWillDialog(BalDialog):
|
||||
try:
|
||||
tx.add_info_from_wallet(self.bal_window.wallet)
|
||||
self.network.run_from_another_thread(tx.add_info_from_network(self.network))
|
||||
txid = self.network.run_from_another_thread(
|
||||
|
||||
# IMPORTANT (task #21 fix): get the txid from the transaction
|
||||
# object, NOT from broadcast_transaction()'s return value.
|
||||
# Network.broadcast_transaction is declared "-> None" and ALWAYS
|
||||
# returns None (it only raises on failure). The previous code stored
|
||||
# that None into `txid` and put set_label() in the `else: # txid`
|
||||
# branch, which was therefore NEVER reached - that is why the
|
||||
# "BAL Invalidate transaction" history label kept missing on this
|
||||
# automatic ("postpone") path. The transaction is already signed and
|
||||
# complete here, so tx.txid() is the correct, stable id - exactly
|
||||
# what the working Tools -> Invalidate path uses
|
||||
# (BalWalletWindow.invalidate_will -> result.txid()).
|
||||
txid = tx.txid()
|
||||
|
||||
# Set the history label BEFORE broadcasting. set_label only writes to
|
||||
# the local wallet metadata (no network needed), so doing it first
|
||||
# guarantees the label exists the moment the transaction shows up in
|
||||
# the History tab, regardless of how fast the broadcast/notification
|
||||
# arrives.
|
||||
if txid:
|
||||
self.bal_window.wallet.set_label(txid, "BAL Invalidate transaction")
|
||||
else:
|
||||
_logger.debug(f"invalidate tx has no txid: {tx}")
|
||||
|
||||
self.network.run_from_another_thread(
|
||||
self.network.broadcast_transaction(tx, timeout=120), timeout=120
|
||||
)
|
||||
self.msg_set_invalidating(self.msg_ok())
|
||||
if not txid:
|
||||
_logger.debug(f"should not be none txid: {txid}")
|
||||
|
||||
except TxBroadcastError as e:
|
||||
_logger.error(f"fail to broadcast transaction:{e}")
|
||||
@@ -909,7 +1194,19 @@ class BalBuildWillDialog(BalDialog):
|
||||
if tx:
|
||||
if tx.is_complete():
|
||||
self.loop_broadcast_invalidating(tx)
|
||||
self.wait(5)
|
||||
# Wait 10 seconds (was 5) AFTER broadcasting the
|
||||
# invalidation so Electrum's wallet/network has time to see
|
||||
# the new transaction before we re-run phase 1. Without this
|
||||
# pause the immediate re-check still detected the old
|
||||
# (not-yet-invalidated) will and re-prompted to invalidate,
|
||||
# producing the reported loop. This runs in the worker
|
||||
# thread, so the GUI is not frozen by the sleep.
|
||||
self.wait(10)
|
||||
# Remember that we just broadcast an invalidation. If the
|
||||
# next phase-1 re-check STILL reports a postpone (because
|
||||
# Electrum has not registered the tx yet), we stop with a
|
||||
# clear message instead of looping (see on_success_phase1).
|
||||
self._invalidation_broadcast = True
|
||||
else:
|
||||
raise Exception("tx not complete")
|
||||
else:
|
||||
@@ -946,46 +1243,74 @@ class BalBuildWillDialog(BalDialog):
|
||||
# is safe. We then stop and close the wizard.
|
||||
if self.have_to_sign == "invalidate_classic":
|
||||
self.thread.stop()
|
||||
# Design decision (window stacking + user clarity):
|
||||
# UNIFY INVALIDATE PROCEDURE (+ task #03):
|
||||
#
|
||||
# When an heir is added to an already-expired will, the rebuilt will
|
||||
# is itself expired and the old will must be invalidated on-chain
|
||||
# before the new one can be used. We previously tried to open the
|
||||
# invalidation transaction window AUTOMATICALLY from here, but doing
|
||||
# so from within the closing wizard proved fragile: depending on the
|
||||
# OS window manager and Qt's event ordering, the transaction window
|
||||
# kept ending up BEHIND the main wallet window (it lost focus when
|
||||
# the wizard closed). Neither closing-before-opening nor a deferred
|
||||
# QTimer close() fixed it reliably on every machine.
|
||||
# before the new one can be used.
|
||||
#
|
||||
# The robust solution is to NOT auto-open any window here. Instead we
|
||||
# close the wizard and show a clear instruction telling the user to
|
||||
# run "Tools -> Invalidate" themselves. That menu path is already
|
||||
# known to work perfectly (its transaction window always stays in
|
||||
# front, because no other window is closing at the same time), and
|
||||
# it also makes the user consciously aware that they are performing a
|
||||
# deliberate, important action (invalidating their old will).
|
||||
# We make the CHECK button and the WIZARD behave IDENTICALLY:
|
||||
# 1. show a WARNING popup (no "Tools -> Invalidate" wording);
|
||||
# 2. AUTOMATICALLY open Electrum's classic transaction dialog via
|
||||
# BalWalletWindow.invalidate_will() (the same code used by the
|
||||
# Tools -> Invalidate menu). That path already sets the
|
||||
# "BAL Invalidate transaction" history label - which fixes
|
||||
# task #03 (the label was previously missing on the automatic
|
||||
# path).
|
||||
#
|
||||
# Close the wizard first so the instruction popup is the only window
|
||||
# left, then show the guidance message.
|
||||
# Window-stacking note (history): opening the transaction window
|
||||
# straight from within the closing wizard used to leave it BEHIND
|
||||
# the main window on some window managers. The robust fix is to
|
||||
# close the wizard FIRST and defer the call with QTimer.singleShot
|
||||
# so it runs on the next event-loop iteration, when the wizard is
|
||||
# already gone and the transaction window becomes the front-most,
|
||||
# focused window.
|
||||
self.close()
|
||||
self.bal_window.show_message(
|
||||
_(
|
||||
"Your will has expired and must be invalidated before it "
|
||||
"can be rebuilt.\n\n"
|
||||
"Please use the top-right menu Tools -> Invalidate to "
|
||||
"invalidate your old will: a transaction window will open "
|
||||
"where you can sign and broadcast the invalidation.\n\n"
|
||||
"can be rebuilt.\n"
|
||||
"A transaction window will now open:\n"
|
||||
"please SIGN and then BROADCAST it to invalidate your old "
|
||||
"will.\n"
|
||||
"After the invalidation is confirmed, press the Check "
|
||||
"button near Tools, to finish the will."
|
||||
"button to finish the will."
|
||||
)
|
||||
)
|
||||
# Deferred so the wizard is fully closed before the classic
|
||||
# invalidate window opens (keeps it in front, fixes the old
|
||||
# "window behind" problem).
|
||||
QTimer.singleShot(0, self.bal_window.invalidate_will)
|
||||
return
|
||||
|
||||
_logger.debug("have to sign {}".format(self.have_to_sign))
|
||||
password = None
|
||||
if self.have_to_sign is None:
|
||||
_logger.debug("have to invalidate")
|
||||
|
||||
# LOOP GUARD (task #21): if we already broadcast an invalidation on
|
||||
# the previous pass and phase 1 STILL reports a postpone, Electrum
|
||||
# has simply not seen the invalidation transaction yet. Re-prompting
|
||||
# to invalidate here is exactly what produced the reported endless
|
||||
# loop ("Invalidate your old will" reappearing right after signing).
|
||||
# Instead of re-prompting, STOP cleanly with a clear message and
|
||||
# tell the user to retry the Check once the invalidation confirms.
|
||||
if self._invalidation_broadcast:
|
||||
self.thread.stop()
|
||||
self.msg_set_invalidating(self.msg_ok())
|
||||
self.bal_window.show_message(
|
||||
_(
|
||||
"Your old will has been invalidated and the "
|
||||
"transaction was broadcast.\n"
|
||||
"Electrum may need a little time to register it.\n"
|
||||
"Please wait until the invalidation transaction is "
|
||||
"confirmed, then press the Check button again to "
|
||||
"finish updating your will."
|
||||
)
|
||||
)
|
||||
self._add_close_button()
|
||||
return
|
||||
|
||||
self.msg_set_invalidating()
|
||||
# need to sign invalidate and restart phase 1
|
||||
|
||||
@@ -1017,6 +1342,26 @@ class BalBuildWillDialog(BalDialog):
|
||||
return
|
||||
|
||||
elif self.have_to_sign:
|
||||
# If the will was rebuilt with an automatically anticipated delivery
|
||||
# date, explain WHY we are now asking to sign: otherwise the sign
|
||||
# prompt appears with no reason (owner feedback). The note is shown
|
||||
# on the "Building your will" row (orange warning) before the modal
|
||||
# password prompt opens, so the user can read it.
|
||||
if self._date_was_anticipated:
|
||||
# Render this notice in BLACK BOLD (not the yellow warning
|
||||
# colour) and split it onto TWO lines after "...previous one."
|
||||
# for readability (allegato17). The "\n" is converted to a line
|
||||
# break by msg_update (it replaces "\n" with "<br>").
|
||||
self.msg_set_building(
|
||||
"<b>{}</b>".format(
|
||||
_(
|
||||
"The delivery date was automatically moved one day "
|
||||
"earlier so the updated will can correctly replace "
|
||||
"the previous one.\n"
|
||||
"Please sign (and broadcast) to confirm the change."
|
||||
)
|
||||
)
|
||||
)
|
||||
password = self.bal_window.get_wallet_password(
|
||||
_("Sign your will"), parent=self
|
||||
)
|
||||
@@ -1307,8 +1652,18 @@ class BalBuildWillDialog(BalDialog):
|
||||
full_text = "<br><br>".join(self.labels).replace("\n", "<br>")
|
||||
self.message_label.setText(full_text)
|
||||
self.message_label.adjustSize()
|
||||
# self.setMinimumHeight(len(self.labels)*40)
|
||||
# Auto-scroll the report to the BOTTOM so the newest line is always
|
||||
# visible (allegato14). The QScrollArea caps the height, so instead of
|
||||
# resizing the whole dialog taller we move its vertical scrollbar to the
|
||||
# maximum. ensureWidgetVisible is deferred via the scrollbar range so it
|
||||
# reflects the just-added text.
|
||||
scrollbar = self.scroll_area.verticalScrollBar()
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
# Let the dialog grow only up to the scroll area's capped height; beyond
|
||||
# that the scrollbar handles overflow and the buttons stay reachable.
|
||||
self.resize(self.sizeHint())
|
||||
# Re-assert the bottom position after the resize/relayout settled.
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
|
||||
def get_text(self):
|
||||
return self.message_label.text()
|
||||
|
||||
@@ -491,8 +491,10 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
||||
self.bal_window.bal_plugin.read_file("icons/reload.png")
|
||||
)
|
||||
)
|
||||
# Tooltip so the icon is self-explanatory when hovered.
|
||||
refresh.setToolTip(_("Check"))
|
||||
# Tooltip so the icon is self-explanatory when hovered. "Check
|
||||
# Inheritance" makes it clear the button re-checks the inheritance/will
|
||||
# state (not a generic refresh).
|
||||
refresh.setToolTip(_("Check Inheritance"))
|
||||
refresh.clicked.connect(self.check)
|
||||
|
||||
widget = QWidget(self)
|
||||
|
||||
@@ -417,6 +417,32 @@ class Plugin(BalPlugin):
|
||||
# be saved on a USB stick and a copy given to the heirs).
|
||||
heir_no_willexecutor = BalCheckBox(self.NO_WILLEXECUTOR)
|
||||
|
||||
# USER TYPE selector (SIMPLE / ADVANCED, global). A two-choice combo
|
||||
# (not a free-text field) bound to the USER_TYPE config:
|
||||
# index 0 -> "BASIC" -> stored value "basic" (DEFAULT)
|
||||
# index 1 -> "ADVANCED" -> stored value "advanced"
|
||||
#
|
||||
# BASIC hides the advanced controls (Raw/Date selector and the
|
||||
# "Check Alive" field) and disables the check-alive postpone behaviour.
|
||||
# Changing it refreshes the open windows so the controls appear/disappear
|
||||
# immediately. It is kept in a named variable so the "Reset" button can
|
||||
# restore its displayed value after a reset.
|
||||
user_type_combo = QComboBox()
|
||||
user_type_combo.addItems([_("BASIC"), _("ADVANCED")])
|
||||
# Map the stored string to the combo index (anything but "advanced"
|
||||
# falls back to BASIC, matching BalPlugin.is_basic_mode()).
|
||||
user_type_combo.setCurrentIndex(
|
||||
0 if str(self.USER_TYPE.get()).lower() != "advanced" else 1
|
||||
)
|
||||
|
||||
def on_user_type_change(idx):
|
||||
# Persist "basic"/"advanced" and refresh the open windows so the
|
||||
# advanced controls appear/disappear right away.
|
||||
self.USER_TYPE.set("advanced" if idx == 1 else "basic")
|
||||
self.update_all()
|
||||
|
||||
user_type_combo.currentIndexChanged.connect(on_user_type_change)
|
||||
|
||||
# Editable line/text widgets are created once and kept in named
|
||||
# variables so the "Reset" button (Group C / C4b) can refresh the
|
||||
# displayed values after resetting the underlying config.
|
||||
@@ -447,6 +473,21 @@ class Plugin(BalPlugin):
|
||||
# Reset/support button row (bottom). Assigning ``QGridLayout(d)`` would
|
||||
# have made the grid the dialog's only layout, leaving no room for them.
|
||||
grid = QGridLayout()
|
||||
add_widget(
|
||||
grid,
|
||||
"User Type",
|
||||
user_type_combo,
|
||||
0,
|
||||
(
|
||||
"Choose how much detail the plugin shows.\n\n"
|
||||
"BASIC (default): a simpler interface. It hides the advanced "
|
||||
"controls (the Raw/Date selector and the 'Check Alive' field) "
|
||||
"and turns off the 'check alive' postpone behaviour, so you "
|
||||
"only set the delivery date.\n\n"
|
||||
"ADVANCED: shows every control, including the 'Check Alive' "
|
||||
"field and the Raw/Date selector."
|
||||
),
|
||||
)
|
||||
add_widget(
|
||||
grid,
|
||||
"Hide Replaced",
|
||||
@@ -475,7 +516,7 @@ class Plugin(BalPlugin):
|
||||
)
|
||||
add_widget(
|
||||
grid,
|
||||
"Editable dates",
|
||||
"Panel editable Date and Fee",
|
||||
heir_editable_dates,
|
||||
4,
|
||||
(
|
||||
@@ -485,14 +526,38 @@ class Plugin(BalPlugin):
|
||||
"When disabled, those dates are display-only outside the wizard."
|
||||
),
|
||||
)
|
||||
# "Add transaction without will-executor" setting (formerly labelled
|
||||
# "No will-executor TX"). When ON the plugin ALSO builds the backup
|
||||
# inheritance transaction that does NOT require a will-executor (the
|
||||
# "celeste"/light-blue one shown in the will list): it can be saved on a
|
||||
# USB stick and a copy handed to the heirs. When OFF only the
|
||||
# transactions destined to the selected will-executors are built.
|
||||
#
|
||||
# Placed here (row 5, right below "Panel editable Date and Fee" and above
|
||||
# "Number of reminders") at the user's request so related options sit
|
||||
# together. The remaining grid rows below were renumbered accordingly.
|
||||
add_widget(
|
||||
grid,
|
||||
"Add transaction without willexecutor",
|
||||
heir_no_willexecutor,
|
||||
5,
|
||||
(
|
||||
"Create a will that does not require a Will-executor; it can be "
|
||||
"saved, for example, on a USB stick, and a copy can be given to "
|
||||
"the heirs."
|
||||
),
|
||||
)
|
||||
add_widget(
|
||||
grid,
|
||||
"Number of reminders",
|
||||
heir_num_reminders,
|
||||
5,
|
||||
6,
|
||||
(
|
||||
"How many reminder alarms the exported calendar (.ics) event "
|
||||
"contains.\n"
|
||||
"contains.\n\n"
|
||||
"BASIC MODE:\n"
|
||||
"Calendar reminder 30, 10 and 1 days before.\n\n\n"
|
||||
"ADVANCED MODE:\n"
|
||||
"The reminders are spread across the check-alive period and "
|
||||
"always fall before the delivery deadline.\n"
|
||||
"If the period is shorter than the requested number, at most "
|
||||
@@ -503,7 +568,7 @@ class Plugin(BalPlugin):
|
||||
grid,
|
||||
"Event summary",
|
||||
edit_event_summary,
|
||||
6,
|
||||
7,
|
||||
(
|
||||
"Default message to be used in event summary\n"
|
||||
"Variables:\n"
|
||||
@@ -516,7 +581,7 @@ class Plugin(BalPlugin):
|
||||
grid,
|
||||
"Event description",
|
||||
edit_event_description,
|
||||
7,
|
||||
8,
|
||||
(
|
||||
"Default message to be used in event description\n"
|
||||
"Variables:\n"
|
||||
@@ -527,21 +592,6 @@ class Plugin(BalPlugin):
|
||||
)
|
||||
#add_widget(grid, "Bal Mode", bal_mode, 4, "choose bal mode")
|
||||
|
||||
# "No will-executor TX" setting. Mirrors the checkbox shown in the
|
||||
# wizard's will-executor download window (both bound to NO_WILLEXECUTOR),
|
||||
# so it can also be toggled from the plugin settings. Default ON.
|
||||
add_widget(
|
||||
grid,
|
||||
"No will-executor TX",
|
||||
heir_no_willexecutor,
|
||||
8,
|
||||
(
|
||||
"Create a will that does not require a Will-executor; it can be "
|
||||
"saved, for example, on a USB stick, and a copy can be given to "
|
||||
"the heirs."
|
||||
),
|
||||
)
|
||||
|
||||
# add_widget(
|
||||
# grid,
|
||||
# "Ping Willexecutors",
|
||||
@@ -584,6 +634,7 @@ class Plugin(BalPlugin):
|
||||
# Map each config object to the widget that displays it, so we can
|
||||
# both reset the stored value and update what the user sees.
|
||||
resets = [
|
||||
(self.USER_TYPE, user_type_combo, "user_type"),
|
||||
(self.HIDE_REPLACED, heir_hide_replaced, "check"),
|
||||
(self.HIDE_INVALIDATED, heir_hide_invalidated, "check"),
|
||||
(self.AUTO_SIGN, heir_auto_sign, "check"),
|
||||
@@ -607,6 +658,11 @@ class Plugin(BalPlugin):
|
||||
widget.setText(cfg.default)
|
||||
elif kind == "text":
|
||||
widget.setPlainText(cfg.default)
|
||||
elif kind == "user_type":
|
||||
# Default is "basic" -> combo index 0; "advanced" -> index 1.
|
||||
widget.setCurrentIndex(
|
||||
1 if str(cfg.default).lower() == "advanced" else 0
|
||||
)
|
||||
# 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.
|
||||
@@ -638,6 +694,9 @@ class Plugin(BalPlugin):
|
||||
# Outer layout: warning (top) -> settings grid -> bottom button row.
|
||||
outer = QVBoxLayout(d)
|
||||
outer.addWidget(lbl_warning)
|
||||
# Blank vertical gap below the red warning so it is not glued to the
|
||||
# first setting row ("User Type"); requested by the user for readability.
|
||||
outer.addSpacing(12)
|
||||
outer.addLayout(grid)
|
||||
outer.addLayout(bottom_row)
|
||||
|
||||
|
||||
@@ -75,6 +75,34 @@ def compute_reminder_offsets(days, count):
|
||||
return sorted(offsets, reverse=True)
|
||||
|
||||
|
||||
# Fixed reminder offsets (in days BEFORE the delivery date) used in BASIC mode.
|
||||
# In BASIC the check-alive parameter is hidden/unmanaged, so reminders cannot be
|
||||
# spread over it; instead the owner asked for three fixed reminders: 30, 10 and
|
||||
# 1 day before the inheritance delivery date.
|
||||
BASIC_REMINDER_OFFSETS = (30, 10, 1)
|
||||
|
||||
|
||||
def basic_reminder_offsets(days_to_deadline):
|
||||
"""Return the BASIC-mode reminder offsets that still fall in the future.
|
||||
|
||||
BASIC mode uses the fixed offsets in ``BASIC_REMINDER_OFFSETS`` (30, 10 and
|
||||
1 day before the delivery date). Any offset that would land in the past is
|
||||
dropped, because a reminder before "today" is useless: if the delivery date
|
||||
is only ``days_to_deadline`` days away, only the offsets that are ``<=
|
||||
days_to_deadline`` are kept.
|
||||
|
||||
Args:
|
||||
days_to_deadline: whole days from now until the delivery date.
|
||||
|
||||
Returns:
|
||||
A list of integer day-offsets (each ``>= 1``), sorted as in
|
||||
``BASIC_REMINDER_OFFSETS`` (descending: earliest reminder first). Empty
|
||||
when the delivery date is less than one day away.
|
||||
"""
|
||||
horizon = max(int(days_to_deadline), 0)
|
||||
return [off for off in BASIC_REMINDER_OFFSETS if 1 <= off <= horizon]
|
||||
|
||||
|
||||
class ClickableLabel(QLabel):
|
||||
doubleClicked = pyqtSignal()
|
||||
|
||||
@@ -256,6 +284,13 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
||||
1: self.locktime_date_e,
|
||||
}
|
||||
self.combo.addItems(options)
|
||||
# SIMPLE / ADVANCED (task: hide the Raw/Date selector in BASIC mode).
|
||||
#
|
||||
# In BASIC mode the user must not see or use the Raw/Date selector:
|
||||
# every date field is forced to the calendar ("Date") editor and the
|
||||
# combo is hidden. We keep a flag so the rest of __init__ can force the
|
||||
# Date editor regardless of the stored value's format.
|
||||
self._basic_mode = self.bal_window.bal_plugin.is_basic_mode()
|
||||
default_index = 0
|
||||
if not default_locktime:
|
||||
default_locktime = self.bal_window.bal_plugin.WILL_SETTINGS.get()[self.base_field]
|
||||
@@ -264,6 +299,10 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
||||
default_index = 1
|
||||
except Exception:
|
||||
default_index = 0
|
||||
# Force the calendar ("Date") editor in BASIC mode so the user always
|
||||
# picks a date and never sees the RAW input ("30d"/"1y" style).
|
||||
if self._basic_mode:
|
||||
default_index = 1
|
||||
#hbox.addWidget(QLabel(self.label_text))
|
||||
help_button=HelpButton(self.help_text)
|
||||
help_button.setText(self.label_text)
|
||||
@@ -290,6 +329,11 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
|
||||
self.set_value(default_locktime)
|
||||
self.current_value=default_locktime
|
||||
hbox.addWidget(self.combo)
|
||||
# In BASIC mode hide the Raw/Date selector entirely: the field stays
|
||||
# locked on the calendar editor chosen above, so the user only ever
|
||||
# picks a Date and cannot switch to RAW.
|
||||
if self._basic_mode:
|
||||
self.combo.setVisible(False)
|
||||
for w in self.editors:
|
||||
hbox.addWidget(w)
|
||||
|
||||
@@ -594,13 +638,19 @@ class LockTimeWidget(BalTimeEditWidget):
|
||||
"<b>DELIVERY TIME</b><br><br>"
|
||||
"Set Locktime for transactions.<br>"
|
||||
"Any time is needed transaction will be anticipated by 1day<br><br>"
|
||||
# The Raw locktime syntax below is only available in ADVANCED mode
|
||||
# (in BASIC mode the Raw/Date selector is hidden and only the Date
|
||||
# picker is shown), so we say so explicitly to avoid confusing users.
|
||||
"(ONLY IN ADVANCED MODE)<br>"
|
||||
"if you choose Raw, you can insert various options based on suffix:<br>"
|
||||
" - d: number of days after current day(ex: 1d means tomorrow)<br>"
|
||||
" - y: number of years after currrent day(ex: 1y means one year from today)<br>"
|
||||
)
|
||||
label_text = "🚛"
|
||||
#label_text = "Locktime"
|
||||
tooltip_text = "Delivery time"
|
||||
# Hover tooltip for the delivery-time icon; mirrors the style of the fee
|
||||
# icon tooltip ("..., click for more information") so the two are consistent.
|
||||
tooltip_text = "Delivery Time, click for more information"
|
||||
base_field = "locktime"
|
||||
|
||||
def __init__(self, bal_window, parent, init_value=None):
|
||||
@@ -641,6 +691,15 @@ class WillSettingsWidget(QWidget):
|
||||
self.widgets["threshold"] = ThresholdTimeWidget(bal_window, self)
|
||||
self.widgets["locktime"].valueEdited.connect(self.on_locktime_change)
|
||||
self.widgets["threshold"].valueEdited.connect(self.on_locktime_change)
|
||||
# SIMPLE / ADVANCED: in BASIC mode hide the whole "Check Alive"
|
||||
# (threshold) row, including its leading icon. The widget is still
|
||||
# created and kept in self.widgets so the rest of the code (and the
|
||||
# saved settings) keep working; it is only hidden from view. The
|
||||
# Delivery time (locktime) row stays visible. We hide it after creation
|
||||
# so both the horizontal (toolbar/Heirs) and vertical (wizard) layouts
|
||||
# below add an already-hidden widget.
|
||||
if bal_window.bal_plugin.is_basic_mode():
|
||||
self.widgets["threshold"].setVisible(False)
|
||||
# self.widgets['baltx_fees'].valueChange.connect(self.bal_window.update_setting_widgets)
|
||||
self.on_locktime_change()
|
||||
self.widgets["baltx_fees"] = BalTxFeesWidget(bal_window, self)
|
||||
@@ -656,53 +715,135 @@ class WillSettingsWidget(QWidget):
|
||||
box.addWidget(self.calendar_button)
|
||||
box.addWidget(self.widgets["baltx_fees"])
|
||||
else:
|
||||
# Vertical layout (the "Build your will" wizard): make every row the
|
||||
# same width and left aligned so they all fit in one tidy block,
|
||||
# instead of letting the calendar button and the fee field stretch to
|
||||
# the dialog's right edge (which made them far wider than the date
|
||||
# rows above).
|
||||
#
|
||||
# IMPORTANT: the leading icons keep their ORIGINAL size. The icons
|
||||
# are HelpButtons, which already pin themselves to a fixed width
|
||||
# (2.2 * char_width_in_lineedit()); we must NOT widen them, otherwise
|
||||
# they look oversized compared with the original toolbar layout. We
|
||||
# only need to (1) align the calendar row's left edge with the icons'
|
||||
# original width and (2) cap every row to the date-row width.
|
||||
# ----------------------------------------------------------------- #
|
||||
# Vertical layout (the "Build your will" wizard) - Layout H. #
|
||||
# #
|
||||
# User requirements (allegato4): #
|
||||
# * the leading ICONS (delivery time, calendar, fee and - in #
|
||||
# ADVANCED mode - check-alive) must be aligned one under the #
|
||||
# other on the left; #
|
||||
# * the editable FIELDS must all start from the SAME x position, #
|
||||
# just to the right of their icon; #
|
||||
# * the fee field must be WIDER (its up/down arrows were covering #
|
||||
# the digits) - not a tiny box. #
|
||||
# #
|
||||
# DESIGN NOTE - why we do NOT pull the icon/field out of each #
|
||||
# composite into a shared grid: the LockTime / Check-Alive composites #
|
||||
# hold TWO editors (Raw and Date) plus a Raw/Date combo that the user #
|
||||
# can switch at runtime in ADVANCED mode; ``self.editor`` is swapped #
|
||||
# live (see on_current_index_changed). Reparenting only the currently #
|
||||
# active editor would orphan the other editor and the combo and break #
|
||||
# ADVANCED mode. So we keep every composite INTACT and instead align #
|
||||
# them by: #
|
||||
# 1. forcing every leading icon (prefix_widget) to the SAME fixed #
|
||||
# width, so each composite's field starts at the same x; and #
|
||||
# 2. stacking the whole composites left-aligned in the VBox. #
|
||||
# This is robust to the Raw/Date switching and keeps all internal #
|
||||
# logic working untouched. #
|
||||
# ----------------------------------------------------------------- #
|
||||
locktime_w = self.widgets["locktime"]
|
||||
threshold_w = self.widgets["threshold"]
|
||||
fees_w = self.widgets["baltx_fees"]
|
||||
|
||||
# Original icon width (HelpButton's own fixed width); used only to
|
||||
# offset the calendar button so its field starts under the others.
|
||||
icon_w = locktime_w.prefix_widget.sizeHint().width()
|
||||
|
||||
# Common row width = natural width of the date rows (the reference).
|
||||
row_w = max(
|
||||
locktime_w.sizeHint().width(),
|
||||
threshold_w.sizeHint().width(),
|
||||
# Common icon width = the widest leading icon (incl. the calendar
|
||||
# button). Forcing every icon to this width makes the icons line up
|
||||
# one under the other and, because each field sits immediately to the
|
||||
# right of its icon, makes every field start at the same x too.
|
||||
icon_w = max(
|
||||
locktime_w.prefix_widget.sizeHint().width(),
|
||||
threshold_w.prefix_widget.sizeHint().width(),
|
||||
fees_w.prefix_widget.sizeHint().width(),
|
||||
self.calendar_button.sizeHint().width(),
|
||||
)
|
||||
for w in (locktime_w, threshold_w, fees_w):
|
||||
w.setFixedWidth(row_w)
|
||||
for icon in (
|
||||
locktime_w.prefix_widget,
|
||||
threshold_w.prefix_widget,
|
||||
fees_w.prefix_widget,
|
||||
self.calendar_button,
|
||||
):
|
||||
icon.setFixedWidth(icon_w)
|
||||
|
||||
# The calendar row has no prefix icon: wrap it so it starts with an
|
||||
# empty spacer of the icon width (calendar field aligned with the
|
||||
# date/fee fields) and cap it to the same total width as the rows
|
||||
# above, so it no longer stretches to the dialog's right edge.
|
||||
calendar_row = QWidget(self)
|
||||
calendar_box = QHBoxLayout(calendar_row)
|
||||
calendar_box.setContentsMargins(0, 0, 0, 0)
|
||||
calendar_box.setSpacing(0)
|
||||
calendar_spacer = QWidget()
|
||||
calendar_spacer.setFixedWidth(icon_w)
|
||||
calendar_box.addWidget(calendar_spacer)
|
||||
calendar_box.addWidget(self.calendar_button)
|
||||
calendar_row.setFixedWidth(row_w)
|
||||
# WIDEN the fee field (user feedback: the spin-box up/down arrows
|
||||
# were covering the digits). ~8 chars leaves room for the value and
|
||||
# the arrows.
|
||||
fees_w.field_widget.setFixedWidth(8 * char_width_in_lineedit())
|
||||
|
||||
# WHY THE TEXT WAS TRUNCATED (allegato16, fixed here):
|
||||
# A QLabel added to a box layout WITH an alignment flag (the old
|
||||
# ``alignment=Qt.AlignmentFlag.AlignLeft``) is NOT stretched to the
|
||||
# layout width - Qt gives it only its sizeHint. For a word-wrapped
|
||||
# label that sizeHint width is ambiguous/narrow, so the text wrapped
|
||||
# against an almost-minimum width and the reserved height was too
|
||||
# small, cutting the sentence in half. setMinimumWidth alone did not
|
||||
# help because the alignment flag still prevented horizontal stretch.
|
||||
#
|
||||
# TARGETED FIX:
|
||||
# 1. give the whole vertical WillSettingsWidget a sensible minimum
|
||||
# width, so the box (and thus the labels) has real width to work
|
||||
# with even before the parent dialog stretches it;
|
||||
# 2. add the two explanatory labels WITHOUT an alignment flag, so
|
||||
# they expand to the full box width and word-wrap correctly;
|
||||
# 3. set an Expanding/Minimum size policy so the label takes the
|
||||
# available width and computes its height from that width
|
||||
# (heightForWidth), guaranteeing the full text is shown.
|
||||
hint_min_width = 44 * char_width_in_lineedit()
|
||||
self.setMinimumWidth(hint_min_width)
|
||||
|
||||
# Explanatory hint ABOVE the date field (wizard only): tell the user
|
||||
# what the delivery date means. Wrapped so it fits the dialog width.
|
||||
# The explicit "\n" forces the line break exactly where the owner
|
||||
# asked (allegato2): after "(or backup)". With setWordWrap(True) the
|
||||
# QLabel honours the newline, so the sentence always shows on two
|
||||
# tidy lines instead of wrapping at an arbitrary point.
|
||||
date_hint = QLabel(
|
||||
_(
|
||||
"Enter the date on which you want the inheritance (or "
|
||||
"backup)\nof your Electrum wallet to take effect."
|
||||
)
|
||||
)
|
||||
date_hint.setWordWrap(True)
|
||||
date_hint.setMinimumWidth(hint_min_width)
|
||||
date_hint.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
|
||||
)
|
||||
# NOTE: added WITHOUT an alignment flag on purpose (see above), so
|
||||
# the label is stretched to the full width and wraps correctly.
|
||||
box.addWidget(date_hint)
|
||||
|
||||
# Stack the composites one under the other, all left-aligned so the
|
||||
# fixed-width icons share a common left edge.
|
||||
box.addWidget(locktime_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
box.addWidget(threshold_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
box.addWidget(calendar_row, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
# In BASIC mode the Check-Alive (threshold) row is hidden entirely
|
||||
# (it was already set invisible above); in ADVANCED mode it shows
|
||||
# with its icon aligned under the delivery-time icon.
|
||||
box.addWidget(
|
||||
self.calendar_button, alignment=Qt.AlignmentFlag.AlignLeft
|
||||
)
|
||||
box.addWidget(fees_w, alignment=Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
# Cautionary note BELOW the miner-fee field (wizard only): warn the
|
||||
# user not to lower the miner fee unless they know what they do.
|
||||
# Explicit "\n" break after "miner fees" (allegato2), same rationale
|
||||
# as date_hint above.
|
||||
fee_note = QLabel(
|
||||
_(
|
||||
"Please note: Do not reduce the miner fees\nunless you "
|
||||
"know what you\u2019re doing"
|
||||
)
|
||||
)
|
||||
fee_note.setWordWrap(True)
|
||||
fee_note.setMinimumWidth(hint_min_width)
|
||||
fee_note.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum
|
||||
)
|
||||
# Added WITHOUT an alignment flag (see date_hint above) so it
|
||||
# stretches to full width and wraps correctly instead of truncating.
|
||||
box.addWidget(fee_note)
|
||||
|
||||
self.apply_editable_dates()
|
||||
return
|
||||
|
||||
# Group C / C2: apply the current "Editable dates" setting to the date
|
||||
# fields. Done once at creation here, and re-applied later by
|
||||
# apply_editable_dates() whenever the setting changes (called from
|
||||
@@ -744,6 +885,39 @@ class WillSettingsWidget(QWidget):
|
||||
# editable outside the wizard only when the setting is ticked.
|
||||
self.widgets["baltx_fees"].set_read_only(not editable_dates)
|
||||
|
||||
def apply_user_type_visibility(self):
|
||||
"""Re-apply the BASIC/ADVANCED visibility of the Check-Alive field.
|
||||
|
||||
WHY this is needed (bug reported by the owner): the Check-Alive
|
||||
(threshold) field is hidden in BASIC mode and shown in ADVANCED mode.
|
||||
That visibility used to be decided ONLY in __init__. The toolbar
|
||||
settings widgets of the WILL and HEIR tabs are created once and then
|
||||
REUSED across the session (they are not rebuilt when the user switches
|
||||
USER TYPE), so after switching from BASIC to ADVANCED the Check-Alive
|
||||
field stayed hidden there. The wizard worked only because it is recreated
|
||||
every time it is opened.
|
||||
|
||||
This method re-reads is_basic_mode() and shows/hides the Check-Alive
|
||||
field accordingly. It is called from BalWindow.update_all() (which the
|
||||
USER TYPE combo triggers when changed), exactly like apply_editable_dates
|
||||
is, so toggling BASIC/ADVANCED takes effect immediately on the already
|
||||
existing WILL/HEIR toolbars without restarting Electrum.
|
||||
|
||||
It is safe to call repeatedly and on either layout (horizontal toolbar
|
||||
or vertical wizard): it only flips the visibility of the threshold
|
||||
widget.
|
||||
"""
|
||||
try:
|
||||
basic = self.bal_window.bal_plugin.is_basic_mode()
|
||||
except Exception:
|
||||
# If the mode cannot be read, keep the field visible (the safe,
|
||||
# information-preserving default).
|
||||
basic = False
|
||||
threshold = self.widgets.get("threshold")
|
||||
if threshold is not None:
|
||||
# Hidden in BASIC, visible in ADVANCED.
|
||||
threshold.setVisible(not basic)
|
||||
|
||||
def open_or_save_calendar(self):
|
||||
"""Build and save an .ics calendar file with SEPARATE reminder events.
|
||||
|
||||
@@ -774,23 +948,43 @@ class WillSettingsWidget(QWidget):
|
||||
"""
|
||||
now = BalCalendar.format_time(datetime.now())
|
||||
|
||||
# locktime = delivery deadline; threshold = start of the check-alive
|
||||
# period. Both are datetimes exposed by the date widgets as ``.alarm``.
|
||||
# locktime = delivery deadline. It is exposed by the date widget as
|
||||
# ``.alarm`` and already reflects the (possibly auto-anticipated) minimum
|
||||
# transaction locktime, so the calendar uses the correct delivery date.
|
||||
locktime = self.widgets["locktime"].alarm
|
||||
threshold = self.widgets["threshold"].alarm
|
||||
|
||||
# Whole days available between check-alive and the deadline.
|
||||
days = (locktime - threshold).days
|
||||
|
||||
# How many reminder events the user asked for (default 3 if unreadable).
|
||||
try:
|
||||
count = int(self.bal_window.bal_plugin.NUM_REMINDERS.get())
|
||||
except Exception:
|
||||
count = 3
|
||||
|
||||
# Day-offsets BEFORE the deadline, e.g. [30, 16, 1]. The list always
|
||||
# ends with 1 (one day before the locktime) when >= 2 reminders fit.
|
||||
offsets = compute_reminder_offsets(days, count)
|
||||
# BASIC vs ADVANCED reminder strategy.
|
||||
#
|
||||
# In ADVANCED mode the reminders are spread uniformly across the
|
||||
# check-alive (threshold) period, ending one day before the deadline.
|
||||
#
|
||||
# In BASIC mode the check-alive parameter is NOT shown nor managed by the
|
||||
# user (it stays at an arbitrary default), so spreading reminders over it
|
||||
# is meaningless. The owner asked that, in BASIC, the calendar simply
|
||||
# saves the inheritance delivery date with three fixed reminders: 30 days
|
||||
# before, 10 days before and 1 day before. We also drop any fixed offset
|
||||
# that would fall in the past (a reminder before "today" is useless), so
|
||||
# a short-dated will still gets the reminders that are still in the
|
||||
# future.
|
||||
if self.bal_window.bal_plugin.is_basic_mode():
|
||||
# Whole days from now until the delivery date. Fixed offsets (30, 10,
|
||||
# 1 day before) are applied by basic_reminder_offsets, which also
|
||||
# drops any offset that would fall in the past.
|
||||
days_to_deadline = (locktime - datetime.now()).days
|
||||
offsets = basic_reminder_offsets(days_to_deadline)
|
||||
else:
|
||||
# ADVANCED: spread reminders over the check-alive period as before.
|
||||
threshold = self.widgets["threshold"].alarm
|
||||
# Whole days available between check-alive and the deadline.
|
||||
days = (locktime - threshold).days
|
||||
# How many reminders the user asked for (default 3 if unreadable).
|
||||
try:
|
||||
count = int(self.bal_window.bal_plugin.NUM_REMINDERS.get())
|
||||
except Exception:
|
||||
count = 3
|
||||
# Day-offsets BEFORE the deadline, e.g. [30, 16, 1]. The list always
|
||||
# ends with 1 (one day before the locktime) when >= 2 reminders fit.
|
||||
offsets = compute_reminder_offsets(days, count)
|
||||
|
||||
# Per-event heir details and the shared description/summary templates.
|
||||
heirs_details = "\r\n".join(
|
||||
|
||||
@@ -466,7 +466,17 @@ class BalWindow:
|
||||
self.willexecutors = Willexecutors.get_willexecutors(
|
||||
self.bal_plugin, update=True, bal_window=self, task=False
|
||||
)
|
||||
if self.date_to_check < datetime.now().timestamp():
|
||||
# SIMPLE / ADVANCED: in BASIC mode the "Check Alive" parameter must
|
||||
# behave AS IF IT DID NOT EXIST. The check-alive threshold drives
|
||||
# the "you are alive -> postpone the inheritance" prompt; raising
|
||||
# CheckAliveError here is what triggers that postpone/invalidate
|
||||
# flow. In BASIC we therefore SKIP this check entirely, so a passed
|
||||
# check-alive date never forces a postpone/rewrite of the will. The
|
||||
# delivery time (locktime) is unaffected and still fully enforced.
|
||||
if (
|
||||
not self.bal_plugin.is_basic_mode()
|
||||
and self.date_to_check < datetime.now().timestamp()
|
||||
):
|
||||
raise CheckAliveError(self.date_to_check)
|
||||
|
||||
self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get())
|
||||
@@ -591,7 +601,15 @@ class BalWindow:
|
||||
elif isinstance(e, TxFeesChangedException):
|
||||
message = "Txfees are changed"
|
||||
elif isinstance(e, HeirNotFoundException):
|
||||
message = "Heir not found"
|
||||
# Task #01b: replace the misleading "Heir not found" text.
|
||||
# This branch is most often hit because the delivery date
|
||||
# was anticipated, not because an heir is missing, so we use
|
||||
# a clear message that covers both the DATE and the HEIRS
|
||||
# cases (kept consistent with dialogs.py / the CHECK window).
|
||||
message = (
|
||||
"Found CHANGES to the DATE or the HEIRS,\n"
|
||||
"a NEW WILL must be prepared."
|
||||
)
|
||||
|
||||
if message:
|
||||
self.show_message(
|
||||
@@ -1141,11 +1159,12 @@ class BalWindow:
|
||||
|
||||
base_msg = _("Downloading will-executors list...")
|
||||
download_start = time.time()
|
||||
# Upper bound shown to the user. fetch_will_executors_list tries up to
|
||||
# two endpoints, each with timeout=10 and one retry (~21s worst case),
|
||||
# so ~45s is a realistic maximum. Showing "Xs / 45s" tells the user how
|
||||
# long they may have to wait instead of an open-ended counter.
|
||||
download_deadline = 45
|
||||
# Upper bound shown to the user. Unified with every other network wait
|
||||
# via the single shared Willexecutors.NETWORK_DEADLINE constant (the
|
||||
# user asked for one consistent 20s value everywhere instead of the old
|
||||
# scattered 30s/45s numbers). Showing "Xs / NETWORK_DEADLINEs" tells the
|
||||
# user how long they may have to wait instead of an open-ended counter.
|
||||
download_deadline = Willexecutors.NETWORK_DEADLINE
|
||||
|
||||
def task():
|
||||
# Heartbeat: show an elapsed-seconds counter (with the max wait made
|
||||
@@ -1316,6 +1335,16 @@ class BalWindow:
|
||||
_settings_widget.apply_editable_dates()
|
||||
except Exception as _edit_err:
|
||||
_logger.debug(f"apply_editable_dates error: {_edit_err}")
|
||||
# Re-apply BASIC/ADVANCED visibility of the Check-Alive
|
||||
# field on the existing WILL/HEIR toolbars, so switching
|
||||
# USER TYPE shows/hides it immediately (bug fix: it used to
|
||||
# reappear only in the wizard, not on these tabs).
|
||||
try:
|
||||
_settings_widget.apply_user_type_visibility()
|
||||
except Exception as _vis_err:
|
||||
_logger.debug(
|
||||
f"apply_user_type_visibility error: {_vis_err}"
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"error while updating window: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user