Files
bal-electrum-plugin/tests/test_sync_locktime_built_txs.py

191 lines
7.0 KiB
Python

"""
Tests for ``BalBuildWillDialog._sync_locktime_to_built_txs``.
This is the post-build sync that keeps the plugin's stored delivery date
(WILL_SETTINGS["locktime"]) in lockstep with the BUILT transactions' fixed
locktime when the core AUTOMATICALLY anticipates it (one day earlier than
stored).
RELATIVE recipes ("90d" / "1y") are now PRESERVED: the daily-drift problem
that once forced freezing them to absolute timestamps is solved at the root by
anchoring every relative recipe against the built transactions
(``Util.resolve_locktime_against_tx`` for the postpone detection,
``resolve_date_to_check(..., built_locktime=...)`` for the reference
timestamp). Only a genuine automatic anticipation on an ABSOLUTE stored date
moves the stored value.
The method is exercised with a lightweight fake ``self`` (no Qt event loop, no
Electrum wallet) by calling it as an unbound method.
Run:
source "$BAL_HOME/electrum/env/bin/activate"
python3 tests/test_sync_locktime_built_txs.py
"""
import os
import sys
from types import SimpleNamespace
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
from bal.gui.qt.dialogs import BalBuildWillDialog # noqa: E402 (path insert above)
# ------------------------------------------------------------------ #
# Fakes
# ------------------------------------------------------------------ #
def _fake_willitem(tx_locktime):
"""Minimal stand-in for a WillItem: enough for Will.get_min_locktime."""
return SimpleNamespace(
tx=SimpleNamespace(locktime=tx_locktime),
get_status=lambda name: True,
)
def _make_dialog(will_settings, tx_locktimes, recorded):
"""Build a fake dialog ``self`` for _sync_locktime_to_built_txs."""
def update_setting_widgets(new_value, field, update_all=False):
will_settings[field] = new_value
recorded.append((field, new_value, update_all))
return SimpleNamespace(
bal_window=SimpleNamespace(
willitems={
f"tx{i}": _fake_willitem(lt) for i, lt in enumerate(tx_locktimes)
},
will_settings=will_settings,
update_setting_widgets=update_setting_widgets,
),
_date_was_anticipated=False,
)
def _call_sync(will_settings, tx_locktimes, recorded):
fake = _make_dialog(will_settings, tx_locktimes, recorded)
BalBuildWillDialog._sync_locktime_to_built_txs(fake)
return fake
# ------------------------------------------------------------------ #
# Tests
# ------------------------------------------------------------------ #
def test_relative_locktime_preserved():
"""A RELATIVE stored locktime ("90d"/"1y") is PRESERVED after a rebuild:
it is anchored against the built transactions on every check, so it must
not be frozen to an absolute timestamp in WILL_SETTINGS."""
tx_locktime = 1_800_000_000
recorded = []
fake = _call_sync(
{"locktime": "90d", "threshold": "30d"}, [tx_locktime], recorded
)
assert fake.bal_window.will_settings["locktime"] == "90d"
assert fake.bal_window.will_settings["threshold"] == "30d"
assert recorded == [], "a relative recipe must never be rewritten"
assert fake._date_was_anticipated is False
def test_relative_threshold_preserved():
"""Same for the relative "Check Alive" threshold: it stays relative."""
tx_locktime = 1_800_000_000
recorded = []
fake = _call_sync(
{"locktime": "90d", "threshold": "30d"}, [tx_locktime], recorded
)
assert fake.bal_window.will_settings["threshold"] == "30d"
assert recorded == []
def test_absolute_locktime_unchanged_on_equal():
"""An absolute stored locktime that already matches the built txs is left
untouched (no spurious rewrite)."""
tx_locktime = 1_800_000_000
recorded = []
fake = _call_sync(
{"locktime": tx_locktime, "threshold": 1_700_000_000},
[tx_locktime],
recorded,
)
assert fake.bal_window.will_settings["locktime"] == tx_locktime
assert fake._date_was_anticipated is False
def test_anticipation_sets_flag_and_moves_earlier():
"""A real automatic anticipation of an ABSOLUTE stored date (built earlier
than stored) still moves the date earlier and flags the sign prompt."""
tx_locktime = 1_700_000_000
recorded = []
fake = _call_sync(
{"locktime": 1_800_000_000, "threshold": 1_600_000_000},
[tx_locktime],
recorded,
)
assert fake.bal_window.will_settings["locktime"] == tx_locktime
assert fake._date_was_anticipated is True
def test_stored_earlier_than_built_never_moved_later():
"""A stored absolute date that is already EARLIER than the built txs (the
user moved the delivery later) is never pulled back up on rebuild: only
anticipation (built < stored) moves the value."""
stored = 1_800_000_000
recorded = []
fake = _call_sync(
{"locktime": stored, "threshold": 1_700_000_000},
[1_900_000_000],
recorded,
)
assert fake.bal_window.will_settings["locktime"] == stored
assert fake._date_was_anticipated is False
def test_multiple_txs_uses_minimum_locktime():
"""When several ABSOLUTE transactions carry different locktimes, the minimum
is used for a genuine automatic anticipation (owner-confirmed behaviour for
the delivery date shown in the UI)."""
min_locktime = 1_750_000_000
recorded = []
fake = _call_sync(
{"locktime": 1_800_000_000, "threshold": 1_600_000_000},
[min_locktime, min_locktime + 86_400],
recorded,
)
assert fake.bal_window.will_settings["locktime"] == min_locktime
def test_relative_locktime_stays_coherent_via_anchor():
"""Daily-drift guard: an UNCHANGED relative recipe is resolved against the
tx build moment (``Util.resolve_locktime_against_tx``), so even WITHOUT
being frozen to an absolute value it still reads as COHERENT (== tx
locktime) on later days - the postpone check never fires again."""
from datetime import datetime, timedelta, timezone
from bal.core.util import Util
# resolve_locktime_against_tx normalises to UTC midnight before anchoring,
# so use a midnight-UTC frozen tx locktime (the timestamp the engine itself
# stores after building).
tx_locktime = int(datetime(2027, 1, 15, tzinfo=timezone.utc).timestamp())
built = "90d" # recipe frozen at build time
current = "90d" # unchanged recipe today
for _day in range(0, 7):
resolved = Util.resolve_locktime_against_tx(current, built, tx_locktime)
assert resolved == tx_locktime # no POSTPONE / drift
# Sanity: a naive forward-from-now re-parse would have drifted past it
# (the bug the anchor fixes).
drifted = int(
(
datetime.fromtimestamp(tx_locktime) + timedelta(days=1)
).timestamp()
)
assert drifted > tx_locktime
if __name__ == "__main__":
for name in sorted(dir()):
if name.startswith("test_"):
globals()[name]()
print(f" [OK] {name}")
print("[OK] All sync_locktime_built_txs tests passed.")