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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user