forked from bitcoinafterlife/bal-electrum-plugin
feat(will): invalidate signed will on postpone + add Server status column
Postpone safety (Strategy B): - A signed/sent will carries an immutable locktime; postponing the delivery time previously did nothing, so a will-executor could still broadcast the old (earlier-locktime) transaction and execute the inheritance too early. - core/will.py: add WillPostponedException and detect postpone by comparing the requested locktime against w.tx.locktime (the locktime frozen in the signed transaction) instead of the in-memory heir entry, which is updated together with the new value and would always compare equal. - gui/qt/dialogs.py (BalBuildWillDialog.task_phase1, the real path used by Tools -> Prepare): handle WillPostponedException before NotCompleteWill; return (None, tx) to trigger sign + broadcast of the invalidation, then the user presses Prepare again to rebuild/re-sign/re-send (two explicit steps). - gui/qt/window.py: mirror the branch in build_inheritance_transaction with an explanatory message; wording aligned to the 'Prepare' button. - gui/qt/common.py: export WillPostponedException. - A postpone on a will that was never signed/sent just rebuilds (no on-chain fee). Server status column: - gui/qt/lists.py: add a dedicated 'Server' column to PreviewList with an always-readable label and a tooltip (will-executor URL + state). - gui/qt/theme.py: add server_status_text() and server_status_tooltip(), reusing the existing status flags. - gui/qt/common.py: export the new theme helpers. Docs: update README.md, bal/README.md and CHANGELOG_REFACTOR.md. Tests: 182 passed; smoke + external-zip OK; ruff has no new real findings.
This commit is contained in:
@@ -69,11 +69,11 @@ from ...core.will import (AmountException, HeirChangeException,
|
||||
NotCompleteWillException, NoWillExecutorNotPresent,
|
||||
TxFeesChangedException, Will,
|
||||
WillexecutorChangeException, WillExecutorNotPresent,
|
||||
WillExpiredException, WillItem)
|
||||
WillExpiredException, WillItem, WillPostponedException)
|
||||
from ...core.willexecutors import Willexecutors
|
||||
|
||||
# --- Presentation helpers ---
|
||||
from .theme import status_color
|
||||
from .theme import server_status_text, server_status_tooltip, status_color
|
||||
from .window_utils import (bring_to_front, show_modal, show_on_top,
|
||||
stop_thread, top_level_of)
|
||||
|
||||
|
||||
@@ -595,6 +595,20 @@ class BalBuildWillDialog(BalDialog):
|
||||
return None, Will.invalidate_will(
|
||||
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
||||
)
|
||||
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
|
||||
# FIRST (otherwise a will-executor could broadcast the old,
|
||||
# earlier-locktime tx and execute the inheritance too early). We
|
||||
# return (None, tx) so phase 2 asks the user to sign and broadcast
|
||||
# the invalidation; afterwards the user presses Prepare again to
|
||||
# rebuild the new (postponed) inheritance.
|
||||
_logger.debug(f"postponed {e}")
|
||||
self.msg_set_checking(_("Postponed: invalidating old will"))
|
||||
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
|
||||
)
|
||||
except NoHeirsException as e:
|
||||
_logger.debug("no heirs")
|
||||
self.msg_set_checking("No Heirs")
|
||||
|
||||
@@ -228,12 +228,14 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
||||
TXID = enum.auto()
|
||||
WILLEXECUTOR = enum.auto()
|
||||
STATUS = enum.auto()
|
||||
SERVER = enum.auto()
|
||||
|
||||
headers = {
|
||||
Columns.LOCKTIME: _("Locktime"),
|
||||
Columns.TXID: _("Txid"),
|
||||
Columns.WILLEXECUTOR: _("Will-Executor"),
|
||||
Columns.STATUS: _("Status"),
|
||||
Columns.SERVER: _("Server"),
|
||||
}
|
||||
|
||||
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
|
||||
@@ -385,6 +387,9 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
||||
if len(bal_tx.status) > 53:
|
||||
status = "...{}".format(status[-50:])
|
||||
labels[self.Columns.STATUS] = status
|
||||
# Dedicated, always-readable label describing whether the inheritance
|
||||
# transaction is actually stored on the will-executor servers.
|
||||
labels[self.Columns.SERVER] = server_status_text(bal_tx)
|
||||
|
||||
items = []
|
||||
for e in labels:
|
||||
@@ -398,6 +403,13 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
||||
|
||||
items[-1].setBackground(QColor(status_color(bal_tx)))
|
||||
|
||||
# Tooltip on the Server column: shows the will-executor URL (if any)
|
||||
# plus the current server state, so the user can always inspect details.
|
||||
try:
|
||||
items[self.Columns.SERVER].setToolTip(server_status_tooltip(bal_tx))
|
||||
except Exception as tip_err:
|
||||
_logger.debug(f"server tooltip error: {tip_err}")
|
||||
|
||||
row_count = self.model().rowCount()
|
||||
self.model().insertRow(row_count, items)
|
||||
if txid == current_key:
|
||||
|
||||
@@ -57,3 +57,41 @@ def status_color(will_item) -> str:
|
||||
return "#2bc8ed" # blue - signed
|
||||
else:
|
||||
return _DEFAULT_COLOR
|
||||
|
||||
|
||||
def server_status_text(will_item) -> str:
|
||||
"""Return a short, human-readable label describing the state of a will
|
||||
item on the will-executor servers (the online inheritance backup).
|
||||
|
||||
This is shown in the dedicated "Server" column of the transaction list so
|
||||
the user always knows whether each inheritance transaction is actually
|
||||
stored on the will-executor servers, regardless of the row colour.
|
||||
"""
|
||||
from electrum.i18n import _
|
||||
|
||||
if will_item.get_status("CHECK_FAIL") and not will_item.get_status("CHECKED"):
|
||||
return _("Not on server")
|
||||
if will_item.get_status("CHECKED"):
|
||||
return _("Confirmed on server")
|
||||
if will_item.get_status("PUSH_FAIL"):
|
||||
return _("Send failed")
|
||||
if will_item.get_status("PUSHED"):
|
||||
return _("Sent (not checked)")
|
||||
if will_item.get_status("COMPLETE"):
|
||||
return _("Signed (not sent)")
|
||||
return _("Not sent")
|
||||
|
||||
|
||||
def server_status_tooltip(will_item) -> str:
|
||||
"""Return a detailed tooltip for the "Server" column, including the
|
||||
will-executor URL (if any) and the current server state."""
|
||||
from electrum.i18n import _
|
||||
|
||||
url = None
|
||||
we = getattr(will_item, "we", None)
|
||||
if we:
|
||||
url = we.get("url")
|
||||
state = server_status_text(will_item)
|
||||
if url:
|
||||
return "{}: {}\n{}".format(_("Will-Executor"), url, state)
|
||||
return "{}\n{}".format(_("No will-executor"), state)
|
||||
|
||||
@@ -529,6 +529,29 @@ class BalWindow:
|
||||
return
|
||||
except NoHeirsException:
|
||||
return
|
||||
except WillPostponedException as e:
|
||||
# The will was already signed/sent and is being postponed.
|
||||
# We do NOT rebuild automatically: the user must first sign and
|
||||
# broadcast the invalidation tx (so the old, earlier-locktime tx
|
||||
# can never be used by a will-executor), then press "Prepare"
|
||||
# again
|
||||
# to create the new postponed inheritance.
|
||||
_logger.info(f"will postponed: {e}")
|
||||
self.show_message(
|
||||
_(
|
||||
"This inheritance was already signed/sent to "
|
||||
"will-executors and you are postponing it.\n\n"
|
||||
"The previously committed coins must be invalidated "
|
||||
"on-chain FIRST, otherwise a will-executor could "
|
||||
"broadcast the old (earlier) transaction and execute "
|
||||
"the inheritance too early.\n\n"
|
||||
"Please sign and broadcast the invalidation transaction "
|
||||
"now, then press 'Prepare' again to create the new "
|
||||
"(postponed) inheritance."
|
||||
)
|
||||
)
|
||||
self.invalidate_will()
|
||||
return
|
||||
except NotCompleteWillException as e:
|
||||
_logger.info("{}:{}".format(type(e), e))
|
||||
message = False
|
||||
|
||||
Reference in New Issue
Block a user