feat(bal): UI batch v0.3.9 + expired-will invalidate regression fix
UI batch (TASK A/B/C/D): - (A) Clearer 'Will expired' message: shortened will id (8+8 chars) and readable UTC date instead of raw UNIX timestamp; shown in WARNING colour (orange) instead of ERROR (red) in the wizard, as it is an expected step. - (A2) Split the orange message onto two lines via <br> (rendered as HTML by msg_warning) to avoid an overly long single line. - (B) History tab labels (text only): inheritance txs -> 'BAL Inheritance transaction'; invalidate tx -> 'BAL Invalidate transaction'. Colours left to Electrum defaults. - (C) New 'No will-executor TX' checkbox in plugin settings, bound to the existing NO_WILLEXECUTOR config (default ON, kept in sync with the wizard), with help text, and included in the settings reset action. - (D) Wizard button label 'Create your will' -> 'Build Your Will'. Regression fix (A3): - When an heir was added to an already-expired will via the wizard, the automatic invalidation no longer triggered. Root cause: the inner check_will() after build_will() raised WillExpiredException and the handler returned (False, None), which never invalidated. - Auto-opening the invalidation tx window from the closing wizard proved unreliable (the window ended up behind the main wallet on some window managers; a re-check loop could ask to invalidate repeatedly before the tx reached the mempool). The robust fix: close the wizard and show a clear instruction popup guiding the user to run 'Tools -> Invalidate' and then press 'Check' to finish the will. Reasoning documented in the code. Version bumped to 0.3.9 (manifest.json, __init__.py, plugin_base.py, VERSION). CHANGELOG entry #15 updated. Verified: py_compile OK, ruff clean (no new errors), full test suite 239 passed.
This commit is contained in:
@@ -27,6 +27,7 @@ The status flags themselves (the source of truth) stay here; only the mapping
|
||||
"""
|
||||
|
||||
import copy
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.logging import Logger, get_logger
|
||||
@@ -652,6 +653,47 @@ class Will:
|
||||
_logger.info("will ok")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _short_will_id(will_id):
|
||||
"""Return a human-friendly, shortened form of a will id (hash).
|
||||
|
||||
Will ids are long hex strings (e.g. a 64-char txid) that are hard to
|
||||
read in a message box. This keeps only the first and last 8 characters
|
||||
joined by an ellipsis (e.g. ``9f1b0a75…fed9ae1b``). Short ids are left
|
||||
untouched.
|
||||
|
||||
Args:
|
||||
will_id: The will identifier (hash) to shorten; coerced to ``str``.
|
||||
|
||||
Returns:
|
||||
str: The shortened id, or the original string if it is short.
|
||||
"""
|
||||
text = str(will_id)
|
||||
# Only shorten when there is something to gain (>= 20 chars), otherwise
|
||||
# the ellipsis form would not actually be shorter or clearer.
|
||||
if len(text) <= 20:
|
||||
return text
|
||||
return f"{text[:8]}\u2026{text[-8:]}"
|
||||
|
||||
@staticmethod
|
||||
def _format_locktime(locktime):
|
||||
"""Format a UNIX locktime timestamp as a readable UTC date string.
|
||||
|
||||
Args:
|
||||
locktime: UNIX timestamp (seconds) to format.
|
||||
|
||||
Returns:
|
||||
str: A string like ``2026-06-22 11:00 UTC``. If formatting fails
|
||||
for any reason, the raw timestamp is returned as a fallback so the
|
||||
caller always has something to show.
|
||||
"""
|
||||
try:
|
||||
dt = datetime.fromtimestamp(int(locktime), tz=timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d %H:%M UTC")
|
||||
except Exception:
|
||||
# Never let date formatting break the (already exceptional) flow.
|
||||
return str(locktime)
|
||||
|
||||
@staticmethod
|
||||
def check_will_expired(all_inputs_min_locktime, timestamp_to_check):
|
||||
"""Raise WillExpiredException if any valid transaction has expired.
|
||||
@@ -659,6 +701,12 @@ class Will:
|
||||
Locktimes are always UNIX timestamps, so a transaction is expired when
|
||||
its locktime is in the past relative to ``timestamp_to_check``.
|
||||
|
||||
When a will is expired the message is written to be reassuring rather
|
||||
than alarming: being past the locktime is an EXPECTED situation that the
|
||||
plugin handles by invalidating the old will and re-signing it. The
|
||||
message therefore uses a shortened will id and a human-readable UTC date
|
||||
instead of raw values.
|
||||
|
||||
Args:
|
||||
all_inputs_min_locktime: Mapping prevout -> will-item info, used to
|
||||
find the minimum locktime per input.
|
||||
@@ -671,12 +719,27 @@ class Will:
|
||||
locktime = int(wid[0][1].tx.locktime)
|
||||
# Locktimes are always timestamps: expired when in the past.
|
||||
if locktime < int(timestamp_to_check):
|
||||
# Build a clear, non-technical message: short id +
|
||||
# readable date, and explain what will happen next.
|
||||
short_id = Will._short_will_id(wid[0][0])
|
||||
when = Will._format_locktime(locktime)
|
||||
# The message is rendered as HTML by msg_warning(),
|
||||
# so a <br> tag forces a line break: the second
|
||||
# sentence goes on its own line to avoid an overly
|
||||
# long single line in the wizard.
|
||||
raise WillExpiredException(
|
||||
f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}"
|
||||
"Will expired (id {id}, locktime {when}) \u2014<br>"
|
||||
"too late to anticipate, the will will be "
|
||||
"invalidated and re-signed.".format(
|
||||
id=short_id, when=when
|
||||
)
|
||||
)
|
||||
else:
|
||||
from datetime import datetime
|
||||
_logger.debug(f"Will Not Expired {wid[0][0]}: {datetime.fromtimestamp(locktime).isoformat()} > {datetime.fromtimestamp(timestamp_to_check).isoformat()}")
|
||||
_logger.debug(
|
||||
f"Will Not Expired {wid[0][0]}: "
|
||||
f"{Will._format_locktime(locktime)} > "
|
||||
f"{Will._format_locktime(timestamp_to_check)}"
|
||||
)
|
||||
|
||||
# def check_all_input_spent_are_in_wallet():
|
||||
# _logger.info("check all input spent are in wallet or valid txs")
|
||||
|
||||
Reference in New Issue
Block a user