BalWindow/plugin: history persistence, will import/merge, sig tracking, local-spender fixes

This commit is contained in:
2026-08-01 17:21:36 -04:00
parent 08394f4868
commit 30a5720ceb
18 changed files with 3139 additions and 153 deletions

View File

@@ -228,6 +228,21 @@ class BalPlugin(BasePlugin):
self.PREVIEW = BalConfig(config, "bal_preview", True)
self.SAVE_TXS = BalConfig(config, "bal_save_txs", True)
# SAVE_HISTORY (history persistence): when enabled, the valid will
# transactions are saved into the wallet's LOCAL history (the History
# tab) after every check, each with a configurable label. Default ON.
self.SAVE_HISTORY = BalConfig(config, "bal_save_history", True)
# HISTORY_LABEL: label text applied to the will transactions saved into
# the wallet's local history. May contain the "{willexecutor}" token,
# which is replaced with the will-executor URL of each will item at
# save time.
self.HISTORY_LABEL = BalConfig(
config,
"bal_history_label",
"BitcoinAfterLife inheritance transaction - {willexecutor}",
)
# AUTO_SIGN (Group B / B2): when enabled, pressing "Check" will, after
# querying the will-executor servers, automatically sign the will
# transactions and broadcast them to their will-executors, without the

View File

@@ -20,6 +20,7 @@ original implementation.
import bisect
from datetime import datetime, timedelta
from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL
from electrum.transaction import PartialTxOutput
# Bitcoin consensus rule: an nLockTime value strictly below this threshold is
@@ -493,6 +494,106 @@ class Util:
return True
return False
@staticmethod
def get_available_utxos(wallet, history_label, will_locktime=None):
"""Return the wallet's UTXOs as seen by the plugin's flows.
``wallet.get_utxos()`` drops any output that a wallet-LOCAL transaction
marks as spent. The plugin itself creates such local spenders when it
saves an incomplete will transaction into the local history; a *later*
will transaction stored there (a replacement/future will with a locktime
strictly after ``will_locktime``) must not hide the coins from the will
being checked or rebuilt. This view therefore restores those coins.
A local spender is ignored (the coin is kept available) only when ALL of
these hold:
* it is a wallet-local or future transaction (not broadcast),
* its wallet label matches the BAL history label template (after the
"{willexecutor}" substitution),
* the stored spender's locktime is strictly LATER than ``will_locktime``.
Real (broadcast/confirmed) spenders are never ignored. With a falsy
``will_locktime`` this returns ``wallet.get_utxos()`` unchanged.
Args:
wallet: The Electrum wallet object.
history_label: The BAL history label template (may contain
"{willexecutor}").
will_locktime: Reference locktime of the will being operated on.
"""
if not wallet or not will_locktime:
return list(wallet.get_utxos()) if wallet else []
adb = getattr(wallet, "adb", None)
if adb is None or not hasattr(adb, "get_addr_outputs"):
return list(wallet.get_utxos())
addresses = (
wallet.get_addresses() if hasattr(wallet, "get_addresses") else []
)
utxos = []
for addr in addresses:
try:
outputs = adb.get_addr_outputs(addr)
except Exception:
continue
for utxo in outputs.values():
if utxo.spent_height is None:
utxos.append(utxo)
continue
spender = getattr(utxo, "spent_txid", None)
if spender and Util._is_ignorable_local_spender(
wallet, spender, history_label, will_locktime
):
utxos.append(utxo)
return utxos
@staticmethod
def _is_ignorable_local_spender(wallet, spender, history_label, will_locktime):
"""True when the local ``spender`` tx is a later BAL history will tx.
See ``get_available_utxos`` for the exact conditions. Defensive: any
lookup failure makes this return False, so a spender is never ignored
on uncertain data.
"""
adb = wallet.adb
try:
height = int(adb.get_tx_height(spender).height())
except Exception:
return False
if height not in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE):
return False
try:
label = wallet.get_label_for_txid(spender)
except Exception:
label = None
if not label or not Util._label_matches_history(label, history_label):
return False
try:
stored = adb.db.get_transaction(spender)
except Exception:
return False
if stored is None:
return False
try:
return int(stored.locktime) > int(will_locktime)
except Exception:
return False
@staticmethod
def _label_matches_history(label, history_label):
"""True when ``label`` is the ``history_label`` template with the
"{willexecutor}" token substituted by some (possibly empty) executor URL.
"""
token = "{willexecutor}"
if token in history_label:
prefix, suffix = history_label.split(token, 1)
return (
label.startswith(prefix)
and label.endswith(suffix)
and len(label) >= len(prefix) + len(suffix)
)
return label == history_label
@staticmethod
def cmp_output(outputa, outputb):
"""Two outputs are equal when both address and value match."""

View File

