191 lines
7.0 KiB
Python
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.")
|