forked from bitcoinafterlife/bal-electrum-plugin
fix(plugin): missed-update fixes, server re-check, and bold Building Will results (v0.3.2)
Revert the v0.3.1 double-invalidation change (it caused an inheritance-list regression: stale/invalidated wills lingered and heir/date updates became incoherent) and add several targeted missed-update fixes plus a UI refinement. Revert (v0.3.1 -> v0.3.2): - Remove Will.mark_invalidated_by_tx() and its call in loop_broadcast_invalidating. core/will.py and gui/qt/dialogs.py are restored to the working v0.3.0 behaviour. The postpone double-invalidation issue is intentionally left open, to be addressed without touching the shared broadcast path. FIX 1 - detect heir removal on Check / Electrum close: - core/will.py (check_willexecutors_and_heirs): the else-branch now raises HeirNotFoundException when a will still carries an heir that is no longer in the current heirs set (heir removed), mirroring the existing 'heir added' path. Rebuild therefore triggers on Check and on_close (same build_will_task path), as decided by the user (manual update only, no auto-rebuild). FIX 2 - Check queries servers for already-sent wills: - core/will.py: new Will.needs_server_check(w) returns True for any VALID will with a will-executor that is not yet CHECKED (no longer limited to PUSHED). - gui/qt/lists.py (PreviewList.check): use needs_server_check so wills stuck on 'New / Not sent' are re-checked instead of reporting 'nothing to do'. FIX 3 - Settings-dialog hide toggles refresh the list: - core/plugin_base.py: new sync_hide_filters() re-reads the cached _hide_invalidated / _hide_replaced flags from the persisted config. - gui/qt/window.py (update_all): call sync_hide_filters() before refreshing, so toggling 'Hide Invalidated' / 'Hide Replaced' in the Settings dialog (which writes the config directly) updates the transaction list immediately instead of requiring an Electrum restart. UI - bold results in the Building Will dialog: - gui/qt/dialogs.py (BalBuildWillDialog): render the right-side results in bold (Ok, Ko, Nothing to do, Skipped, Wait, Timeout, ...) keeping the left-side state labels in normal weight. Centralised in msg_ok/msg_error/msg_warning/ msg_set_status, plus the will-executor push/check rows now show Ok/Ko and True/False in bold + colour (green/red). Tests/tooling: - tests/test_core_will.py: add test_check_heirs_unchanged_is_coherent, test_check_heir_removed_triggers_rebuild, test_check_heir_added_triggers_rebuild, test_needs_server_check. - tests/sim_update_flows.py: real-world update-scenario simulation. - tests/preview_build_will_dialog.py, tests/preview_we_rows.py: GUI-only before/after previews of the bold formatting. - Bump version to 0.3.2 (VERSION, manifest.json, __init__.py, plugin_base.py). 186 tests pass; smoke test, external-zip test and update-flow simulation OK; ruff reports only pre-existing star-import false positives.
This commit is contained in:
110
tests/preview_build_will_dialog.py
Normal file
110
tests/preview_build_will_dialog.py
Normal file
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render a visual PREVIEW (before/after) of the "Building Will" dialog text.
|
||||
|
||||
This is a throwaway, GUI-only helper used to show the user how the proposed
|
||||
"bold results" formatting looks compared to the current rendering, BEFORE any
|
||||
production code is changed. It does NOT import the plugin; it just reproduces
|
||||
the exact rich-text the dialog builds via ``msg_set_status`` / ``msg_ok`` /
|
||||
``msg_error`` so the preview is faithful.
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen python3 tests/preview_build_will_dialog.py
|
||||
It writes two PNGs in the repo root: preview_before.png and preview_after.png.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
# Same colors as BalBuildWillDialog
|
||||
COLOR_WARNING = "#cfa808"
|
||||
COLOR_ERROR = "#ff0000"
|
||||
COLOR_OK = "#05ad05"
|
||||
|
||||
|
||||
# ---- current rendering (BEFORE) -------------------------------------------
|
||||
def ok_before(e="Ok"):
|
||||
return "<font color='{}'>{}</font>".format(COLOR_OK, e)
|
||||
|
||||
|
||||
def error_before(e):
|
||||
return "<font color='{}'>{}</font>".format(COLOR_ERROR, e)
|
||||
|
||||
|
||||
def row_before(msg, status, color=None):
|
||||
if color is None:
|
||||
return f"{msg}:\t{status}"
|
||||
return "<font color={}>{}:\t{}</font>".format(color, msg, status)
|
||||
|
||||
|
||||
# ---- proposed rendering (AFTER): results in bold --------------------------
|
||||
def ok_after(e="Ok"):
|
||||
return "<font color='{}'><b>{}</b></font>".format(COLOR_OK, e)
|
||||
|
||||
|
||||
def error_after(e):
|
||||
return "<font color='{}'><b>{}</b></font>".format(COLOR_ERROR, e)
|
||||
|
||||
|
||||
def row_after(msg, status, color=None):
|
||||
# Left state label stays normal; only the result (status) becomes bold.
|
||||
if color is None:
|
||||
return f"{msg}:\t<b>{status}</b>"
|
||||
# When a color is given for the whole line, keep the label normal and bold
|
||||
# only the status portion.
|
||||
return "{}:\t<font color={}><b>{}</b></font>".format(msg, color, status)
|
||||
|
||||
|
||||
def build_rows(mode):
|
||||
if mode == "before":
|
||||
ok, err, row = ok_before, error_before, row_before
|
||||
else:
|
||||
ok, err, row = ok_after, error_after, row_after
|
||||
rows = [
|
||||
row("checking variables", "Wait"),
|
||||
row("Checking your will", ok()),
|
||||
row("Signing your will", "Nothing to do"),
|
||||
row("Broadcasting your will to executors", "Nothing to do"),
|
||||
ok(),
|
||||
row("Invalidating old will", err("Ko")),
|
||||
"https://executor.example.org : " + ok(),
|
||||
"https://other.example.org : " + err("Ko"),
|
||||
"Please wait 2secs",
|
||||
row("Will-Executor excluded", "Skipped", COLOR_ERROR),
|
||||
]
|
||||
return rows
|
||||
|
||||
|
||||
def render(mode, path):
|
||||
rows = build_rows(mode)
|
||||
full_text = "<br><br>".join(rows).replace("\n", "<br>")
|
||||
w = QWidget()
|
||||
w.setStyleSheet("background:#2b2b2b;")
|
||||
lay = QVBoxLayout(w)
|
||||
title = QLabel(f"Building Will — {mode.upper()}")
|
||||
title.setStyleSheet("color:#ffffff; font-size:15px; font-weight:bold;")
|
||||
lbl = QLabel(full_text)
|
||||
lbl.setTextFormat(Qt.TextFormat.RichText)
|
||||
lbl.setStyleSheet("color:#dddddd; font-size:13px;")
|
||||
lbl_font = lbl.font()
|
||||
lbl_font.setPointSize(11)
|
||||
lbl.setFont(lbl_font)
|
||||
lay.addWidget(title)
|
||||
lay.addWidget(lbl)
|
||||
w.resize(560, 420)
|
||||
w.show()
|
||||
app.processEvents()
|
||||
pix = w.grab()
|
||||
pix.save(path)
|
||||
print(f"[{mode}] saved -> {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
render("before", os.path.join(here, "preview_before.png"))
|
||||
render("after", os.path.join(here, "preview_after.png"))
|
||||
105
tests/preview_we_rows.py
Normal file
105
tests/preview_we_rows.py
Normal file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Visual PREVIEW focused on the WILL-EXECUTOR rows of the Building Will dialog.
|
||||
|
||||
Reproduces faithfully the three real variants built in dialogs.py:
|
||||
|
||||
1. Broadcasting (push) result -> line 774: "{url} : {Ok|Ko}" (plain, no color today)
|
||||
2. Timeout -> line 783: "{url} : <font red>Timeout - no answer</font>"
|
||||
3. Checking already-present -> line 825/834:
|
||||
"checking {url} - {wid} : Waiting"
|
||||
"checked {url} - {wid} : True/False" (plain, no color today)
|
||||
|
||||
Shows BEFORE (current) vs AFTER (proposed: result in bold, keeping label as-is).
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen python3 tests/preview_we_rows.py
|
||||
Writes preview_we_before.png / preview_we_after.png in the repo root.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QLabel, QWidget, QVBoxLayout
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
COLOR_ERROR = "#ff0000"
|
||||
COLOR_OK = "#05ad05"
|
||||
|
||||
URL1 = "https://executor.example.org"
|
||||
URL2 = "https://other-executor.net"
|
||||
WID = "a1b2c3"
|
||||
|
||||
|
||||
def err(e):
|
||||
return "<font color='{}'>{}</font>".format(COLOR_ERROR, e)
|
||||
|
||||
|
||||
# ---------------- BEFORE: exactly as the code builds today -----------------
|
||||
def rows_before():
|
||||
return [
|
||||
# 1. push results (plain text, no color/bold today)
|
||||
"{} : {}".format(URL1, "Ok"),
|
||||
"{} : {}".format(URL2, "Ko"),
|
||||
# 2. timeout (already red, not bold)
|
||||
"{} : {}".format(URL1, err("Timeout - no answer")),
|
||||
# 3. already-present check
|
||||
"checking {} - {} : {}".format(URL1, WID, "Waiting"),
|
||||
"checked {} - {} : {}".format(URL1, WID, "True"),
|
||||
"checked {} - {} : {}".format(URL2, WID, "False"),
|
||||
]
|
||||
|
||||
|
||||
# ---------------- AFTER: result portion in bold, label unchanged -----------
|
||||
def err_after(e):
|
||||
return "<font color='{}'><b>{}</b></font>".format(COLOR_ERROR, e)
|
||||
|
||||
|
||||
def rows_after():
|
||||
return [
|
||||
# 1. push results: color + bold the Ok / Ko outcome
|
||||
"{} : <font color='{}'><b>{}</b></font>".format(URL1, COLOR_OK, "Ok"),
|
||||
"{} : <font color='{}'><b>{}</b></font>".format(URL2, COLOR_ERROR, "Ko"),
|
||||
# 2. timeout: bold the red message
|
||||
"{} : {}".format(URL1, err_after("Timeout - no answer")),
|
||||
# 3. already-present check: bold the result
|
||||
"checking {} - {} : <b>{}</b>".format(URL1, WID, "Waiting"),
|
||||
"checked {} - {} : <font color='{}'><b>{}</b></font>".format(
|
||||
URL1, WID, COLOR_OK, "True"
|
||||
),
|
||||
"checked {} - {} : <font color='{}'><b>{}</b></font>".format(
|
||||
URL2, WID, COLOR_ERROR, "False"
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def render(rows, title, path):
|
||||
full_text = "<br><br>".join(rows).replace("\n", "<br>")
|
||||
w = QWidget()
|
||||
w.setStyleSheet("background:#2b2b2b;")
|
||||
lay = QVBoxLayout(w)
|
||||
t = QLabel(title)
|
||||
t.setStyleSheet("color:#ffffff; font-size:15px; font-weight:bold;")
|
||||
lbl = QLabel(full_text)
|
||||
lbl.setTextFormat(Qt.TextFormat.RichText)
|
||||
lbl.setStyleSheet("color:#dddddd;")
|
||||
f = lbl.font()
|
||||
f.setPointSize(11)
|
||||
lbl.setFont(f)
|
||||
lay.addWidget(t)
|
||||
lay.addWidget(lbl)
|
||||
w.resize(560, 320)
|
||||
w.show()
|
||||
app.processEvents()
|
||||
w.grab().save(path)
|
||||
print(f"saved -> {path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
render(rows_before(), "Will-Executor rows — BEFORE",
|
||||
os.path.join(here, "preview_we_before.png"))
|
||||
render(rows_after(), "Will-Executor rows — AFTER",
|
||||
os.path.join(here, "preview_we_after.png"))
|
||||
171
tests/sim_update_flows.py
Normal file
171
tests/sim_update_flows.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Real-world simulation of the inheritance update flows.
|
||||
|
||||
This script does NOT touch the GUI. It drives the core decision function
|
||||
``Will.check_willexecutors_and_heirs`` (the one that decides whether a will is
|
||||
still coherent or must be rebuilt) through the scenarios the user reported:
|
||||
|
||||
1. delivery date moved forward (postpone) -> must NOT stay "coherent"
|
||||
2. an heir is added -> must trigger rebuild
|
||||
3. an heir is removed -> must trigger rebuild
|
||||
4. a single heir percentage / amount is changed -> must trigger rebuild
|
||||
5. nothing changed -> stays coherent
|
||||
|
||||
For each scenario we report which exception (if any) is raised, because that is
|
||||
exactly what the GUI relies on to decide whether to rebuild the inheritance
|
||||
transactions. If the function returns True (coherent) when something DID
|
||||
change, the GUI will (correctly) show no update -- which is the symptom the
|
||||
user described.
|
||||
|
||||
Run:
|
||||
QT_QPA_PLATFORM=offscreen PYTHONPATH=electrum-src python3 tests/sim_update_flows.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import copy
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir))
|
||||
|
||||
from bal.core.will import (
|
||||
WillItem, Will,
|
||||
NotCompleteWillException, HeirNotFoundException, NoHeirsException,
|
||||
TxFeesChangedException, WillExpiredException,
|
||||
)
|
||||
from bal.core.util import Util
|
||||
|
||||
# A valid serialized tx (1 input + 1 output, version 2). Its nLockTime is 0.
|
||||
_VALID_TX_HEX = (
|
||||
"01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b"
|
||||
"f38633b424eb4031000000006c493046022100a82bbc57a0136751e543"
|
||||
"3f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7d"
|
||||
"e89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501"
|
||||
"2102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae3"
|
||||
"5cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a"
|
||||
"42146f11ef8414ae929feaafc388ac00000000"
|
||||
)
|
||||
|
||||
# A locktime far in the past (so the frozen tx.locktime is a fixed integer we
|
||||
# control via monkey-patching below). We will override w.tx.locktime per test.
|
||||
TX_FEES = 100
|
||||
|
||||
|
||||
def _make_will_item(heirs, tx_locktime, status_complete=False):
|
||||
"""Build a WillItem whose stored heirs == ``heirs`` and whose tx.locktime
|
||||
is forced to ``tx_locktime`` (the value frozen in the signed Bitcoin tx)."""
|
||||
d = {
|
||||
"tx": _VALID_TX_HEX,
|
||||
"heirs": copy.deepcopy(heirs),
|
||||
"willexecutor": None,
|
||||
"status": "",
|
||||
"description": "",
|
||||
"time": 0,
|
||||
"change": "",
|
||||
"baltx_fees": TX_FEES,
|
||||
}
|
||||
item = WillItem(d, _id="willid_1")
|
||||
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
|
||||
# Force the locktime frozen "inside" the signed tx.
|
||||
item.tx.locktime = tx_locktime
|
||||
if status_complete:
|
||||
item.set_status("COMPLETE", True)
|
||||
return item
|
||||
|
||||
|
||||
def _run(label, will_heirs, current_heirs, tx_locktime,
|
||||
status_complete=False, check_date=0):
|
||||
"""Run check_willexecutors_and_heirs and report the outcome."""
|
||||
item = _make_will_item(will_heirs, tx_locktime, status_complete)
|
||||
will = {"willid_1": item}
|
||||
outcome = None
|
||||
try:
|
||||
result = Will.check_willexecutors_and_heirs(
|
||||
will,
|
||||
current_heirs, # the (possibly edited) heirs dict
|
||||
{}, # willexecutors
|
||||
False, # self_willexecutor
|
||||
check_date, # check_date (timestamp)
|
||||
TX_FEES, # tx_fees
|
||||
)
|
||||
outcome = f"coherent (returned {result})"
|
||||
except HeirNotFoundException as e:
|
||||
outcome = f"HeirNotFoundException: {e}"
|
||||
except NoHeirsException as e:
|
||||
outcome = f"NoHeirsException: {e}"
|
||||
except TxFeesChangedException as e:
|
||||
outcome = f"TxFeesChangedException: {e}"
|
||||
except WillExpiredException as e:
|
||||
outcome = f"WillExpiredException: {e}"
|
||||
except NotCompleteWillException as e:
|
||||
outcome = f"{type(e).__name__}: {e}"
|
||||
except Exception as e:
|
||||
outcome = f"!! UNEXPECTED {type(e).__name__}: {e}"
|
||||
print(f"[{label}]")
|
||||
print(f" -> {outcome}")
|
||||
return outcome
|
||||
|
||||
|
||||
def main():
|
||||
# locktime string "0d" -> Util.parse_locktime_string returns a timestamp
|
||||
# ~ now. We use explicit integer timestamps to keep things deterministic.
|
||||
base_lt = 1900000000 # frozen tx.locktime (year ~2030)
|
||||
later_lt = "2000000000" # a later locktime string (postpone)
|
||||
same_lt = str(base_lt)
|
||||
|
||||
# Scenario 0: nothing changed -> should be coherent.
|
||||
heirs = {"alice": ["addr_alice", 5000, same_lt]}
|
||||
_run("0. nothing changed",
|
||||
will_heirs=heirs, current_heirs=copy.deepcopy(heirs),
|
||||
tx_locktime=base_lt, check_date=0)
|
||||
|
||||
# Scenario 1: delivery date moved forward (postpone), will NOT yet signed.
|
||||
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||
heirs_now = {"alice": ["addr_alice", 5000, later_lt]}
|
||||
_run("1. date postponed (unsigned will)",
|
||||
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||
tx_locktime=base_lt, check_date=0)
|
||||
|
||||
# Scenario 1b: postpone on a SIGNED will (status COMPLETE).
|
||||
_run("1b. date postponed (SIGNED will)",
|
||||
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||
tx_locktime=base_lt, status_complete=True, check_date=0)
|
||||
|
||||
# Scenario 2: an heir is ADDED.
|
||||
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||
heirs_now = {
|
||||
"alice": ["addr_alice", 5000, same_lt],
|
||||
"bob": ["addr_bob", 3000, same_lt],
|
||||
}
|
||||
_run("2. heir added (bob)",
|
||||
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||
tx_locktime=base_lt, check_date=0)
|
||||
|
||||
# Scenario 3: an heir is REMOVED.
|
||||
heirs_will = {
|
||||
"alice": ["addr_alice", 5000, same_lt],
|
||||
"bob": ["addr_bob", 3000, same_lt],
|
||||
}
|
||||
heirs_now = {"alice": ["addr_alice", 5000, same_lt]}
|
||||
_run("3. heir removed (bob)",
|
||||
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||
tx_locktime=base_lt, check_date=0)
|
||||
|
||||
# Scenario 4: a single heir AMOUNT/percentage changed.
|
||||
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||
heirs_now = {"alice": ["addr_alice", 9999, same_lt]}
|
||||
_run("4. heir amount changed (5000 -> 9999)",
|
||||
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||
tx_locktime=base_lt, check_date=0)
|
||||
|
||||
# Scenario 5: heir ADDRESS changed.
|
||||
heirs_will = {"alice": ["addr_alice", 5000, same_lt]}
|
||||
heirs_now = {"alice": ["addr_NEW", 5000, same_lt]}
|
||||
_run("5. heir address changed",
|
||||
will_heirs=heirs_will, current_heirs=heirs_now,
|
||||
tx_locktime=base_lt, check_date=0)
|
||||
|
||||
print("\n[done] simulation finished")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -164,6 +164,103 @@ def test_will_only_valid_list():
|
||||
assert "b" not in result
|
||||
|
||||
|
||||
def _make_will_with_heirs(heirs, tx_locktime):
|
||||
"""Build a single-item will whose stored heirs == ``heirs`` and whose
|
||||
frozen tx.locktime == ``tx_locktime`` (what the will-executors hold)."""
|
||||
item = WillItem(_make_minimal_willitem_dict(heirs=copy.deepcopy(heirs)))
|
||||
item.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
|
||||
item.tx.locktime = tx_locktime
|
||||
return {"willid_1": item}
|
||||
|
||||
|
||||
def test_check_heirs_unchanged_is_coherent():
|
||||
"""No heir change -> the will stays coherent (no rebuild)."""
|
||||
lt = 1900000000
|
||||
heirs = {"alice": ["addr_alice", 5000, str(lt)]}
|
||||
will = _make_will_with_heirs(heirs, lt)
|
||||
result = Will.check_willexecutors_and_heirs(
|
||||
will, copy.deepcopy(heirs), {}, False, 0, 100
|
||||
)
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_check_heir_removed_triggers_rebuild():
|
||||
"""Removing an heir MUST be detected (HeirNotFoundException), so the Check
|
||||
button and on_close rebuild the inheritance. Regression test for the bug
|
||||
where a removed heir silently stayed in the transaction."""
|
||||
from bal.core.will import HeirNotFoundException
|
||||
lt = 1900000000
|
||||
will_heirs = {
|
||||
"alice": ["addr_alice", 5000, str(lt)],
|
||||
"bob": ["addr_bob", 3000, str(lt)],
|
||||
}
|
||||
current_heirs = {"alice": ["addr_alice", 5000, str(lt)]} # bob removed
|
||||
will = _make_will_with_heirs(will_heirs, lt)
|
||||
raised = False
|
||||
try:
|
||||
Will.check_willexecutors_and_heirs(
|
||||
will, current_heirs, {}, False, 0, 100
|
||||
)
|
||||
except HeirNotFoundException:
|
||||
raised = True
|
||||
assert raised, "removing an heir must raise HeirNotFoundException"
|
||||
|
||||
|
||||
def test_check_heir_added_triggers_rebuild():
|
||||
"""Adding an heir must be detected (HeirNotFoundException)."""
|
||||
from bal.core.will import HeirNotFoundException
|
||||
lt = 1900000000
|
||||
will_heirs = {"alice": ["addr_alice", 5000, str(lt)]}
|
||||
current_heirs = {
|
||||
"alice": ["addr_alice", 5000, str(lt)],
|
||||
"bob": ["addr_bob", 3000, str(lt)], # added
|
||||
}
|
||||
will = _make_will_with_heirs(will_heirs, lt)
|
||||
raised = False
|
||||
try:
|
||||
Will.check_willexecutors_and_heirs(
|
||||
will, current_heirs, {}, False, 0, 100
|
||||
)
|
||||
except HeirNotFoundException:
|
||||
raised = True
|
||||
assert raised, "adding an heir must raise HeirNotFoundException"
|
||||
|
||||
|
||||
def test_needs_server_check():
|
||||
"""Check button selection logic: a VALID will with a will-executor that is
|
||||
not yet CHECKED must be queried on the server, even if it is not PUSHED
|
||||
(regression for the 'New / Not sent' wills that Check ignored)."""
|
||||
we = {"url": "https://we.example.com"}
|
||||
|
||||
# New (not PUSHED) but has a will-executor -> must be checked.
|
||||
item_new = _make_willitem_blank()
|
||||
item_new.we = we
|
||||
assert Will.needs_server_check(item_new) is True
|
||||
|
||||
# PUSHED but not CHECKED -> must be checked (previous behaviour).
|
||||
item_pushed = _make_willitem_blank()
|
||||
item_pushed.we = we
|
||||
item_pushed.set_status("PUSHED", True)
|
||||
assert Will.needs_server_check(item_pushed) is True
|
||||
|
||||
# Already CHECKED -> no need to check again.
|
||||
item_checked = _make_willitem_blank()
|
||||
item_checked.we = we
|
||||
item_checked.set_status("CHECKED", True)
|
||||
assert Will.needs_server_check(item_checked) is False
|
||||
|
||||
# No will-executor assigned -> nothing to check on a server.
|
||||
item_no_we = _make_willitem_blank()
|
||||
item_no_we.we = None
|
||||
assert Will.needs_server_check(item_no_we) is False
|
||||
|
||||
# Not VALID (e.g. invalidated) -> not checked.
|
||||
item_invalid = _make_willitem_blank()
|
||||
item_invalid.we = we
|
||||
item_invalid.set_status("INVALIDATED", True)
|
||||
assert Will.needs_server_check(item_invalid) is False
|
||||
|
||||
|
||||
def test_will_is_new():
|
||||
item1 = _make_willitem_blank()
|
||||
item1.set_status("COMPLETE", True)
|
||||
@@ -228,79 +325,6 @@ def test_will_check_tx_height():
|
||||
# Exception classes
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def test_will_mark_invalidated_by_tx():
|
||||
"""A valid will spending the same prevout as the invalidation tx must be
|
||||
marked INVALIDATED (and therefore lose its VALID flag). This is what
|
||||
prevents the postpone/expire check from firing a *second* invalidation
|
||||
when phase 1 is restarted after a successful on-chain invalidation."""
|
||||
class FakePrevout:
|
||||
def __init__(self, s):
|
||||
self._s = s
|
||||
def to_str(self):
|
||||
return self._s
|
||||
|
||||
class FakeInput:
|
||||
def __init__(self, s):
|
||||
self.prevout = FakePrevout(s)
|
||||
|
||||
class FakeTx:
|
||||
def __init__(self, prevouts):
|
||||
self._inputs = [FakeInput(p) for p in prevouts]
|
||||
def inputs(self):
|
||||
return self._inputs
|
||||
|
||||
# The real test will item spends this prevout (from _VALID_TX_HEX).
|
||||
spent = "3140eb24b43386f35ba69e3875eb6c93130ac66201d01c58f598defc949a5c2a:0"
|
||||
|
||||
# Will item that spends the same UTXO -> must be invalidated.
|
||||
item_match = _make_willitem_blank()
|
||||
item_match.set_status("COMPLETE", True)
|
||||
# Will item that spends an unrelated UTXO -> must stay VALID.
|
||||
item_other = _make_willitem_blank()
|
||||
item_other.tx = FakeTx(["deadbeef:1"])
|
||||
item_other.children = {}
|
||||
|
||||
will = {"match": item_match, "other": item_other}
|
||||
inval_tx = FakeTx([spent])
|
||||
|
||||
invalidated = Will.mark_invalidated_by_tx(will, inval_tx)
|
||||
|
||||
assert "match" in invalidated
|
||||
assert "other" not in invalidated
|
||||
assert will["match"].get_status("INVALIDATED") is True
|
||||
assert will["match"].get_status("VALID") is False
|
||||
assert will["other"].get_status("VALID") is True
|
||||
|
||||
|
||||
def test_will_mark_invalidated_by_tx_no_match():
|
||||
"""If no valid will spends any of the invalidation tx's prevouts, nothing
|
||||
is marked."""
|
||||
class FakePrevout:
|
||||
def __init__(self, s):
|
||||
self._s = s
|
||||
def to_str(self):
|
||||
return self._s
|
||||
|
||||
class FakeInput:
|
||||
def __init__(self, s):
|
||||
self.prevout = FakePrevout(s)
|
||||
|
||||
class FakeTx:
|
||||
def __init__(self, prevouts):
|
||||
self._inputs = [FakeInput(p) for p in prevouts]
|
||||
def inputs(self):
|
||||
return self._inputs
|
||||
|
||||
item = _make_willitem_blank()
|
||||
will = {"a": item}
|
||||
inval_tx = FakeTx(["unrelated:9"])
|
||||
|
||||
invalidated = Will.mark_invalidated_by_tx(will, inval_tx)
|
||||
|
||||
assert invalidated == []
|
||||
assert will["a"].get_status("VALID") is True
|
||||
|
||||
|
||||
def test_exceptions():
|
||||
from bal.core.will import (
|
||||
WillException, WillExpiredException, NotCompleteWillException,
|
||||
|
||||
Reference in New Issue
Block a user