@@ -40,6 +40,7 @@ from electrum.transaction import (
tx_from_any,
)
from electrum.util import (
UnrelatedTransactionException,
bfh,
)
@@ -451,11 +452,15 @@ class Will:
return out
@staticmethod
def invalidate_will(will, wallet, fees_per_byte):
def invalidate_will(will, wallet, fees_per_byte, history_label=None,
will_locktime=None):
print("invalidate tx in will module")
will_only_valid = Will.only_valid_list(will)
inputs = Will.get_all_inputs(will_only_valid)
utxos = wallet.get_utxos()
if history_label is not None and will_locktime is not None:
utxos = Util.get_available_utxos(wallet, history_label, will_locktime)
else:
utxos = wallet.get_utxos()
filtered_inputs = []
prevout_to_spend = []
current_height = Util.get_current_height(wallet.network)
@@ -533,10 +538,26 @@ class Will:
wi.set_status("INVALIDATED", True)
else:
if wallet.db.get_transaction(wi._id):
wi.set_status("CONFIRMED", True)
else:
# The funding outpoint is not part of the will tree:
# decide from whether a broadcast transaction really
# spends it (a wallet-local history copy of the same
# will tx must neither turn the item CONFIRMED nor
# INVALIDATED - it is just a persistence artifact).
stored = None
if wallet and getattr(wallet, "db", None):
try:
stored = wallet.db.get_transaction(wi._id)
except Exception:
stored = None
spender_height = Will._funding_spender_height(wallet, inp)
if spender_height is None:
if stored:
continue
wi.set_status("INVALIDATED", True)
elif spender_height == 0:
wi.set_status("MEMPOOL", True)
else:
wi.set_status("CONFIRMED", True)
for child in wi.search(all_inputs):
if child.tx.locktime < wi.tx.locktime:
@@ -574,14 +595,22 @@ class Will:
for inp in w.tx.inputs():
inp_str = Util.utxo_to_str(inp)
if inp_str not in utxos_list:
if wallet:
height = Will.check_tx_height(w.tx, wallet)
if height < 0:
if not wallet or not getattr(wallet, "adb", None):
continue
height = Will.check_tx_height(w.tx, wallet)
if height < 0:
# The will tx itself is not on-chain. A missing
# funding UTXO is only a real problem when a
# broadcast transaction actually spends it; a
# wallet-local (history) copy of the same will tx
# marks the funding spent locally and must not
# invalidate the will.
if Will._funding_really_spent(wallet, inp_str):
Will.set_invalidate(wid, willtree)
elif height == 0:
w.set_status("MEMPOOL", True)
else:
w.set_status("CONFIRMED", True)
elif height == 0:
w.set_status("MEMPOOL", True)
else:
w.set_status("CONFIRMED", True)
# def reflect_to_children(treeitem):
# if not treeitem.get_status("VALID"):
@@ -623,6 +652,83 @@ class Will:
f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}"
)
@staticmethod
def _funding_really_spent(wallet, inp_str):
"""True when a broadcast transaction really spends the ``txid:n`` outpoint.
``wallet.adb.get_spender`` discards wallet-local spenders (the stored
will tx from the local history) and future transactions, so this is True
only when the funding was consumed by a real on-chain/mempool tx.
"""
if not wallet or not getattr(wallet, "adb", None):
return False
try:
return wallet.adb.get_spender(inp_str) is not None
except Exception as e:
_logger.error(f"get_spender failed for {inp_str}: {e}")
return False
@staticmethod
def _funding_spender_height(wallet, inp_str):
"""Mined height of the broadcast tx spending ``inp_str``, or None.
Returns ``None`` when no broadcast transaction spends the outpoint (a
wallet-local history spender or a future tx are ignored by
``adb.get_spender``). The height is 0 for a mempool spender and positive
for a confirmed one.
"""
if not wallet or not getattr(wallet, "adb", None):
return None
try:
spender = wallet.adb.get_spender(inp_str)
except Exception as e:
_logger.error(f"get_spender failed for {inp_str}: {e}")
return None
if spender is None:
return None
try:
return int(wallet.adb.get_tx_height(spender).height())
except Exception as e:
_logger.error(f"get_tx_height failed for {spender}: {e}")
return 0
@staticmethod
def _absorb_history_signatures(will, wallet):
"""Merge signatures from the wallet's stored local copy of each will tx.
An incomplete will transaction saved into the local history (see
``save_valid_transactions_to_history``) may later accumulate signatures
(e.g. after a manual merge from a more complete copy). The in-memory
will item would otherwise miss those signatures on the next check. For
every item whose stored wallet copy is the same partial transaction the
signatures are merged into the in-memory one and, if it becomes fully
signed, the item is marked COMPLETE.
This method must never raise: history absorption is a convenience on top
of the will check, so any failure is logged and ignored.
"""
if not wallet or not getattr(wallet, "db", None):
return
for wi in will.values():
try:
if (
wi.tx is None
or not isinstance(wi.tx, PartialTransaction)
or wi.tx.is_complete()
):
continue
stored = wallet.db.get_transaction(wi._id)
if (
not isinstance(stored, Transaction)
or stored.txid() != wi.tx.txid()
):
continue
wi.tx.combine_with_other_psbt(stored)
if wi.tx.is_complete():
wi.set_status("COMPLETE", True)
except Exception as e:
_logger.error(f"absorb history signatures failed for item {wi._id}: {e}")
@staticmethod
def check_will(will, all_utxos, wallet, timestamp_to_check):
"""Validate a will against the current wallet state.
@@ -638,6 +744,7 @@ class Will:
timestamp_to_check: The reference UNIX timestamp (usually "now")
used to decide whether any transaction has expired.
"""
Will._absorb_history_signatures(will, wallet)
Will.add_willtree(will)
utxos_list = Will.utxos_strs(all_utxos)
@@ -651,6 +758,186 @@ class Will:
Will.search_rai(all_inputs, all_utxos, will, wallet)
Will.check_signatures(will, wallet)
@staticmethod
def save_valid_transactions_to_history(will, wallet, history_label):
"""Keep the wallet's LOCAL history in sync with the current will state.
Called after the will has been built/signed/checked (see the
SAVE_HISTORY / HISTORY_LABEL settings). A will transaction belongs in the
local history while it is still "New" (not yet fully signed - i.e. an
incomplete partial transaction), and must be removed once it becomes
"Complete" (fully signed), because at that point it is ready to be
broadcast and will appear in the history on its own.
For every will item that is valid and whose transaction has a txid it:
1. decodes the label template, replacing "{willexecutor}" with the
will-executor URL of the item,
2. if the transaction is NOT complete, stores it via
``wallet.adb.add_transaction`` (merging signatures when an
already-stored partial transaction is upgraded by a more complete
one) and tags it with the decoded label,
3. if the transaction IS complete, does not store it: its matching
local-history entry is removed by the cleanup below.
Finally it deletes every wallet-local transaction whose label exactly
matches the decoded label of a current valid item but that is no longer
among the just-saved transactions, so fully-signed, rebuilt or replaced
wills do not pile up stale entries.
This method must never raise: history persistence is a convenience on
top of the will check, so any failure is logged and ignored.
Args:
will: The will dictionary (WillItem entries keyed by txid).
wallet: The Electrum wallet object (may be falsy for offline
checks, in which case this is a no-op).
history_label: The label template to apply (may contain
"{willexecutor}").
"""
if not wallet or not getattr(wallet, "adb", None):
return
saved_txids = []
try:
current_labels = {
history_label.replace(
"{willexecutor}", (wi.we or {}).get("url", "")
)
for wi in will.values()
if wi.get_status("VALID")
and wi.tx is not None
and wi.tx.txid() is not None
}
for wi in will.values():
if not wi.get_status("VALID"):
continue
if wi.tx is None or wi.tx.txid() is None:
continue
# Fully-signed (complete) transactions must NOT be saved: they
# are removed from the local history so the list does not show a
# placeholder for a transaction that will appear on its own once
# broadcast/confirmed. Only the not-yet-complete "New" items are
# stored. Note that fully-segwit partial txs have a txid even
# when incomplete, so the txid() check alone is not enough.
if wi.tx.is_complete():
continue
try:
txid = wi.tx.txid()
label = history_label.replace(
"{willexecutor}", (wi.we or {}).get("url", "")
)
Will._add_transaction_to_history(wallet, wi.tx, txid)
try:
wallet.set_label(txid, label)
except Exception as e:
_logger.error(f"set_label failed for {txid}: {e}")
saved_txids.append(txid)
except Exception as e:
_logger.error(f"save to history failed for item {wi._id}: {e}")
# Delete stale wallet-local txs whose label matches a current valid
# item but that are no longer among the saved ones. This removes
# entries for fully-signed (complete) items and for rebuilt/replaced
# wills with the same executor.
for txid, label in Will._wallet_labels(wallet):
if txid in saved_txids:
continue
if label not in current_labels:
continue
try:
wallet.adb.remove_transaction(txid)
try:
wallet.set_label(txid, None)
except Exception:
pass
except Exception as e:
_logger.error(f"remove from history failed for {txid}: {e}")
try:
wallet.save_db()
except Exception as e:
_logger.error(f"save_db failed after history update: {e}")
except Exception as e:
_logger.error(f"save_valid_transactions_to_history failed: {e}")
@staticmethod
def _add_transaction_to_history(wallet, tx, txid):
"""Store *tx* into the wallet's local history via ``adb``.
If a partial transaction with the same txid is already stored and *tx*
carries additional signatures, the signatures are merged into the stored
one before saving. ``allow_unrelated`` is retried as a fallback so that
self-created txs (which are not yet part of the wallet's UTXO set) are
still accepted.
"""
adb = wallet.adb
existing = None
try:
existing = wallet.db.get_transaction(txid)
except Exception:
existing = None
try:
if (
isinstance(existing, PartialTransaction)
and not existing.is_complete()
and isinstance(tx, PartialTransaction)
):
existing.combine_with_other_psbt(tx)
adb.add_transaction(existing)
else:
try:
adb.add_transaction(tx)
except UnrelatedTransactionException:
adb.add_transaction(tx, allow_unrelated=True)
except Exception as e:
raise RuntimeError(f"add_transaction failed for {txid}: {e}") from e
@staticmethod
def _wallet_labels(wallet):
"""Return the wallet's ``(txid, label)`` pairs in a defensive way."""
try:
get_all_labels = wallet.get_all_labels
except AttributeError:
return []
try:
return list(get_all_labels().items())
except Exception as e:
_logger.error(f"get_all_labels failed: {e}")
return []
@staticmethod
def check_signatures(will, wallet=None):
"""Refresh the per-item signature counts and the PARTIALLY_SIGNED status.
The signature counts are derived from the transaction itself via
Electrum's ``signature_count()``, which needs a script descriptor on
each input (attached from the wallet when available). Items that already
carry their own descriptors (e.g. imported/merged partial transactions)
are counted even without a wallet.
An item with at least one signature present but fewer than required is
marked PARTIALLY_SIGNED. Items that are already signed (COMPLETE) or
whose transaction is complete always clear the flag.
"""
for wi in will.values():
try:
if wi.get_status("COMPLETE") or wi.tx is None or wi.tx.is_complete():
wi.set_status("PARTIALLY_SIGNED", False)
continue
if wallet:
wi.tx.add_info_from_wallet(wallet)
if not hasattr(wi.tx, "signature_count"):
continue
have, required = wi.tx.signature_count()
wi.sigs_have = int(have)
wi.sigs_required = int(required)
if required > 1 and 0 < have < required:
wi.set_status("PARTIALLY_SIGNED", True)
else:
wi.set_status("PARTIALLY_SIGNED", False)
except Exception as e:
_logger.error(f"check_signatures failed for item {wi._id}: {e}")
@staticmethod
def get_min_locktime(will,default_value=None):
return min((v.tx.locktime for v in will.values() if v.get_status('VALID')), default=default_value)
@@ -976,6 +1263,7 @@ class WillItem(Logger):
"MEMPOOL": ["Mempool", False],
"PUSH_FAIL": ["Push failed", False],
"PUSHED": ["Pushed", False],
"PARTIALLY_SIGNED": ["Partially Signed", False],
"REPLACED": ["Replaced", False],
"RESTORED": ["Restored", False],
"UPDATED": ["Updated", False],
@@ -1034,6 +1322,9 @@ class WillItem(Logger):
self.STATUS["PUSHED"][1] = True
self.STATUS["PUSH_FAIL"][1] = False
if status in ["COMPLETE"]:
self.STATUS["PARTIALLY_SIGNED"][1] = False
return value
def get_status(self, status):
@@ -1054,6 +1345,8 @@ class WillItem(Logger):
self.time = w.get("time", None)
self.change = w.get("change", None)
self.tx_fees = w.get("baltx_fees", 0)
self.sigs_required = int(w.get("sigs_required", 0))
self.sigs_have = int(w.get("sigs_have", 0))
self.father = w.get("Father", None)
self.children = w.get("Children", None)
self.STATUS = copy.deepcopy(WillItem.STATUS_DEFAULT)
@@ -1090,6 +1383,8 @@ class WillItem(Logger):
"time": self.time,
"change": self.change,
"baltx_fees": self.tx_fees,
"sigs_required": self.sigs_required,
"sigs_have": self.sigs_have,
}
for key in self.STATUS:
try:

