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:
@@ -1 +1 @@
|
||||
0.3.1
|
||||
0.3.2
|
||||
@@ -34,4 +34,4 @@ The plugin targets Electrum 4.7.2 (the last stable release exposing
|
||||
``json_db.register_dict``) and PyQt6.
|
||||
"""
|
||||
|
||||
__version__ = "0.3.1"
|
||||
__version__ = "0.3.2"
|
||||
|
||||
@@ -91,7 +91,7 @@ class BalPlugin(BasePlugin):
|
||||
"""
|
||||
|
||||
_version = None
|
||||
__version__ = "0.3.1" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
__version__ = "0.3.2" # AUTOMATICALLY GENERATED DO NOT EDIT
|
||||
|
||||
# Command used to open an .ics calendar file, per operating system.
|
||||
default_app = {
|
||||
@@ -237,6 +237,29 @@ class BalPlugin(BasePlugin):
|
||||
"""Absolute path to a file bundled inside the plugin directory."""
|
||||
return os.path.join(self.plugin_dir, *parts)
|
||||
|
||||
def sync_hide_filters(self):
|
||||
"""Re-read the "hide" filter flags from the persisted config.
|
||||
|
||||
The cached ``_hide_invalidated`` / ``_hide_replaced`` flags are used by
|
||||
the GUI list to decide which rows to skip. They can be changed from two
|
||||
different places:
|
||||
|
||||
* the list toolbar buttons, which call :meth:`hide_invalidated` /
|
||||
:meth:`hide_replaced` (a toggle that updates both the cache and the
|
||||
config), and
|
||||
* the Settings dialog checkboxes, which write the config directly
|
||||
(``BalConfig.set``) without touching the cached flags.
|
||||
|
||||
In the second case the cache and the config would drift apart and the
|
||||
transaction list would keep filtering with the *old* value, so the
|
||||
toggled rows never appear/disappear until Electrum is restarted.
|
||||
Re-syncing the cache from the config here (called by ``update_all``)
|
||||
keeps every code path coherent regardless of where the change came
|
||||
from.
|
||||
"""
|
||||
self._hide_invalidated = self.HIDE_INVALIDATED.get()
|
||||
self._hide_replaced = self.HIDE_REPLACED.get()
|
||||
|
||||
def hide_invalidated(self):
|
||||
"""Toggle (and persist) the "hide invalidated transactions" filter."""
|
||||
self._hide_invalidated = not self._hide_invalidated
|
||||
|
||||
@@ -83,6 +83,25 @@ class Will:
|
||||
if v.get_status("VALID"):
|
||||
yield k
|
||||
|
||||
@staticmethod
|
||||
def needs_server_check(w):
|
||||
"""Return True if ``w`` should be queried on its will-executor server
|
||||
when the user presses Check (or on Electrum close).
|
||||
|
||||
A will needs a server check when it is VALID, has a will-executor
|
||||
assigned, and is not yet CHECKED. This intentionally includes wills
|
||||
that are not (yet) marked PUSHED: a will that was actually sent in the
|
||||
past but whose saved status still reads "New" would otherwise be
|
||||
skipped, leaving the Server column stuck on "Not sent". The server
|
||||
response (see WillItem.set_check_willexecutor) then corrects the status
|
||||
to PUSHED/CHECKED if the transaction is present, or CHECK_FAIL if not.
|
||||
"""
|
||||
return bool(
|
||||
w.get_status("VALID")
|
||||
and w.we
|
||||
and not w.get_status("CHECKED")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def search_equal_tx(will, tx, wid):
|
||||
for w in will:
|
||||
@@ -420,31 +439,6 @@ class Will:
|
||||
_logger.debug("len utxo_to_spend <=0")
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def mark_invalidated_by_tx(will, tx):
|
||||
"""Mark as INVALIDATED every valid will item that spends at least one
|
||||
of the prevouts consumed by ``tx`` (the on-chain invalidation tx that
|
||||
was just broadcast).
|
||||
|
||||
Once the invalidation tx is broadcast, the previously signed/sent will
|
||||
transactions that relied on those same UTXOs can no longer be mined, so
|
||||
their will items must stop being VALID. Setting INVALIDATED clears the
|
||||
VALID flag (see WillItem.set_status), which removes them from
|
||||
only_valid_list and therefore prevents the postpone/expire check from
|
||||
firing a *second* invalidation on the next pass.
|
||||
|
||||
Returns the list of will ids that were marked.
|
||||
"""
|
||||
spent_prevouts = {i.prevout.to_str() for i in tx.inputs()}
|
||||
invalidated = []
|
||||
for wid in Will.only_valid_list(will):
|
||||
w = will[wid]
|
||||
wi_prevouts = {i.prevout.to_str() for i in w.tx.inputs()}
|
||||
if spent_prevouts & wi_prevouts:
|
||||
Will.set_invalidate(wid, will)
|
||||
invalidated.append(wid)
|
||||
return invalidated
|
||||
|
||||
@staticmethod
|
||||
def is_new(will):
|
||||
for wid, w in will.items():
|
||||
@@ -732,9 +726,18 @@ class Will:
|
||||
# signed/sent falls through here -> a plain rebuild via
|
||||
# HeirNotFoundException (no on-chain fee needed).
|
||||
else:
|
||||
# The will still carries this heir, but the heir is no
|
||||
# longer present in the current heirs set: the user
|
||||
# removed it. This must trigger a rebuild exactly like
|
||||
# "heir added" does, otherwise the removed heir would
|
||||
# silently stay in the inheritance transaction. Raising
|
||||
# HeirNotFoundException reuses the same rebuild path used
|
||||
# by the Check button and by on_close (Electrum quit).
|
||||
_logger.debug(
|
||||
f"heir not present transaction is not valid:{wheir} {wid}, {w}"
|
||||
f"heir removed, transaction is not valid:"
|
||||
f"{wheir} {wid}, {w}"
|
||||
)
|
||||
raise HeirNotFoundException(wheir)
|
||||
|
||||
if willexecutor := w.we:
|
||||
count = willexecutors_found.get(willexecutor["url"], 0)
|
||||
|
||||
@@ -708,19 +708,6 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.msg_set_invalidating(self.msg_ok())
|
||||
if not txid:
|
||||
_logger.debug(f"should not be none txid: {txid}")
|
||||
else:
|
||||
# The invalidation tx is now broadcast, so the old signed/sent
|
||||
# will transactions spending those same UTXOs can no longer be
|
||||
# mined. Mark them INVALIDATED (which clears their VALID flag)
|
||||
# so the postpone/expire check does NOT fire a second
|
||||
# invalidation when phase 1 is restarted to rebuild the new
|
||||
# (postponed) will.
|
||||
invalidated = Will.mark_invalidated_by_tx(
|
||||
self.bal_window.willitems, tx
|
||||
)
|
||||
if invalidated:
|
||||
_logger.debug(f"invalidated will items: {invalidated}")
|
||||
self.bal_window.save_willitems()
|
||||
|
||||
except TxBroadcastError as e:
|
||||
_logger.error(f"fail to broadcast transaction:{e}")
|
||||
@@ -784,7 +771,10 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.bal_window.willitems[wid].set_status("PUSH_FAIL", True)
|
||||
retry_flag["value"] = True
|
||||
done["count"] += 1
|
||||
self.msg_edit_row("{} : {}".format(url, "Ok" if ok else "Ko"))
|
||||
# Show the per-server result (Ok/Ko) in bold + color so the
|
||||
# outcome stands out, keeping the server URL in normal weight.
|
||||
result = self.msg_ok("Ok") if ok else self.msg_error("Ko")
|
||||
self.msg_edit_row("{} : {}".format(url, result))
|
||||
self.msg_set_pushing(_status_line())
|
||||
|
||||
def on_timeout(url, willexecutor):
|
||||
@@ -836,7 +826,7 @@ class BalBuildWillDialog(BalDialog):
|
||||
if self._stopping:
|
||||
return
|
||||
row = self.msg_edit_row(
|
||||
"checking {} - {} : {}".format(
|
||||
"checking {} - {} : <b>{}</b>".format(
|
||||
self.bal_window.willitems[wid].we["url"], wid, "Waiting"
|
||||
)
|
||||
)
|
||||
@@ -844,11 +834,16 @@ class BalBuildWillDialog(BalDialog):
|
||||
w.set_check_willexecutor(
|
||||
Willexecutors.check_transaction(wid, w.we["url"])
|
||||
)
|
||||
# Show the CHECKED result in bold + color (green True /
|
||||
# red False) so the outcome stands out, keeping the server
|
||||
# URL and tx id in normal weight.
|
||||
checked = self.bal_window.willitems[wid].get_status("CHECKED")
|
||||
result = self.msg_ok(checked) if checked else self.msg_error(checked)
|
||||
row = self.msg_edit_row(
|
||||
"checked {} - {} : {}".format(
|
||||
self.bal_window.willitems[wid].we["url"],
|
||||
wid,
|
||||
self.bal_window.willitems[wid].get_status("CHECKED"),
|
||||
result,
|
||||
),
|
||||
row,
|
||||
)
|
||||
@@ -1034,20 +1029,31 @@ class BalBuildWillDialog(BalDialog):
|
||||
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
|
||||
|
||||
def msg_error(self, e):
|
||||
return "<font color='{}'>{}</font>".format(self.COLOR_ERROR, e)
|
||||
# Results are shown in bold so the outcome stands out from the
|
||||
# left-side state label (which stays in normal weight).
|
||||
return "<font color='{}'><b>{}</b></font>".format(self.COLOR_ERROR, e)
|
||||
|
||||
def msg_ok(self, e="Ok"):
|
||||
return "<font color='{}'>{}</font>".format(self.COLOR_OK, e)
|
||||
# Results are shown in bold (see msg_error).
|
||||
return "<font color='{}'><b>{}</b></font>".format(self.COLOR_OK, e)
|
||||
|
||||
def msg_warning(self, e):
|
||||
return "<font color='{}'>{}</font".format(self.COLOR_WARNING, e)
|
||||
# Results are shown in bold (see msg_error).
|
||||
return "<font color='{}'><b>{}</b></font>".format(self.COLOR_WARNING, e)
|
||||
|
||||
def msg_set_status(self, msg, row=None, status=None, color=None):
|
||||
# The left "state" label keeps its normal weight; only the right-side
|
||||
# result (``status``) is rendered in bold so it is easy to read at a
|
||||
# glance. ``status`` may already contain rich-text emitted by
|
||||
# msg_ok/msg_error/msg_warning (which add their own <b>...</b>); wrapping
|
||||
# it again in <b> is harmless for those cases.
|
||||
status = "Wait" if status is None else status
|
||||
if color is None:
|
||||
line = f"{_(msg)}:\t{status}"
|
||||
line = "{}:\t<b>{}</b>".format(_(msg), status)
|
||||
else:
|
||||
line = "<font color={}>{}:\t{}</font>".format(color, _(msg), status)
|
||||
line = "{}:\t<font color={}><b>{}</b></font>".format(
|
||||
_(msg), color, status
|
||||
)
|
||||
return self.msg_edit_row(line, row)
|
||||
|
||||
def ask_password(self, msg=None):
|
||||
|
||||
@@ -532,11 +532,15 @@ class PreviewList(MyTreeView, MessageBoxMixin):
|
||||
|
||||
will = {}
|
||||
for wid, w in self.bal_window.willitems.items():
|
||||
if (
|
||||
w.get_status("VALID")
|
||||
and w.get_status("PUSHED")
|
||||
and not w.get_status("CHECKED")
|
||||
):
|
||||
# Query the will-executor server for every valid will that HAS a
|
||||
# will-executor assigned and is not yet CHECKED. Previously only
|
||||
# transactions already marked PUSHED were checked, so a will that
|
||||
# had actually been sent in the past but whose saved status still
|
||||
# read "New" (not PUSHED) was skipped and the Check button reported
|
||||
# "nothing to do". Will.needs_server_check now also includes such
|
||||
# non-PUSHED wills, so the server can confirm the transaction is
|
||||
# present and correct the status (see set_check_willexecutor).
|
||||
if Will.needs_server_check(w):
|
||||
will[wid] = w
|
||||
if will:
|
||||
self.bal_window.check_transactions(will)
|
||||
|
||||
@@ -1227,6 +1227,13 @@ class BalWindow:
|
||||
|
||||
def update_all(self):
|
||||
try:
|
||||
# Re-sync the cached "hide invalidated/replaced" flags from the
|
||||
# persisted config before refreshing the list. The Settings dialog
|
||||
# checkboxes write the config directly (without touching the cached
|
||||
# flags), so without this the list would keep filtering with the old
|
||||
# value and the invalidated/replaced rows would not appear/disappear
|
||||
# until Electrum was restarted.
|
||||
self.bal_plugin.sync_hide_filters()
|
||||
Will.add_willtree(self.willitems)
|
||||
all_utxos = self.wallet.get_utxos()
|
||||
utxos_list = Will.utxos_strs(all_utxos)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bal",
|
||||
"fullname": "Bitcoin After Life",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
|
||||
"author": "Svatantrya",
|
||||
"licence": "MIT",
|
||||
|
||||
Reference in New Issue
Block a user