core+gui: signed relative Check Alive (negative = before delivery, positive = after creation)

This commit is contained in:
2026-10-03 10:49:25 -04:00
parent 4a33116b66
commit beab22ce23
19 changed files with 775 additions and 225 deletions

View File

@@ -46,7 +46,12 @@ from ...core.animated_qr import (
ur1_frames,
ur2_frames,
)
from ...core.checkalive import CheckAliveError, resolve_date_to_check
from ...core.checkalive import (
CheckAliveAfterDeliveryError,
CheckAliveError,
resolve_date_to_check,
resolve_relative_check_alive_date,
)
from ...core.qrtransfer import (
CHUNK_PRESETS,
MissingFramesError,
@@ -751,6 +756,13 @@ class BalBuildWillDialog(BalDialog):
varrow = self.msg_set_status("Checking variables")
try:
self.bal_window.init_class_variables()
except CheckAliveAfterDeliveryError:
self.msg_set_status(
"Checking variables", varrow,
"Check Alive must be after today and before the delivery date",
self.COLOR_ERROR,
)
return "check_alive_before_delivery", None
except CheckAliveError as cae:
fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
tx = Will.invalidate_will(
@@ -1194,10 +1206,11 @@ class BalBuildWillDialog(BalDialog):
self.bal_window.update_setting_widgets(
min_locktime, "locktime", update_all=True
)
# A relative "Check Alive" threshold ("N days BEFORE the delivery") is
# also PRESERVED: it is anchored on every check by
# ``resolve_date_to_check`` / ``resolve_guard_threshold``, so it does
# not need to be frozen to an absolute date here.
# A relative "Check Alive" threshold is also PRESERVED: it is anchored
# on every check by ``resolve_date_to_check`` / ``resolve_guard_threshold``
# (negative counts back from the delivery, positive forward from the
# inheritance creation date), so it never needs freezing to an absolute
# date here.
def on_accept(self):
try:
@@ -1542,6 +1555,19 @@ class BalBuildWillDialog(BalDialog):
self._add_no_heirs_buttons()
return
if self.have_to_sign == "check_alive_before_delivery":
# The Check Alive date is on/after the delivery (a configuration
# error, not an expired will): do NOT invalidate, just tell the
# user and let them close the wizard.
self.bal_window.show_error(
_(
"CheckAlive must be after today and before the delivery "
"date please update it"
)
)
self._add_close_button()
return
_logger.debug("have to sign {}".format(self.have_to_sign))
password = None
if self.have_to_sign is None:
@@ -1830,9 +1856,14 @@ class BalBuildWillDialog(BalDialog):
threshold = None
num_reminders = 3
else:
threshold_ts = BalTimestamp(
self.bal_window.will_settings["threshold"]
).to_timestamp()
# The Check Alive reference shares the same sign rule as the
# core: negative counts back from the delivery, positive counts
# forward from the inheritance creation date.
threshold_ts = resolve_relative_check_alive_date(
self.bal_window.will_settings["threshold"],
delivery_date=locktime_ts,
creation_date=Will.get_creation_time(self.bal_window.willitems),
)
threshold = datetime.fromtimestamp(threshold_ts)
raw_description = self.bal_window.bal_plugin.EVENT_DESCRIPTION.get()
raw_summary = self.bal_window.bal_plugin.EVENT_SUMMARY.get()

View File

@@ -20,6 +20,7 @@ Contents:
from typing import TYPE_CHECKING
from ...core.checkalive import resolve_relative_check_alive_date
from ...core.heirs import get_op_return_hex, is_op_return_address
from ...core.input_rules import (
LockTimeEditor,
@@ -215,6 +216,10 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
label_text = None
tooltip_text = None
base_field = None
# A leading "-" is accepted by the RAW editor only for the Check Alive
# field (its sign selects the anchor date); the delivery field never
# accepts a sign. Subclasses override this.
allow_negative_raw = False
def __init__(self, bal_window, parent, default_locktime=None):
super().__init__(parent)
@@ -225,7 +230,9 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
hbox.setContentsMargins(0, 0, 0, 0)
hbox.setSpacing(0)
self.setMinimumWidth(40 * char_width_in_lineedit())
self.locktime_raw_e = TimeRawEditWidget(self, time_edit=self)
self.locktime_raw_e = TimeRawEditWidget(
self, time_edit=self, allow_negative=self.allow_negative_raw
)
self.locktime_date_e = LockTimeDateEdit(self, time_edit=self)
self.editors = [self.locktime_raw_e, self.locktime_date_e]
self.combo = QComboBox()
@@ -478,9 +485,9 @@ class TimeRawEditWidget(QWidget):
def is_acceptable_locktime(self, value):
return True
def __init__(self, parent, time_edit=None):
def __init__(self, parent, time_edit=None, allow_negative=False):
super().__init__(parent)
self.editor = LockTimeRawEdit(parent, time_edit)
self.editor = LockTimeRawEdit(parent, time_edit, allow_negative=allow_negative)
self.label = QLabel("")
# Group C / C3: this trailing label shows the ABSOLUTE date computed
# from the RAW value (e.g. "30d" -> "2027-06-23"), so it needs room for a
@@ -506,7 +513,7 @@ class TimeRawEditWidget(QWidget):
class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
def __init__(self, parent=None, time_edit=None):
def __init__(self, parent=None, time_edit=None, allow_negative=False):
QLineEdit.__init__(self, parent)
# Group C / C3: narrow the RAW input to roughly a third of its former
# width. The accepted values are short relative durations such as "30d"
@@ -516,16 +523,22 @@ class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
self.textChanged.connect(self.numbify)
self.isdays = False
self.isyears = False
# The Check Alive field accepts a leading "-" (the sign selects the
# anchor date); the delivery field does not.
self.allow_negative = allow_negative
self.time_edit = time_edit
def numbify(self):
# Only digits plus the day ("d") and year ("y") suffixes are accepted.
# The block-height suffix ("b") was removed (A1): locktimes are always
# UNIX timestamps now, so block-relative input is no longer allowed.
# Only digits plus the day ("d") and year ("y") suffixes are accepted
# (and, for the Check Alive field, a single leading "-"). The
# block-height suffix ("b") was removed (A1): locktimes are always UNIX
# timestamps now, so block-relative input is no longer allowed.
# The sanitisation itself lives in bal.core.input_rules.
text = self.text().strip()
pos = self.cursorPosition()
s, isdays, isyears, pos = normalize_locktime_raw_text(text, pos)
s, isdays, isyears, pos = normalize_locktime_raw_text(
text, pos, allow_negative=self.allow_negative
)
self.isdays = isdays
self.isyears = isyears
self.blockSignals(True)
@@ -639,6 +652,11 @@ class ThresholdTimeWidget(BalTimeEditWidget):
"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 current day(ex: 1y means one year from today)<br>"
"The sign selects the reference date:<br>"
" - a NEGATIVE value (ex: -30d) counts backwards from the "
"<b>delivery</b> date (30 days before the delivery);<br>"
" - a POSITIVE value (ex: 30d) counts forward from the "
"<b>inheritance creation</b> date (30 days after the will was created).<br>"
"<br><b>Note:</b> the date/time is expressed in your computer's "
"<b>Local time</b> (not UTC time).<br>"
)
@@ -646,6 +664,8 @@ class ThresholdTimeWidget(BalTimeEditWidget):
#label_text = "Check Alive"
tooltip_text = "Check Alive"
base_field = "threshold"
# The Check Alive field accepts a signed value; the delivery field does not.
allow_negative_raw = True
def __init__(self, bal_window, parent, init_value=None):
if init_value is None:
@@ -1127,18 +1147,23 @@ class WillSettingsWidget(QWidget):
def on_locktime_change(self):
locktime = self.widgets["locktime"].get_value()
threshold = self.widgets["threshold"].get_value()
threshold_raw = self.widgets["threshold"].get_value()
locktime = BalTimestamp(locktime)
threshold = BalTimestamp(threshold)
min_locktime = min(
Will.get_min_locktime(self.bal_window.willitems, NLOCKTIME_MAX),
locktime.to_timestamp(),
)
# Resolved Check Alive date. For relative values this counts BACKWARDS
# from the delivery time (reverse=True): "90d" means "90 days BEFORE the
# delivery", not "90 days from now".
td = threshold.to_date(min_locktime, True)
# Resolved Check Alive date. The SIGN of the RAW value selects the
# anchor: negative counts backwards from the delivery, positive counts
# forward from the inheritance creation date. The rule lives in
# ``bal.core.checkalive`` so this display matches every validity check.
resolved = resolve_relative_check_alive_date(
threshold_raw,
delivery_date=min_locktime,
creation_date=Will.get_creation_time(self.bal_window.willitems),
)
td = BalTimestamp._safe_fromtimestamp(resolved)
self.widgets["threshold"].alarm=td
self.bal_window.will_settings["real_threshold"]=td.timestamp()
try:

View File

@@ -20,9 +20,11 @@ import threading
from electrum.util import MyEncoder
from ...core.checkalive import (
CheckAliveAfterDeliveryError,
CheckAliveError,
check_alive_expired,
ensure_check_alive_valid,
resolve_date_to_check,
resolve_delivery_date,
resolve_guard_threshold,
)
from .common import (
@@ -491,6 +493,7 @@ class BalWindow:
self.bal_plugin.is_basic_mode(),
self.will_settings,
built_locktime=_new_locktime,
creation_date=Will.get_creation_time(self.willitems),
)
will = {}
# willtodelete = []
@@ -739,6 +742,7 @@ class BalWindow:
self.bal_plugin.is_basic_mode(),
self.will_settings,
built_locktime=Will.get_min_locktime(self.willitems),
creation_date=Will.get_creation_time(self.willitems),
)
# found = False
# NOTE: block-height tracking removed (A1) - locktimes are always
@@ -749,19 +753,20 @@ class BalWindow:
self.willexecutors = Willexecutors.get_willexecutors(
self.bal_plugin, update=True, bal_window=self, task=False
)
# 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.
# The BASIC/ADVANCED rule lives in
# ``bal.core.checkalive.check_alive_expired``.
if check_alive_expired(
self.bal_plugin.is_basic_mode(), self.date_to_check
):
raise CheckAliveError(self.date_to_check)
# The Check Alive must be strictly after today and strictly before
# the delivery. In BASIC mode the single validity gate below is a
# no-op (see ``bal.core.checkalive.ensure_check_alive_valid``), so a
# passed check-alive date never forces a postpone/rewrite; the
# delivery time (locktime) stays fully enforced.
delivery_date = resolve_delivery_date(
self.will_settings,
built_locktime=Will.get_min_locktime(self.willitems),
)
ensure_check_alive_valid(
self.bal_plugin.is_basic_mode(),
self.date_to_check,
delivery_date=delivery_date,
)
self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get())
@@ -786,7 +791,9 @@ class BalWindow:
"""
locktime = Util.parse_locktime_string(self.will_settings["locktime"])
threshold_ts = resolve_guard_threshold(
self.bal_plugin.is_basic_mode(), self.will_settings
self.bal_plugin.is_basic_mode(),
self.will_settings,
creation_date=Will.get_creation_time(self.willitems),
)
if threshold_ts is not None:
return locktime < threshold_ts
@@ -836,6 +843,13 @@ class BalWindow:
_(f"Will-executor fee too high: {e}")
)
return
except CheckAliveAfterDeliveryError:
self.show_error(
_(
"CheckAlive must be after today and before the delivery date please update it"
)
)
return
except CheckAliveError:
self.show_error(
_(
@@ -1295,6 +1309,12 @@ class BalWindow:
# means the old will must be invalidated on-chain.
try:
self.init_class_variables()
except CheckAliveAfterDeliveryError:
_logger.info(
"auto rebuild: check-alive not before the delivery, "
"nothing to rebuild/invalidate"
)
return False
except CheckAliveError:
_logger.info("auto rebuild: check-alive threshold passed -> invalidate")
self._auto_invalidate_will()