View File

@@ -59,6 +59,7 @@ from electrum.transaction import SerializationError, Transaction, tx_from_any
from electrum.util import (
DECIMAL_POINT,
FileExportFailed,
FileImportFailed,
UserCancelled,
decimal_point_to_base_unit_name,
read_json_file,
@@ -137,7 +138,12 @@ from ...core.willexecutors import ( # noqa: F401
)
# --- Presentation helpers ---
from .theme import server_status_text, server_status_tooltip, status_color
from .theme import (
server_status_text,
server_status_tooltip,
signature_suffix,
status_color,
)
from .window_utils import (
bring_to_front,
show_modal,

View File

@@ -625,7 +625,12 @@ class BalBuildWillDialog(BalDialog):
except CheckAliveError as cae:
fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
tx = Will.invalidate_will(
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte,
history_label=self.bal_window.bal_plugin.HISTORY_LABEL.get(),
will_locktime=Will.get_min_locktime(
self.bal_window.willitems,
default_value=self.bal_window.date_to_check,
),
)
if tx:
_logger.debug(
@@ -646,7 +651,14 @@ class BalBuildWillDialog(BalDialog):
Will.check_amounts(
self.bal_window.heirs,
self.bal_window.willexecutors,
self.bal_window.window.wallet.get_utxos(),
Util.get_available_utxos(
self.bal_window.window.wallet,
self.bal_window.bal_plugin.HISTORY_LABEL.get(),
Will.get_min_locktime(
self.bal_window.willitems,
default_value=self.bal_window.date_to_check,
),
),
self.bal_window.date_to_check,
self.bal_window.window.wallet.dust_threshold(),
max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
@@ -700,7 +712,12 @@ class BalBuildWillDialog(BalDialog):
self.msg_set_checking(_("Postponed: invalidating old will"))
fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
return None, Will.invalidate_will(
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte,
history_label=self.bal_window.bal_plugin.HISTORY_LABEL.get(),
will_locktime=Will.get_min_locktime(
self.bal_window.willitems,
default_value=self.bal_window.date_to_check,
),
)
except NoHeirsException:
_logger.debug("no heirs")
@@ -1478,6 +1495,13 @@ class BalBuildWillDialog(BalDialog):
def on_success_phase2(self, arg=False):
self.thread.stop()
self.bal_window.save_willitems()
# After the whole check/sign/broadcast cycle, keep the wallet's local
# history in sync with the current will state (save the still "New"
# incomplete txs, remove the now-complete ones).
try:
self.bal_window._save_will_to_history()
except Exception as e:
_logger.error(f"save_will_to_history after phase2 failed: {e}")
self.msg_edit_row(_("Finished"))
# Instead of auto-closing after a countdown, let the user decide when to
# dismiss the dialog: they can read the full "Building Will" report at
@@ -1970,10 +1994,18 @@ class BalBuildWillDialog(BalDialog):
class WillDetailDialog(BalDialog):
def __init__(self, bal_window):
self.will = bal_window.willitems
self.threshold = bal_window.will_settings["real_threshold"]
def __init__(self, bal_window, will=None, threshold=None):
# ``will``/``threshold`` are passed when showing an IMPORTED (read-only)
# will. In that case every action button below operates on the imported
# will, never on the live wallet state.
self._external_will = will is not None
self.will = will if self._external_will else bal_window.willitems
if threshold is not None:
self.threshold = threshold
elif self._external_will:
self.threshold = max(wi.tx.locktime for wi in self.will.values())
else:
self.threshold = bal_window.will_settings["real_threshold"]
self.bal_window = bal_window
Will.add_willtree(self.will)
@@ -2005,8 +2037,14 @@ class WillDetailDialog(BalDialog):
b.clicked.connect(self.export_will)
hlayout.addWidget(b)
b = QPushButton(_("Invalidate"))
b.clicked.connect(bal_window.invalidate_will)
b.clicked.connect(self.invalidate_will)
hlayout.addWidget(b)
self.merge_button = None
if self._external_will:
b = QPushButton(_("Merge"))
b.clicked.connect(self.merge_will)
hlayout.addWidget(b)
self.merge_button = b
self.vlayout.addWidget(w)
self.paint_scroll_area()
@@ -2027,22 +2065,48 @@ class WillDetailDialog(BalDialog):
self.scrollbox = QScrollArea()
viewport = QWidget(self.scrollbox)
self.willlayout = QVBoxLayout(viewport)
self.detailsWidget = WillWidget(parent=self)
self.detailsWidget = WillWidget(parent=self, will=self.will)
self.willlayout.addWidget(self.detailsWidget)
self.scrollbox.setWidget(viewport)
viewport.setLayout(self.willlayout)
def ask_password_and_sign_transactions(self):
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
self.bal_window.ask_password_and_sign_transactions(
callback=self.update, will=self.will if self._external_will else None
)
self.update()
def broadcast_transactions(self):
self.bal_window.broadcast_transactions()
self.bal_window.broadcast_transactions(
will=self.will if self._external_will else None
)
self.update()
def export_will(self):
self.bal_window.export_will()
self.bal_window.export_will(will=self.will if self._external_will else None)
def invalidate_will(self):
self.bal_window.invalidate_will(
will=self.will if self._external_will else None
)
def merge_will(self):
"""Merge the imported will into the live will and switch to it.
The merge is performed by :meth:`BalWindow.merge_will` (the same
common method used by the tools-menu "Merge" action). Afterwards the
dialog stops showing the read-only imported will and operates on the
live wallet willitems directly, so any further Sign/Broadcast/Export/
Invalidate action targets the saved will items.
"""
self.bal_window.merge_will(self.will)
self._external_will = False
self.will = self.bal_window.willitems
self.threshold = self.bal_window.will_settings["real_threshold"]
if self.merge_button:
self.merge_button.hide()
self.update()
def toggle_replaced(self):
self.bal_window.bal_plugin.hide_replaced()
@@ -2061,7 +2125,8 @@ class WillDetailDialog(BalDialog):
self.update()
def update(self):
self.will = self.bal_window.willitems
if not self._external_will:
self.will = self.bal_window.willitems
pos = self.vlayout.indexOf(self.scrollbox)
self.vlayout.removeWidget(self.scrollbox)
self.paint_scroll_area()

View File

@@ -321,13 +321,6 @@ class PreviewList(MyTreeView, MessageBoxMixin):
_("check ").format(column_title),
lambda: self.check_transactions(selected_keys),
)
if self.bal_window.bal_plugin.ENABLE_MULTIVERSE.get():
try:
self.importaction = self.menu.addAction(
_("Import"), self.import_will
)
except Exception:
pass
menu.addSeparator()
menu.addAction(
@@ -396,8 +389,8 @@ class PreviewList(MyTreeView, MessageBoxMixin):
if bal_tx.we:
we = bal_tx.we["url"]
labels[self.Columns.WILLEXECUTOR] = we
status = bal_tx.status
if len(bal_tx.status) > 53:
status = bal_tx.status + signature_suffix(bal_tx)
if len(status) > 53:
status = "...{}".format(status[-50:])
labels[self.Columns.STATUS] = status
# Dedicated, always-readable label describing whether the inheritance
@@ -474,8 +467,8 @@ class PreviewList(MyTreeView, MessageBoxMixin):
menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)
menu.addAction(_("Sign"), self.ask_password_and_sign_transactions)
menu.addAction(_("Export"), self.export_will)
if self.bal_window.bal_plugin.ENABLE_MULTIVERSE.get():
self.importaction = menu.addAction(_("Import"), self.import_will)
menu.addAction(_("Import"), self.import_will_into_details)
menu.addAction(_("Merge"), self.merge_will)
menu.addAction(_("Broadcast"), self.broadcast)
menu.addAction(_("Check"), self.check)
menu.addAction(_("Invalidate"), self.invalidate_will)
@@ -547,8 +540,11 @@ class PreviewList(MyTreeView, MessageBoxMixin):
self.bal_window.export_will()
self.update()
def import_will(self):
self.bal_window.import_will()
def import_will_into_details(self):
self.bal_window.import_will_into_details()
def merge_will(self):
self.bal_window.merge_will_ui()
def ask_password_and_sign_transactions(self):
self.bal_window.ask_password_and_sign_transactions(callback=self.update)

View File

@@ -510,8 +510,10 @@ class Plugin(BalPlugin):
lbl_event_description, edit_event_description, help_event_description,
lbl_calendar_app, edit_calendar_app, help_calendar_app,
lbl_auto_sign, heir_auto_sign, help_auto_sign,
lbl_save_history, heir_save_history, help_save_history,
lbl_history_label, edit_history_label, help_history_label,
reset_btn_6, reset_btn_7, reset_btn_8, reset_btn_9, reset_btn_10,
reset_btn_auto_sign):
reset_btn_11, reset_btn_12, reset_btn_auto_sign):
w.setVisible(not basic)
# Opzione 2: apply the per-mode Raw/Date editor default ONLY here, on
# a real USER TYPE change (not inside update_all/CHECK), so pressing
@@ -537,6 +539,20 @@ class Plugin(BalPlugin):
edit_calendar_app = BalLineEdit(self.CALENDAR_APP)
edit_calendar_app.setMinimumWidth(360)
# "Save inheritance transactions in wallet history" checkbox + label
# field (History persistence). When the checkbox is ON, the valid will
# transactions are saved into the wallet's LOCAL history (the History
# tab) after each check, each tagged with the label below. The label
# field is disabled while the checkbox is off, so the user cannot set a
# label for a feature that is not active.
def on_save_history_change():
edit_history_label.setEnabled(bool(self.SAVE_HISTORY.get()))
heir_save_history = BalCheckBox(self.SAVE_HISTORY, on_click=on_save_history_change)
edit_history_label = BalLineEdit(self.HISTORY_LABEL)
edit_history_label.setMinimumWidth(360)
edit_history_label.setEnabled(bool(self.SAVE_HISTORY.get()))
def _make_reset_btn(cfg, widget, kind):
"""Return a small ``↺`` button that resets a single setting."""
btn = QPushButton("\u21ba")
@@ -747,6 +763,37 @@ class Plugin(BalPlugin):
reset_btn_10 = _make_reset_btn(self.CALENDAR_APP, edit_calendar_app, "line")
grid.addWidget(_hide_if_basic(reset_btn_10), 11, 3)
# Save-in-history toggle and history label: advanced-only rows. The
# label field is disabled while the checkbox is off (see
# on_save_history_change above).
lbl_save_history = QLabel(_("Save inheritance transactions in history"))
help_save_history = HelpButton(
"After each check, save the valid will transactions into the "
"wallet's local history (the History tab), each with a label.\n"
"The label may contain the variable:\n"
" {willexecutor}: replaced with the will-executor URL of the item\n"
"Only used in ADVANCED mode."
)
grid.addWidget(_hide_if_basic(lbl_save_history), 12, 0)
grid.addWidget(_hide_if_basic(heir_save_history), 12, 1)
grid.addWidget(_hide_if_basic(help_save_history), 12, 2)
reset_btn_11 = _make_reset_btn(self.SAVE_HISTORY, heir_save_history, "check")
grid.addWidget(_hide_if_basic(reset_btn_11), 12, 3)
lbl_history_label = QLabel(_("History label"))
help_history_label = HelpButton(
"Label applied to the will transactions saved into the wallet's "
"local history.\n"
"Variables:\n"
" {willexecutor}: replaced with the will-executor URL of the item\n"
"Only used in ADVANCED mode."
)
grid.addWidget(_hide_if_basic(lbl_history_label), 13, 0)
grid.addWidget(_hide_if_basic(edit_history_label), 13, 1)
grid.addWidget(_hide_if_basic(help_history_label), 13, 2)
reset_btn_12 = _make_reset_btn(self.HISTORY_LABEL, edit_history_label, "line")
grid.addWidget(_hide_if_basic(reset_btn_12), 13, 3)
# NOTE: the ADVANCED-only widgets above have ALREADY been given their
# correct initial visibility inline (via _hide_if_basic) BEFORE being
# added to the grid. The old code did the opposite - it added them
@@ -754,12 +801,12 @@ class Plugin(BalPlugin):
# the Windows relayout flicker. Do NOT reintroduce a post-hoc
# setVisible() loop here.
grid.addWidget(heir_repush, 12, 0)
grid.addWidget(heir_repush, 14, 0)
grid.addWidget(
HelpButton(
"Broadcast all transactions to willexecutors including those already pushed"
),
12,
14,
2,
)
@@ -793,6 +840,8 @@ class Plugin(BalPlugin):
(self.EVENT_DESCRIPTION, edit_event_description, "text"),
(self.WELIST_SERVER, edit_welist_server, "line"),
(self.CALENDAR_APP, edit_calendar_app, "line"),
(self.SAVE_HISTORY, heir_save_history, "check"),
(self.HISTORY_LABEL, edit_history_label, "line"),
]
for cfg, widget, kind in resets:
# Persist the default value back into the Electrum config.
@@ -813,6 +862,10 @@ class Plugin(BalPlugin):
widget.setCurrentIndex(
1 if str(cfg.default).lower() == "advanced" else 0
)
# Re-sync the history-label field's enabled state after a reset: the
# reset restores SAVE_HISTORY to its default, so the field must
# follow the (default) checkbox state again.
edit_history_label.setEnabled(bool(self.SAVE_HISTORY.get()))
# Refresh the open BAL windows so any dependent view (e.g. the
# editable-dates state is not in this list, but hide filters are)
# reflects the reset values.

View File

@@ -54,12 +54,31 @@ def status_color(will_item) -> str:
return "#e83845" # red - failed to push to will-executor
elif will_item.get_status("PUSHED"):
return "#73f3c8" # teal - pushed to will-executor
elif will_item.get_status("PARTIALLY_SIGNED"):
return "#ffb347" # amber - some signatures present, more needed
elif will_item.get_status("COMPLETE"):
return "#2bc8ed" # blue - signed
else:
return _DEFAULT_COLOR
def signature_suffix(will_item) -> str:
"""Return the ``" (added/required)"`` suffix for a non-signed will item.
Used by the transaction list and the detail view to show how many of the
required signatures have already been added, e.g. ``"(1/2)"`` for a 2-of-3
transaction carrying one signature. Returns ``""`` for signed transactions
or when the required count is unknown (no descriptor available yet).
"""
if will_item.get_status("COMPLETE"):
return ""
required = int(getattr(will_item, "sigs_required", 0) or 0)
added = int(getattr(will_item, "sigs_have", 0) or 0)
if not required:
return ""
return " ({}/{})".format(added, required)
def server_status_text(will_item) -> str:
"""Return a short, human-readable label describing the state of a will
item on the will-executor servers (the online inheritance backup).

View File

@@ -1482,11 +1482,11 @@ class BalSpinBox(QSpinBox):
class WillWidget(QWidget):
def __init__(self, father=None, parent=None):
def __init__(self, father=None, parent=None, will=None):
super().__init__()
vlayout = QVBoxLayout()
self.setLayout(vlayout)
self.will = parent.bal_window.willitems
self.will = will if will is not None else parent.bal_window.willitems
self._bal_parent = parent
for w in self.will:
if (
@@ -1513,7 +1513,10 @@ class WillWidget(QWidget):
willpushbutton = QPushButton(w)
willpushbutton.clicked.connect(
partial(self._bal_parent.bal_window.show_transaction, txid=w)
partial(
self._bal_parent.bal_window.show_transaction,
tx=self.will[w].tx,
)
)
detaillayout.addWidget(willpushbutton)
locktime = str(BalTimestamp(self.will[w].tx.locktime))
@@ -1535,7 +1538,9 @@ class WillWidget(QWidget):
fee_per_byte = round(total_fees / self.will[w].tx.estimated_size(), 3)
fees_str = str(decoded_fees) + " (" + str(fee_per_byte) + " sats/vbyte)"
detaillayout.addWidget(qlabel("Transaction fees:", fees_str))
detaillayout.addWidget(qlabel("Status:", self.will[w].status))
detaillayout.addWidget(
qlabel("Status:", self.will[w].status + signature_suffix(self.will[w]))
)
detaillayout.addWidget(QLabel(""))
detaillayout.addWidget(QLabel("<b>Heirs:</b>"))
for heir in self.will[w].heirs:
@@ -1570,6 +1575,6 @@ class WillWidget(QWidget):
detailw.setPalette(pal)
hlayout.addWidget(detailw)
hlayout.addWidget(WillWidget(w, parent=parent))
hlayout.addWidget(WillWidget(w, parent=parent, will=self.will))

View File

@@ -355,6 +355,12 @@ class BalWindow:
will = self.build_inheritance_transaction(
ignore_duplicate=ignore_duplicate, keep_original=keep_original
)
# Persist the freshly prepared transactions into the wallet's local
# history (when SAVE_HISTORY is enabled). This runs on every successful
# prepare -- including the "Prepare" menu action -- so the New txs show
# up in History immediately. Abort paths return None and are skipped.
if will:
self._save_will_to_history()
return will
def delete_not_valid(self, txid, s_utxo):
@@ -393,12 +399,20 @@ class BalWindow:
# date_to_check already carries the correct reference timestamp for
# the current mode (the Check Alive in ADVANCED, or "now" in BASIC -
# see init_class_variables). So build the will directly against it;
# no per-mode branch is needed here anymore.
# no per-mode branch is needed here anymore. The available-UTXO view
# restores coins that a newer, wallet-local will tx (stored in the
# history with a later locktime) nominally spent.
txs = self.heirs.get_transactions(
self.bal_plugin,
self.window.wallet,
self.will_settings["baltx_fees"],
None,
Util.get_available_utxos(
self.window.wallet,
self.bal_plugin.HISTORY_LABEL.get(),
Will.get_min_locktime(
self.willitems, default_value=self.date_to_check
),
),
self.date_to_check,
)
@@ -434,17 +448,75 @@ class BalWindow:
return self.willitems
def check_will(self):
return Will.is_will_valid(
result = Will.is_will_valid(
self.willitems,
self.date_to_check,
self.will_settings["baltx_fees"],
self.window.wallet.get_utxos(),
Util.get_available_utxos(
self.window.wallet,
self.bal_plugin.HISTORY_LABEL.get(),
Will.get_min_locktime(
self.willitems, default_value=self.date_to_check
),
),
heirs=self.heirs,
willexecutors=self.willexecutors,
self_willexecutor=self.no_willexecutor,
wallet=self.wallet,
callback_not_valid_tx=self.delete_not_valid,
)
return result
def _save_will_to_history(self):
"""Persist the current will state into the wallet's LOCAL history.
Runs after the will has been prepared/built/signed/checked (the
"Prepare" action, the check dialog's phase 2 and the manual Sign
action). When the SAVE_HISTORY setting is enabled,
``Will.save_valid_transactions_to_history`` stores the still "New" (not
fully-signed) transactions under the configured label and removes
entries for fully-signed ("Complete") and stale ones. The wallet tabs
are then re-rendered through ``_refresh_after_history_save``.
This must never raise: history persistence is a convenience on top of
the will flows, so any failure is logged and ignored.
"""
try:
if not bool(self.bal_plugin.SAVE_HISTORY.get()):
return
Will.save_valid_transactions_to_history(
self.willitems,
self.wallet,
self.bal_plugin.HISTORY_LABEL.get(),
)
except Exception as e:
_logger.error(f"save_will_to_history failed: {e}")
self._schedule_history_refresh()
def _schedule_history_refresh(self):
"""Re-render the wallet tabs after the local history has changed.
The actual refresh must run on the GUI thread (``HistoryModel.refresh``
asserts that), so the call is marshalled through ``QTimer.singleShot``.
Used after saving/removing will transactions in the local history and
after a will rebuild, regardless of the calling thread.
"""
QTimer.singleShot(0, self._refresh_after_history_save)
def _refresh_after_history_save(self):
"""Re-render the wallet tabs after saving txs to the local history.
``update_tabs`` refreshes history plus the receive/send/address/coins
lists; ``update_status`` refreshes the status-bar balance, which
``update_tabs`` does not touch. When ``update_tabs`` is not available we
fall back to refreshing just the History tab.
"""
if hasattr(self.window, "update_tabs"):
self.window.update_tabs()
elif hasattr(self.window, "history_list"):
self.window.history_list.update()
if hasattr(self.window, "update_status"):
self.window.update_status()
def show_message(self, text):
self.window.show_message(text)
@@ -594,7 +666,13 @@ class BalWindow:
Will.check_amounts(
self.heirs,
self.willexecutors,
self.window.wallet.get_utxos(),
Util.get_available_utxos(
self.window.wallet,
self.bal_plugin.HISTORY_LABEL.get(),
Will.get_min_locktime(
self.willitems, default_value=self.date_to_check
),
),
self.date_to_check,
self.window.wallet.dust_threshold(),
max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
@@ -740,8 +818,7 @@ class BalWindow:
)
)
self.window.history_list.update()
self.window.utxo_list.update()
self._schedule_history_refresh()
# Guide the user: the inheritance was just (re)built and is now
# in the "New" state, so it must be SIGNED and then BROADCAST
@@ -811,7 +888,7 @@ class BalWindow:
raise Exception(_("no tx"))
return self.show_transaction_real(tx, parent=parent)
def invalidate_will(self):
def invalidate_will(self, will=None):
def on_success(result):
if result:
self.show_message(
@@ -828,16 +905,27 @@ class BalWindow:
def on_failure(exec_info):
log_error(exec_info, self.bal_window)
willitems = will if will is not None else self.willitems
fee_per_byte = self.will_settings.get("baltx_fees", 1)
task = partial(Will.invalidate_will, self.willitems, self.wallet, fee_per_byte)
task = partial(
Will.invalidate_will,
willitems,
self.wallet,
fee_per_byte,
history_label=self.bal_plugin.HISTORY_LABEL.get(),
will_locktime=Will.get_min_locktime(
willitems, default_value=self.date_to_check
),
)
msg = _("Calculating Transactions")
self.waiting_dialog = BalWaitingDialog(
self, msg, task, on_success, on_failure, exe=False
)
self.waiting_dialog.exe()
def sign_transactions(self, password):
def sign_transactions(self, password, will=None):
try:
willitems = will if will is not None else self.willitems
txs = {}
signed = None
tosign = None
@@ -848,8 +936,8 @@ class BalWindow:
msg = _(f"signed: {signed}\n")
return msg + _(f"signing: {tosign}")
for txid in Will.only_valid(self.willitems):
wi = self.willitems[txid]
for txid in Will.only_valid(willitems):
wi = willitems[txid]
tx = copy.deepcopy(wi.tx)
if wi.get_status("COMPLETE"):
txs[txid] = tx
@@ -861,8 +949,8 @@ class BalWindow:
pass
for txin in tx.inputs():
prevout = txin.prevout.to_json()
if prevout[0] in self.willitems:
change = self.willitems[prevout[0]].tx.outputs()[prevout[1]]
if prevout[0] in willitems:
change = willitems[prevout[0]].tx.outputs()[prevout[1]]
txin._trusted_value_sats = change.value
try:
txin.script_descriptor = change.script_descriptor
@@ -879,6 +967,16 @@ class BalWindow:
if tx.is_complete():
# is_complete = True
wi.set_status("COMPLETE", True)
# Refresh the per-item signature counts from the freshly signed
# partial tx: at this point the signatures are still present
# (before any finalization), so the will list can show the real
# "added/required" count (e.g. "1/2" for a multisig).
try:
have, required = tx.signature_count()
wi.sigs_have = int(have)
wi.sigs_required = int(required)
except Exception as e:
_logger.debug(f"signature_count after signing failed: {e}")
txs[txid] = tx
except Exception:
return None
@@ -945,16 +1043,29 @@ class BalWindow:
# re-wire them if this same window is reused for another wallet.
self._menubar_initialized = False
def ask_password_and_sign_transactions(self, callback=None):
def ask_password_and_sign_transactions(self, callback=None, will=None):
external = will is not None
willitems = will if external else self.willitems
def on_success(txs):
if txs:
for txid, tx in txs.items():
self.willitems[txid].tx = copy.deepcopy(tx)
self.will[txid] = self.willitems[txid].to_dict()
willitems[txid].tx = copy.deepcopy(tx)
if not external:
self.will[txid] = willitems[txid].to_dict()
try:
self.will_list_widget.update()
except Exception:
pass
Will.check_signatures(willitems, self.wallet)
except Exception as e:
_logger.error(f"check_signatures after signing failed: {e}")
if not external:
try:
self.will_list_widget.update()
except Exception:
pass
# After signing, keep the local history in sync (save the still
# incomplete "New" txs, remove the now-complete ones).
if not external:
self._save_will_to_history()
if callback:
try:
callback()
@@ -965,16 +1076,19 @@ class BalWindow:
log_error(exec_info, self.bal_window)
password = self.get_wallet_password()
task = partial(self.sign_transactions, password)
task = partial(self.sign_transactions, password, will=will)
msg = _("Signing transactions...")
self.waiting_dialog = BalWaitingDialog(
self, msg, task, on_success, on_failure, exe=False
)
self.waiting_dialog.exe()
def broadcast_transactions(self, force=False):
def broadcast_transactions(self, force=False, will=None):
external = will is not None
def on_success(sulcess):
self.will_list_widget.update()
if not external:
self.will_list_widget.update()
if sulcess:
_logger.info("error, some transaction was not sent")
self.show_warning(_("Some transaction was not broadcasted"))
@@ -999,15 +1113,16 @@ class BalWindow:
# _logger.error("lasti:", tb.tb_lasti)
# tb = tb.tb_next
task = partial(self.push_transactions_to_willexecutors, force)
task = partial(self.push_transactions_to_willexecutors, force, will=will)
msg = _("Selecting Will-Executors")
self.waiting_dialog = BalWaitingDialog(
self, msg, task, on_success, on_failure, exe=False
)
self.waiting_dialog.exe()
def push_transactions_to_willexecutors(self, force=False):
willexecutors = Willexecutors.get_willexecutor_transactions(self.willitems, force=force)
def push_transactions_to_willexecutors(self, force=False, will=None):
willitems = will if will is not None else self.willitems
willexecutors = Willexecutors.get_willexecutor_transactions(willitems, force=force)
def getMsg(willexecutors):
msg = "Broadcasting Transactions to Will-Executors:\n"
@@ -1036,11 +1151,11 @@ class BalWindow:
willexecutor["broadcast_status"] = _("checking...")
elif ok:
for wid in willexecutor.get("txsids", []):
self.willitems[wid].set_status("PUSHED", True)
willitems[wid].set_status("PUSHED", True)
willexecutor["broadcast_status"] = _("Success")
else:
for wid in willexecutor.get("txsids", []):
self.willitems[wid].set_status("PUSH_FAIL", True)
willitems[wid].set_status("PUSH_FAIL", True)
error["flag"] = True
willexecutor["broadcast_status"] = _("Failed")
willexecutor.pop("txs", None)
@@ -1065,54 +1180,171 @@ class BalWindow:
return
self.waiting_dialog.update(
"checking {} - {} : {}".format(
self.willitems[wid].we["url"], wid, "Waiting"
willitems[wid].we["url"], wid, "Waiting"
)
)
w = self.willitems[wid]
w = willitems[wid]
w.set_check_willexecutor(
Willexecutors.check_transaction(wid, w.we["url"])
)
self.waiting_dialog.update(
"checked {} - {} : {}".format(
self.willitems[wid].we["url"],
willitems[wid].we["url"],
wid,
self.willitems[wid].get_status("CHECKED"),
willitems[wid].get_status("CHECKED"),
)
)
if error["flag"]:
return True
def export_json_file(self, path):
for wid in self.willitems:
self.willitems[wid].set_status("EXPORTED", True)
self.will[wid] = self.willitems[wid].to_dict()
write_json_file(path, self.will)
def export_json_file(self, path, will=None):
if will is None:
for wid in self.willitems:
self.willitems[wid].set_status("EXPORTED", True)
self.will[wid] = self.willitems[wid].to_dict()
write_json_file(path, self.will)
else:
write_json_file(path, {wid: wi.to_dict() for wid, wi in will.items()})
def export_will(self):
def export_will(self, will=None):
try:
export_meta_gui(self.window, "will.json", self.export_json_file)
export_meta_gui(
self.window, "will.json", partial(self.export_json_file, will=will)
)
except Exception as e:
self.show_error(str(e))
raise e
def import_will(self):
def sulcess():
def merge_will(self, imported):
"""Merge imported will items into the live will.
Both the tools-menu "Merge" action and the details-dialog "Merge"
button go through this single method.
For a transaction that already exists in the live will the live
WillItem is kept (never replaced): only the operational statuses
(signed/pushed/checked/mempool/confirmed) that are True in the
imported item are carried over. When the live transaction is not
yet signed the imported transaction is merged into it (signatures
are combined when both are the same unsigned tx, otherwise the
transaction is substituted); an already-signed live transaction is
left untouched. Transactions that are new are added wholesale.
After the merge a local validity check recomputes the
valid/invalidated/replaced statuses (no server contact, no expiry
raise).
"""
for wid, wi in imported.items():
if wid in self.willitems:
live = self.willitems[wid]
was_complete = live.get_status("COMPLETE")
for status in (
"COMPLETE",
"PUSHED",
"CHECKED",
"MEMPOOL",
"CONFIRMED",
):
if wi.get_status(status):
live.set_status(status, True)
if not was_complete:
try:
if live.tx.txid() == wi.tx.txid():
live.tx.combine_with_other_psbt(wi.tx)
else:
live.tx = wi.tx
except Exception:
live.tx = wi.tx
if live.tx.is_complete():
live.set_status("COMPLETE", True)
else:
self.willitems[wid] = wi
Will.normalize_will(self.willitems, self.wallet)
self.save_willitems()
# Local validity check: recompute valid/invalidated/replaced statuses.
try:
Will.add_willtree(self.willitems)
bal_plugin = getattr(self, "bal_plugin", None)
history_label = (
bal_plugin.HISTORY_LABEL.get() if bal_plugin is not None else None
)
all_utxos = Util.get_available_utxos(
self.wallet,
history_label,
Will.get_min_locktime(
self.willitems, default_value=self.date_to_check
),
)
Will.check_invalidated(
self.willitems, Will.utxos_strs(all_utxos), self.wallet
)
Will.search_rai(
Will.get_all_inputs(self.willitems, only_valid=True),
all_utxos,
self.willitems,
self.wallet,
)
Will.check_signatures(self.willitems, self.wallet)
except Exception as e:
log_error(e, self.bal_window)
self.save_willitems()
self.update_all()
def merge_will_from_file(self, path):
try:
willitems = self._load_will_file(path)
except Exception as e:
raise FileImportFailed(_("Invalid will file: {}").format(e)) from None
Will.normalize_will(willitems, self.wallet)
self.merge_will(willitems)
def merge_will_ui(self):
def on_success():
self.will_list_widget.update_will(self.willitems)
import_meta_gui(self.window, _("will"), self.import_json_file, sulcess)
import_meta_gui(self.window, _("will"), self.merge_will_from_file, on_success)
def import_json_file(self, path):
try:
data = read_json_file(path)
willitems = {}
for k, v in data.items():
data[k]["tx"] = tx_from_any(v["tx"])
willitems[k] = WillItem(data[k], _id=k)
self.update_will(willitems)
except Exception as e:
raise e
# raise FileImportFailed(_("Invalid will file"))
def import_will_into_details(self):
"""Import a will file and show it in a WillDetails window.
Unlike the "Merge" actions (which merge the file into the active
will), this is a read-only preview: the parsed will is shown in a
:class:`WillDetailDialog` and the live wallet state is never touched.
The dialog's Sign/Broadcast/Export/Invalidate buttons operate on the
imported will only, and its Merge button merges the imported will
into the live one.
"""
imported = {}
def on_file(path):
try:
willitems = self._load_will_file(path)
except Exception as e:
self.show_error(_("Invalid will file: {}").format(e))
return
# Attach wallet/input info so the imported txs can be signed and
# broadcast (mirrors what merge_will_from_file does).
Will.normalize_will(willitems, self.wallet)
for wi in willitems.values():
wi.set_status("IMPORTED", True)
imported.update(willitems)
def on_success():
if not imported:
return
d = WillDetailDialog(self, will=imported)
show_on_top(d)
import_meta_gui(self.window, _("will"), on_file, on_success)
def _load_will_file(self, path):
data = read_json_file(path)
willitems = {}
for k, v in data.items():
data[k]["tx"] = tx_from_any(v["tx"])
willitems[k] = WillItem(data[k], _id=k)
return willitems
def check_transactions_task(self, will):
start = time.time()
@@ -1507,7 +1739,13 @@ class BalWindow:
for _wid, _w in list(self.willitems.items())[:3]:
_logger.debug(f"NoneType_debug willitems[{_wid}] type={type(_w).__name__}")
Will.add_willtree(self.willitems)
all_utxos = self.wallet.get_utxos()
all_utxos = Util.get_available_utxos(
self.wallet,
self.bal_plugin.HISTORY_LABEL.get(),
Will.get_min_locktime(
self.willitems, default_value=self.date_to_check
),
)
utxos_list = Will.utxos_strs(all_utxos)
Will.check_invalidated(self.willitems, utxos_list, self.wallet)