From 30a5720ceb0409402a6c51ebefc9d85710ae15c7 Mon Sep 17 00:00:00 2001 From: svatantrya Date: Sat, 1 Aug 2026 17:21:36 -0400 Subject: [PATCH] BalWindow/plugin: history persistence, will import/merge, sig tracking, local-spender fixes --- bal/core/plugin_base.py | 15 + bal/core/util.py | 101 ++++ bal/core/will.py | 319 ++++++++++++- bal/gui/qt/common.py | 8 +- bal/gui/qt/dialogs.py | 91 +++- bal/gui/qt/lists.py | 22 +- bal/gui/qt/plugin.py | 59 ++- bal/gui/qt/theme.py | 19 + bal/gui/qt/widgets.py | 15 +- bal/gui/qt/window.py | 346 +++++++++++--- tests/karen7 | 455 ++++++++++++++++-- tests/samanta7 | 194 +++++++- tests/test_core_will_extra.py | 621 ++++++++++++++++++++++++- tests/test_group_c_settings.py | 63 ++- tests/test_gui_prepare_will_history.py | 216 +++++++++ tests/test_gui_theme.py | 53 ++- tests/test_import_will_details.py | 559 ++++++++++++++++++++++ tests/test_settings_history_dialog.py | 136 ++++++ 18 files changed, 3139 insertions(+), 153 deletions(-) create mode 100644 tests/test_gui_prepare_will_history.py create mode 100644 tests/test_import_will_details.py create mode 100644 tests/test_settings_history_dialog.py diff --git a/bal/core/plugin_base.py b/bal/core/plugin_base.py index 809a7ec..6da82c9 100644 --- a/bal/core/plugin_base.py +++ b/bal/core/plugin_base.py @@ -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 diff --git a/bal/core/util.py b/bal/core/util.py index d8bfb69..33824d4 100644 --- a/bal/core/util.py +++ b/bal/core/util.py @@ -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.""" diff --git a/bal/core/will.py b/bal/core/will.py index 54b572c..0e0695d 100644 --- a/bal/core/will.py +++ b/bal/core/will.py @@ -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: diff --git a/bal/gui/qt/common.py b/bal/gui/qt/common.py index 4545dab..c9305ed 100644 --- a/bal/gui/qt/common.py +++ b/bal/gui/qt/common.py @@ -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, diff --git a/bal/gui/qt/dialogs.py b/bal/gui/qt/dialogs.py index 3a0578f..80fee88 100644 --- a/bal/gui/qt/dialogs.py +++ b/bal/gui/qt/dialogs.py @@ -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() diff --git a/bal/gui/qt/lists.py b/bal/gui/qt/lists.py index 940c04e..bacddd2 100644 --- a/bal/gui/qt/lists.py +++ b/bal/gui/qt/lists.py @@ -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) diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py index 1d480eb..1b233e8 100644 --- a/bal/gui/qt/plugin.py +++ b/bal/gui/qt/plugin.py @@ -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. diff --git a/bal/gui/qt/theme.py b/bal/gui/qt/theme.py index 08ea1ac..a455811 100644 --- a/bal/gui/qt/theme.py +++ b/bal/gui/qt/theme.py @@ -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). diff --git a/bal/gui/qt/widgets.py b/bal/gui/qt/widgets.py index f653103..63d97ed 100644 --- a/bal/gui/qt/widgets.py +++ b/bal/gui/qt/widgets.py @@ -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("Heirs:")) 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)) diff --git a/bal/gui/qt/window.py b/bal/gui/qt/window.py index 56c959b..7cd5571 100644 --- a/bal/gui/qt/window.py +++ b/bal/gui/qt/window.py @@ -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) diff --git a/tests/karen7 b/tests/karen7 index ad44a31..2ff4f4b 100644 --- a/tests/karen7 +++ b/tests/karen7 @@ -12,6 +12,7 @@ 734 ] ], + "bcrt1q08atkh7p3xu5cn4azclc7tcuuv7332kmjfjjlr": [], "bcrt1q08z5t4x74u2883sx2qwsmzk2hj8e5n7z83e4vy": [ [ "f084b3a6f67a553a2036f11f2d0d8e548fe8f82ff43105c6f81845cf0178ea9f", @@ -23,7 +24,12 @@ ] ], "bcrt1q0k6rq9xny2jnk9z95w9x5zv8vkkc25d8dq2pmz": [], - "bcrt1q0ma5z02ma7q0cq7j49wz66clf69xw3fq2zl05n": [], + "bcrt1q0ma5z02ma7q0cq7j49wz66clf69xw3fq2zl05n": [ + [ + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c", + 2279 + ] + ], "bcrt1q0tf3hnml0s03rsqfjzmm4tlnxu9k0969yqvhn9": [ [ "0ef3b8b6a2c3d28126a7c79aa95b5bd1f091b7eb8c14c20fd5ee89c409462c94", @@ -1059,7 +1065,16 @@ 734 ] ], - "bcrt1qemzatwg2gwvznm8u5jg4pau79x2htt8wus8usd": [], + "bcrt1qemzatwg2gwvznm8u5jg4pau79x2htt8wus8usd": [ + [ + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa", + 2250 + ], + [ + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df", + 2266 + ] + ], "bcrt1qf37fqcgrv5hhs7cw6qsge62unnzhptgjef9yuc": [], "bcrt1qf42akvcychqwv7gax2lvtt8d6snk6yk6j9dfpt": [ [ @@ -1566,6 +1581,7 @@ ], "bcrt1qmxcczs70me327dm7a3uzcj3ccp7tpsv3s025xv": [], "bcrt1qmzx9eh5lljf4edqz48fe7gktndewvpmc6h9q0y": [], + "bcrt1qn69fe8fjlwyx0eqhzxl4wmypj8re3j6sq5m9zk": [], "bcrt1qn6erz7549527kdfakz7fl460rx4yqfy8dt885j": [ [ "5b67bf78a3b367a4b05806efa537dc1ea82ae8bec788954b5e178c21ed53b385", @@ -2074,6 +2090,8 @@ 734 ] ], + "bcrt1qupdrj7vzc8a9lxvvup9dqe8qlzcjskrk5dae5k": [], + "bcrt1quykurwfx3strtkezdvvkffalncgpx85p83w9v4": [], "bcrt1quzlcysqqmamqvn2f93vdk29rr5sj30wrwf94py": [ [ "2c7b1c375b27d7c61eebbb1d9feaeefe0133af1255ec705e4053a7ec66be732a", @@ -2135,7 +2153,16 @@ 710 ] ], - "bcrt1qw8l4cqjgkrkj8dqtq8pmz343f8r0ye4aldvvgm": [], + "bcrt1qw8l4cqjgkrkj8dqtq8pmz343f8r0ye4aldvvgm": [ + [ + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df", + 2266 + ], + [ + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c", + 2279 + ] + ], "bcrt1qwq0tcpz8378adrxltflqucwhuacafxskljw7lp": [ [ "8015500aaf8017f883467cb6d8ab7d1ac0514993af4a99fd303ef5d4c6adda83", @@ -2197,7 +2224,16 @@ 710 ] ], - "bcrt1qxh5zr72vgglpksts43dad67ra6kzxcjyj6dkq2": [], + "bcrt1qxh5zr72vgglpksts43dad67ra6kzxcjyj6dkq2": [ + [ + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4", + 2248 + ], + [ + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa", + 2250 + ] + ], "bcrt1qxsvctzhc4u4fnkpll5h3tklw80y3nq2ux8j4gc": [ [ "93b5460b7ea21bc89819b080253996a1319ef70d635b1b62fc50151d4255ec94", @@ -2406,7 +2442,10 @@ "bcrt1qvhrmzx3779qmfunmppqv9r50x8h09qpunx09xv", "bcrt1quca5d5uhtucqlkkler4tnng4wcn5l96whlcqy5", "bcrt1qhur55ueke9u6fd5vc55r5yv7nkkewgq5lcqgeg", - "bcrt1q4wle4rjunlheyrpx3cgewjry84lt9vcfn6dzm7" + "bcrt1q4wle4rjunlheyrpx3cgewjry84lt9vcfn6dzm7", + "bcrt1qn69fe8fjlwyx0eqhzxl4wmypj8re3j6sq5m9zk", + "bcrt1quykurwfx3strtkezdvvkffalncgpx85p83w9v4", + "bcrt1q08atkh7p3xu5cn4azclc7tcuuv7332kmjfjjlr" ], "receiving": [ "bcrt1qpm5utekdtmzwnlkh7jq5497vwwf6sm38tljan5", @@ -2607,7 +2646,8 @@ "bcrt1q2ds58hh500vl9f2sepudvw5d7qvj30aqly6vtp", "bcrt1q5y9w84tf3xfxss6u6eueejmk7pmcssnz0g5hzp", "bcrt1qt87xmzg2w7mxnqtzqle2znpva3sfgrw4hvztyn", - "bcrt1qymwjdgm74puzgwv7wpc9zhej75jhnkwlhtskxe" + "bcrt1qymwjdgm74puzgwv7wpc9zhej75jhnkwlhtskxe", + "bcrt1qupdrj7vzc8a9lxvvup9dqe8qlzcjskrk5dae5k" ] }, "channels": {}, @@ -2625,22 +2665,22 @@ "aaaa": [ "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", "34%", - "5y" + "1y" ], "lucia": [ "bcrt1q0cv672t0yhgf9u36pssgsglxtjq69tuakdzy2j", "32%", - "5y" + "1y" ], "mario": [ "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", "35%", - "5y" + "1y" ], "mario2": [ "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", 40000, - "5y" + "1y" ] }, "imported_channel_backups": {}, @@ -2698,10 +2738,10 @@ "0342999661ce320082597a64a4f57ff54412c81c9e0d8c800500316d608c73ba": "BAL Transaction", "03962fa5e7f6ffa884a79895d689ef70e9a74b279b5ddb187d0fe6b49c86e934": "BAL Transaction", "03f79e96b8020af16c5fec1e99e83f37b436908dd8c18163ef3eff442e609b29": "BAL Transaction", - "04a14aa60b6ef0c727fae209070dcb7926389aea0ef643ceaaf788c2c6e8ea1f": "BAL Transaction", + "04a14aa60b6ef0c727fae209070dcb7926389aea0ef643ceaaf788c2c6e8ea1f": "BAL Inheritance transaction", "04ce14acc1c38c9d11f6d4b6488de7d66c0994b0b94563efd0b8bf89a96c5865": "BAL Transaction", "04edd61439362b3c67fcb1ae0e09d5fb145cbd1a8d8d574d24d5231bd65cfb45": "BAL Transaction", - "051f5bb1b0df3f8e5011e71449646a1c6595b4e41a0252afbfbe92489a6c5043": "BAL Transaction", + "051f5bb1b0df3f8e5011e71449646a1c6595b4e41a0252afbfbe92489a6c5043": "BAL Inheritance transaction", "059e0b16f98397641a719e4fff0b04967843c09bac1a3d20d355a9637a1c1983": "BAL Transaction", "05e515b695607b8b8c9f49a688274c19997fd3105d37878e404d10cba621a447": "BAL Transaction", "068aa2bfcaedf969158cb8c9af17bf7c6e0300459636cee2d4a729cd806b1218": "BAL Transaction", @@ -2724,7 +2764,7 @@ "0e4514268c434c1ab0d6e25b4d4a8d8fe5e2344701c72b21b7c30c6b5f332ab1": "BAL Transaction", "0ecc8822fc6402c6f1a07d62c2694bbd6029a6c732118634c1743da12ffb3b32": "BAL Transaction", "0f19080f3488579430dacb94e6af1ff177babefed3d2f8e15636d0f9c0e8d629": "BAL Transaction", - "0f864bd74f0a66410e4c251f0993a61837f01ca947ce1badf0bb57e4b38e6866": "BAL Transaction", + "0f864bd74f0a66410e4c251f0993a61837f01ca947ce1badf0bb57e4b38e6866": "BAL Inheritance transaction", "0fb9da0e14c71d8b85bf00d23fa3b599972ea48f94e567e0f9733ed7357ba686": "BAL Transaction", "0fbfe13e0aaeab6b4032f4f61f15b23efae98a273006f96c6a53739f4c4ee4b3": "BAL Transaction", "0fe91105e4dec1a6885f357870f8a4c12ab4ff3e1eb44bf50c1e31fd7df9dd93": "BAL Transaction", @@ -2732,7 +2772,7 @@ "11a7d226afc8d5fe63f9cc00b7edb5a8e4dd8444e08930d1bbe77e8d14171cd2": "BAL Transaction", "11b6ce930c0c09f264cf9ce9ee5ff174c1550673fc65cff3d3cffac86ef8ea8e": "BAL Transaction", "1210e8189eed90143785def33b65240deab8b2c051526a267aec313762ebe295": "BAL Transaction", - "12e8be720f97a0e449e047d1eb3b8b5700d47112737c3acb894a98e33fd3598d": "BAL Transaction", + "12e8be720f97a0e449e047d1eb3b8b5700d47112737c3acb894a98e33fd3598d": "BAL Inheritance transaction", "13998d34fcafd09ac0740f728d0381233e9ec59628785e8e3d4ce79e59232ac7": "BAL Transaction", "1403ff43f52ec3cf73892c429bb7c0c913814b01f87fff5d71f2f10f240d6dfb": "BAL Transaction", "150945af0c56e570acd35c4c003f074c775d20aafc2f6a20beaf380f5e3d5ecd": "BAL Transaction", @@ -2770,7 +2810,7 @@ "27d8bafd4f7be6a086af8ffc6ce0fb50588c035669582beed48538032bc7d50d": "BAL Transaction", "28b432660bc1232de270461ec9639c07d4438d77f0c2571cede1f92351254a9a": "BAL Transaction", "28e0ba1ecf9c349345c67317a15fb7e53193dfb1657170b32e4dd8354261bdab": "BAL Transaction", - "2967d7c89ad9c988524978a11d5fa51aa16269049c57dce4d01baba990e2d95d": "BAL Transaction", + "2967d7c89ad9c988524978a11d5fa51aa16269049c57dce4d01baba990e2d95d": "BAL Inheritance transaction", "2a4c51bd0df1e37bb015935fa206b6623dccc10c1f11b3afe1ca5513e2be0189": "BAL Transaction", "2bc2274bd279d0f763ca87fbdbf8d9886df829e198128e62ada514f936704a16": "BAL Transaction", "2c7d1cc63a5abcd7fd206da9523a9d8b27e1fc2393288bc1b727870cb2ad5e6d": "BAL Transaction", @@ -2779,17 +2819,17 @@ "2e3aac83fc742ff0ca2457902c1e1e172cbea223d1bb114ac140bc27c7d1760c": "BAL Transaction", "2e53015791cba5b2be4d32f48702310e4542f0cf7c16bb0143760f23e5d2e164": "BAL Transaction", "2e6310e54515b3f8015032f2adc1e203022303dc11015661694b4ce94eb653f0": "BAL Transaction", - "2ed95ab6bcf71b6fd9c6f81840654fb72f19ab945019f265253bf7414ec57496": "BAL Transaction", + "2ed95ab6bcf71b6fd9c6f81840654fb72f19ab945019f265253bf7414ec57496": "BAL Inheritance transaction", "2ef8ee4b5a0c39c0ea5f590551852513766612ff908bca7118a289179df1085d": "BAL Transaction", "2f56e55e487ed0efdb0129e54c8adf20d191b90893ae9e31f54ea5e97086f506": "BAL Transaction", - "30c57bb2828d8fb85ec366a1642093318fd3d4c3e8cb69f90fa15ab3d25c633d": "BAL Transaction", + "30c57bb2828d8fb85ec366a1642093318fd3d4c3e8cb69f90fa15ab3d25c633d": "BAL Inheritance transaction", "312ccfc8805e5ee460bd7e7869ff4cbd1c2b9737285d8bda7fe8d33ca8b1a363": "BAL Transaction", "3137ebf306912e0ba0a0abd277968547c28162cdd7e1826cc61b9844124708ac": "BAL Transaction", "3182535e41faa7aba960ae293d31de27f68ecddf72aa36cfa4ac0c3b7c0de3e5": "BAL Transaction", "31b7e1809f18d667eec17540f76ce6d7ee11e67822a71b34797e9e3c6cf85769": "BAL Transaction", "322f366dcecf411038fb423afb3d07609a03c1a3bb15976e68727acedc516bb9": "BAL Transaction", "32501d75a0d73b52107c109bc512ee9d1c8014409dd92514de559329c15fe869": "BAL Transaction", - "3318599c8ac525d9f9a29c298a943ae887afbf31ed92b781ceaa547e3ddd352b": "BAL Transaction", + "3318599c8ac525d9f9a29c298a943ae887afbf31ed92b781ceaa547e3ddd352b": "BAL Inheritance transaction", "335d73d9f9295b283a07187652a587830eb1eb2ee2829ef566dde8445946e474": "BAL Transaction", "33ec7e9429ba79b66cb0f229e4da8233a3a2915020bf3da9c0dae53368a560e1": "BAL Transaction", "34b6a345c07a9976aa39614ec2dd40205c9106c3dc37db352e3fcb3c85c01548": "BAL Transaction", @@ -2813,7 +2853,7 @@ "3e14d633a30aac5ad3fa7a0dba835033fed9cb020b1d2f25172d7ac8f0f30be9": "BAL Transaction", "3fc59d67b1a1505c54e10f15cfd2a7f24c165127fd644ac5345db83ac11e6d90": "BAL Transaction", "3fefb95bebeb8075fd97dbe770e91ea9131f7ad401ecf3d48a6e7f98efcefaa4": "BAL Transaction", - "41e4db32c6457c93f853d452eed770ba23c78a3127f620fce080af0ddacec101": "BAL Transaction", + "41e4db32c6457c93f853d452eed770ba23c78a3127f620fce080af0ddacec101": "BAL Inheritance transaction", "41f99fcc6acd43b3a7a78819ab5ab3aafccf3324376a3d635f73f9719833b8cf": "BAL Transaction", "444fc6b9d70ceaf44558e815ed5483f26ecf2660815e00a14461ce3da8f922cd": "BAL Transaction", "45667d80a6a7e935664d961527dac643df62487d96d2e4ecd3a03aa4f59502f4": "BAL Transaction", @@ -2824,7 +2864,7 @@ "48293517be6644ee9efb643a220f89e45d396a8890b694cec3b3ee8cdbd85d0d": "BAL Transaction", "48a9705225ac2acfdb8729fefeb3a7dc903d59d352877c6598c24c736c056707": "BAL Transaction", "48f13cbcbbe4ce904a96c1fc78f063cde0e2b70fdda73d0f9137aedf7dd0ce42": "BAL Transaction", - "49d611bdf156bc19df59502a466a0c379f155d4c9d7509917faaf9f9ba67672b": "BAL Transaction", + "49d611bdf156bc19df59502a466a0c379f155d4c9d7509917faaf9f9ba67672b": "BAL Inheritance transaction", "4a69fe3090651e607009ebf0dc537323549d8af2c02b40c4e6345c799f72998c": "BAL Transaction", "4a8f43a9dbe2b02a401bb40696eae3cb1ff0ca23b375504c50aa8183d29b97ff": "BAL Transaction", "4acdd97a874b986907ab1ed97e4ecbb30503a30556ec3578aaedf04656cbd40a": "BAL Transaction", @@ -2855,8 +2895,9 @@ "5ce8e0787b35aa8b36a515bb77751c680b06d717dd266abdfcddca712d75bc65": "BAL Transaction", "5d1fa7991d89863c212e39e886ec3ca0f390b01e9461181d87042a59561d7391": "BAL Transaction", "5d88d33cb04b54f400c2b9492a1721bba80947d28f14e9a47ceac47d7998c175": "BAL Transaction", - "5e9355915428b150861ede28eb86cfd17692e063f495657c3fdedc267a95e20b": "BAL Transaction", + "5e9355915428b150861ede28eb86cfd17692e063f495657c3fdedc267a95e20b": "BAL Inheritance transaction", "5f3642df74775c62a4d69509ee213ca40c366cdc2add79d7108613def88dae29": "BAL Transaction", + "5f8c84f762867a340f7be5577bdfe09fced800b44c5a03e92f4092440e9004f2": "BAL Inheritance transaction", "6008b300292d3e94fa1ae08f9034b80cde6c128cacc91b61c5f0edb7fb72b981": "BAL Transaction", "604bb78f6af7e9f29194d0ec6c488ee0f83536736152f8d9e2dd1e4246f55d54": "BAL Transaction", "60b972d2dd0bb3a32eca07888a86457bf12eef536f189344047def45b9accab6": "BAL Transaction", @@ -2886,7 +2927,7 @@ "6dfa8bf0653257f85339a78b5f3201396e30e27d4e89ace66eade0f28a34b5b6": "BAL Transaction", "6e2aea8713d51d5ad8712b96e7a22aac91be79a44d7fbe8732efb81c315efd21": "BAL Transaction", "6e5e99bef234d6b3c91e9123e2641a606df8dea249c616f42c7e47116d292787": "BAL Transaction", - "6e8b3178ff725013483725bd95df08f7a0284f6dc0a62ab8740d191a8c5889b2": "BAL Transaction", + "6e8b3178ff725013483725bd95df08f7a0284f6dc0a62ab8740d191a8c5889b2": "BAL Inheritance transaction", "6f249d1627dc0335467a63d408855bfee797a6e8113d97387b50f605e9efd8a6": "BAL Transaction", "700d5ba39f4a2e8003222e54c098afffb5a264986d80b4b9d5cb7f2e3e1ab495": "BAL Transaction", "70a46a5453c356de77fd205f582f6c3ca6203f5639ab7fd7d07bbf689b34df1f": "BAL Transaction", @@ -2903,12 +2944,12 @@ "791422370f052af91f46e8bd3a3538d63ffe0f966b47b2a8da53735e8a3db8ec": "BAL Transaction", "7a0ea84937d0d0dd1fbb79ad038eb5dd22096d466cfc0c428992b5ad0ce3e585": "BAL Transaction", "7a6d1cc6be8f7f28728d9a518ce6c016cbd560842882efa05dea0a435e84f645": "BAL Invalidate", - "7a94407ccb2efc2445542bfe1107eadafb8da00692fb058c8ab3f8013ba7a013": "BAL Transaction", + "7a94407ccb2efc2445542bfe1107eadafb8da00692fb058c8ab3f8013ba7a013": "BAL Inheritance transaction", "7a9c0596520c9b7cfa9b1cc0367b7f531a232407a1b859ef1aa9dc4a83f54b6f": "BAL Transaction", "7ade990666b139f3d25cb67f7f8f931852b9ceb894b75fcf5e614cfee5cd7ce2": "BAL Transaction", "7af66fc2d58260672327eeedc1c1e0abcde9907ebea08908bac4de385ae3e0e5": "BAL Transaction", "7b65ff27aface04b738b0b32bd4af9b4bae16281ee9b104398cd02bc989d1776": "BAL Transaction", - "7be33b8465d0cc539990f55186d0a62c9a303b0f955dc2fc35a98da75749bb3d": "BAL Transaction", + "7be33b8465d0cc539990f55186d0a62c9a303b0f955dc2fc35a98da75749bb3d": "BAL Inheritance transaction", "7c88540214820e2d728c7c5cf871baefd9a17cc8d2cb223473b1089b1da8e3db": "BAL Transaction", "7d25d1b0e753475729b42391e9e4f8c6afdd5055d54ea18c92b4d7db5f6dda78": "BAL Transaction", "7d5aaab55aa767a943c343226819a9ed404a0de5b4c795b525b347b51c5301bb": "BAL Transaction", @@ -2939,17 +2980,17 @@ "8fc580ecd8aa170f3546848f7dc632a40188273954787519f7c86fb2ae36394d": "BAL Transaction", "9190f8488c7876d904574f24e287103186d3224892d3ae124d4907d444093c82": "BAL Transaction", "92441f356b8deecd7c74727eed8bc922808531850c09639aaeb0013492d988e3": "BAL Transaction", - "92687e2749995130bbb0a7e2e89961d3aca94131099585d0c4d1508ebfa27752": "BAL Transaction", + "92687e2749995130bbb0a7e2e89961d3aca94131099585d0c4d1508ebfa27752": "BAL Inheritance transaction", "9277f76b387fcd8b8dfcfb953478713c81b0d19add332fd168a6cf880361f262": "BAL Transaction", - "934671a6ef20b76a3ea106e69ff79d62851c1213b3540bc6fb8c34a253b83dcc": "BAL Transaction", + "934671a6ef20b76a3ea106e69ff79d62851c1213b3540bc6fb8c34a253b83dcc": "BAL Inheritance transaction", "93c34606583496c377493021f40a44d6cc0f10d90d35e5310be562c5849dea0f": "BAL Transaction", "94ab63ab27d9bbc9fc819d725a5b05bc03ff648679b448e92ebe51fdf6810379": "BAL Transaction", - "959f3e6f135b585811e87165352bb97ae696309a0e9e4f6c93f63c85b5178d2b": "BAL Transaction", + "959f3e6f135b585811e87165352bb97ae696309a0e9e4f6c93f63c85b5178d2b": "BAL Inheritance transaction", "95de189fc288fae9ed6776e73c092b7e3fc06a5dc16ffe46701b8c2955bbe04d": "BAL Transaction", "96645e438d436b414910635eac3d682f7d5ba2dc44de58ad3ce888d4aa235a40": "BAL Transaction", "96aca9e516fa3fd42fbfb7cc55d662ace4b4ad7ccab574c9c010196bada450ae": "BAL Transaction", "982709b1000fd5d5e595cea9e476bf6c7ef51325b8c0788d81515f1dbc017763": "BAL Transaction", - "98b959e4921df6feab764eb7f41f5afe49e7cae5950a10decb6e3c1644d873f1": "BAL Transaction", + "98b959e4921df6feab764eb7f41f5afe49e7cae5950a10decb6e3c1644d873f1": "BAL Inheritance transaction", "9925e61bbbcc7b87bdf14fda598dcdbaaf8f77e0150c9db746933428ea7a83a2": "BAL Transaction", "99fa37baed19ad25107ee1b374f355eb3abb5daf80125035fa32a22ab8b38ecc": "BAL Transaction", "9bb9310610f731452ddf3a21de32156f898e5d03dacb25f29bfb8cd1f1c6f593": "BAL Transaction", @@ -2969,6 +3010,7 @@ "a2eb6bf05d350b2a243b26effccf3d55fe073e39c043fe8245555f922855a567": "BAL Transaction", "a33b83d5484b65f1486bbda5e01b886ae149bcfe71a30c5e55541e086b880a0b": "BAL Transaction", "a37c2e708ae71561abd7923b973456f09f52ddb39676c4d614205ba53deb1338": "BAL Transaction", + "a38fdd27e8ab86430603726fb3d9c7c30e452df0aca433daf822bcecb99c9fbd": "BAL Inheritance transaction", "a3c1e6c863c3c4f3b8b293872fcf9092026993181ffdacf38c1a2582ba389460": "BAL Invalidate", "a4881681364692001e38ffdfd5625aa5403a5ae5f4e460551de71321af562c0e": "BAL Transaction", "a58ed3884582beaaa89a76897abdc210181d15731e561c77ef8fcce1003325f1": "BAL Transaction", @@ -2977,12 +3019,13 @@ "a7370d4012ed5fe90e2adb65f6803a83f9880d6f11a82bb482a762e739c76701": "BAL Transaction", "a82b44e37ddbd69a4109bb00e83667f79610f1a0901e50e724445e22bb475234": "BAL Transaction", "a89c37287ae76914bf2d0b421ab4b3e444ed36f4c7ebde676d9013bba675bf97": "BAL Transaction", - "a91ff4a30363a1907d57093a92adeaed86820cb36e6225b571b17b655e0bf512": "BAL Transaction", + "a91ff4a30363a1907d57093a92adeaed86820cb36e6225b571b17b655e0bf512": "BAL Inheritance transaction", "aafcddf0b1f7c9b3d6d37a17ec7bb0871a0a0c4f2c321072ca9647c9d741d897": "BAL Transaction", "ac0de48b1054ba94514436f54909001fb2626cf41eb970dfbad612e7a1d34bbd": "BAL Transaction", "acd0108d51fd108143b213ada10f587eb92a96c7d6559dce8693a02bfe5e0ca3": "BAL Transaction", "ad24cab12fb8afcb1cfc91710bb25ef7981302d0fa633c912f90d7d8c5e88c60": "BAL Transaction", "ad2e25ec4473646947853704cb31fd2c37eefc97c9e20b28ec94b93a8f358c45": "BAL Transaction", + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91": "BAL Inheritance transaction", "ae3e03f0a49cf2f700804d44869ec51779f636b0303f7032781b0386fdea3e45": "BAL Transaction", "aee97f5286815061bae493b883f503c43f9682a207d366ffba7400c08a961abc": "BAL Invalidate transaction", "af8a5e53fa3b8976578119ebf79cb77aba0f5326453d16bab7b072fb7ef53cb8": "BAL Transaction", @@ -3005,7 +3048,7 @@ "b6b5d5159c9d1440a15093672d65bd21436345e928b0bd36f2b08862d68d6c9a": "BAL Transaction", "b6e993fb2b0584bb35b8eefcfbe6ba7c9b9b7875e69389063c96f78dd1fca9e1": "BAL Transaction", "b6fd24d674338578282a3eb7322c55c8f54c4af4f3eeb97a4ee45ed6a2bc70f9": "BAL Transaction", - "b708916e8f8ab4562bb415f6876276e3901231649a9ed7f2d22ed0bae65cf0b9": "BAL Transaction", + "b708916e8f8ab4562bb415f6876276e3901231649a9ed7f2d22ed0bae65cf0b9": "BAL Inheritance transaction", "b84824bc26a8d8a3161d86c68adace287337f4478ac4b3ab6ebb1934ffeeae15": "BAL Transaction", "b9b61961a556f0f08c646d570df0c790cae1a1b8fa7b55befcfb41d52db289bf": "BAL Transaction", "bb08265c8e0ae8a07d304b928b867898c670a249265b51dcc5c768aa21c2319f": "BAL Transaction", @@ -3033,7 +3076,7 @@ "c92e5f22dde97f5c7e807a505a3d5ff2426f0f57607b7a4e27c8fd5d5d97603f": "BAL Transaction", "ca2dbe11e9e775c3b5727b59ea134f2d0c0fffe037f775fa3e302f301b923c30": "BAL Transaction", "ca8dafb74a0db880ac900eaab0048f67ae90cd3c6078febb8074423a57583cc3": "BAL Transaction", - "cc249f4b68e8d52c4a2622d86bf13ebc256525e2e85dd7dd02514658e869d4dd": "BAL Transaction", + "cc249f4b68e8d52c4a2622d86bf13ebc256525e2e85dd7dd02514658e869d4dd": "BAL Inheritance transaction", "cd0d987cf28cd30df34adb651541e524ac61593c44b277647f0b41187a8abd84": "BAL Transaction", "cd27e2c77a9742e8e7e6bfc535f83e1e7e32ec17aff8fb88ad3afa8452c023e9": "BAL Transaction", "ce8453af49485fcb423dca1a56c24e1c2882fc48d20f7d7cdf50db94c7aabbab": "BAL Transaction", @@ -3042,7 +3085,7 @@ "d1ad9ae9b9157e1e226b9e838be661863e2f8e676b226262f19b796d1b1af680": "BAL Transaction", "d2aa2659eff3afaa03df3857b9c651ab446d3f30cc3195779f5cbbd7f4a5b21f": "BAL Transaction", "d371cd377314c8b9368b8870304dd329d58dd9ef3b4635483907877f10c2855d": "BAL Transaction", - "d3c6048e8b2d169ea88a8b85869e996b165181a8532853c1e94eb6010a53164f": "BAL Transaction", + "d3c6048e8b2d169ea88a8b85869e996b165181a8532853c1e94eb6010a53164f": "BAL Inheritance transaction", "d41a30d63836d8a41bed7a819a61c7383e728fbac49e918ad099b96dca0af634": "BAL Transaction", "d4495edb0266dccdc77a8f1f6899f0921f56ba14e80163578d122dfd9f0251c4": "BAL Transaction", "d46d913da6bc925eee8bd5f9d657ec233d27710e77dcdb80252f52be11f63cc3": "BAL Transaction", @@ -3050,6 +3093,7 @@ "d70076673eacd58fb751208879026017cf0e5ece79aa61a51d97fad41dd6a66d": "BAL Transaction", "d7fe2bca48350d74a0b0a1404f4eb1f8ff065188c243e0a8887da5c5a9758ebe": "BAL Transaction", "da413024e4e43713a57330935582797d54ef1941dafb31198d3781e109cbf6cb": "BAL Transaction", + "dabe77c276942c463e0c34b49b1aba08f563dce97a32c41c098d59e58f476539": "BAL Inheritance transaction", "dad8661abf23b3e70f9a1cde4b5c31ceb4b4fc20b993cd181f3437336e3deebd": "BAL Transaction", "db0a6167c75774f2f5b32b7387a2f28fec74ff13646ede4fc586a4d602c36c00": "BAL Transaction", "db4bb399b37e567553605a50afc1c2eb570fb621994bb3f9ef8f7570ef71f174": "BAL Transaction", @@ -3068,6 +3112,7 @@ "e160dd89be90ec695de63786285ecc5958aba9c225e50f345704113c6732752e": "BAL Transaction", "e1742e6d9a9c3a60bab39a0796235511f3539e8eca13ed81349ccaf95d21f516": "BAL Transaction", "e216f36276646b01f5e5af7bced7765a18870e8b5a7b0827938cdac8b5d97144": "BAL Transaction", + "e289ca8134bc76e77dfe30295482489bfe15831bf6d6326471fb8d1fc876ca96": "BAL Inheritance transaction", "e2c550c4613e5126df9c69383535f03651000902378979658c5df090cf6f811d": "BAL Transaction", "e2d2f23a7291b8cdbbffffc516e433a30c537f3c4f042f9a5aca924d57cc0ea3": "BAL Transaction", "e41564ef450198b9d55c845e1ba85a5385547d510423fe32fe475b7ef279f60a": "BAL Transaction", @@ -3086,17 +3131,17 @@ "eb1417a958183da33a6397a482bd346f0e8d5caa35e5331fb158c6908d838782": "BAL Transaction", "eb51046b98d7240fe428c3af0d953c0a0a1a0d66ba93cfa715676cca77b662df": "BAL Transaction", "eb9e58480b7d7bec43a8a5683a61b7fdf7a5d201c73f2996b7fd43d3701d67c5": "BAL Transaction", - "eb9f798774b196368f5f1e24a22669c48dcd8406aee7b8de878ad0a4b42a189f": "BAL Transaction", + "eb9f798774b196368f5f1e24a22669c48dcd8406aee7b8de878ad0a4b42a189f": "BAL Inheritance transaction", "ebdafa5f7369da5f0a1f028dbe691341d21ed2be2841d542b7cdc912828779c6": "BAL Transaction", "ebeaa58132e094d36c1547b3869a09fca67380f0d91af6a258db2f34595be8bd": "BAL Transaction", "ec93c51e45cfb522369bfab0b576d162980e0f4a3bf3ea67a1e163990bf64073": "BAL Transaction", "ecc52f18f8c901324d1754af8d9ad5087976518c8c14f904192801925568b914": "BAL Transaction", - "eced91d575c32dbf7b9712d7f2e6881f5992cdad954d486f4278f223cc7cae02": "BAL Transaction", + "eced91d575c32dbf7b9712d7f2e6881f5992cdad954d486f4278f223cc7cae02": "BAL Inheritance transaction", "ed2a6efc33fe112a71f99064a639949b83b33f04b64e3899a694c519d9d87a78": "BAL Transaction", "ed84cb24feaee9ce5e192ee8074be273be4e26752bb63eb73daeb0b640324bdf": "BAL Transaction", "ed8d44d7e759281f28590abc46d011bb071718800d9ebc280e6e2d1726701cd3": "BAL Transaction", "edb2daf688ce7a74c9767592252b99b5822412e4d6228b4fb6b89f55bab74515": "BAL Transaction", - "edf4cf16e67adb2b4edae518122f902d5e6c07499ea4f5661e32b272bb8d67d0": "BAL Transaction", + "edf4cf16e67adb2b4edae518122f902d5e6c07499ea4f5661e32b272bb8d67d0": "BAL Inheritance transaction", "ee359445be96eca2f001a4b3643ffd091e16d9fbb1890f89c7f922b61c9d231a": "BAL Transaction", "ee5d4e94794d6b04438601635be8a80816625a10f0b119fa144f2f295bc526e3": "BAL Transaction", "eef9d5b762d135cd3d58d677fb5aea2b4add475df034a7783ad4b968eadef6d2": "BAL Transaction", @@ -3106,7 +3151,7 @@ "f1516704e850e5b1ed4c9a6320a48a8ae81e2f3f9652a4accedce7ed0837e201": "BAL Transaction", "f1623847ce6864d02e2a170a9166dbc3cd1725671b1afcf6e729dbf41ae48c69": "BAL Transaction", "f1c2d97b87ec7ed1b58e8b2b70dd3263e958e6b02a45477ef9b2c5f98f53df25": "BAL Transaction", - "f3fe529f7269138b4d08bb934e0d61546d5d590db7a3f62a23a360590a3d533b": "BAL Transaction", + "f3fe529f7269138b4d08bb934e0d61546d5d590db7a3f62a23a360590a3d533b": "BAL Inheritance transaction", "f3ffba12772fa75e55a64ea23e82cf3b9e9a741e6df6cfcb913194bdc90b6c94": "BAL Transaction", "f547b63419bbd93363c074fa69624f614e170d43468026d993a3bf5fb0898f99": "BAL Invalidate", "f5b006855cf327b641873c53ddd019dff16d55db335178cbd54125adab9eba5a": "BAL Transaction", @@ -3215,6 +3260,7 @@ "a001edc1d43b5b41adc5a4c5ce9b6edd9dcad9fa3e50cca287d256a23cca9d4e:0": 50000, "a1aee934c5dda700d15934667507f90db1ce36d7dd3068165b54159df320cd02:0": 50000, "aa407bf21c31fa3ebe8bf074767bfec17df01644da94b3b709c3d3551f3210ca:0": 50000, + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91:0": 1000, "b28b20fb9089f2d021d812dc34ce461fdcd800137434edaa09160a395c182c8c:0": 50000, "b6e993fb2b0584bb35b8eefcfbe6ba7c9b9b7875e69389063c96f78dd1fca9e1:0": 50000, "bdaa76d37810e4d4ad1d2e7ac6e198ad96c48683e552e874901a7af3d8ae4e50:0": 50000, @@ -3437,6 +3483,9 @@ "0f43fc3c570715792fdefe09942cbc8ca56ab7de89dc48f5f5d3d40866782d6f": { "a8b6b761eaf7d647ea08b3f58fdd382812bbefc5f96acf114d1be3be2ab1d94f:0": 1250000000 }, + "0ffbdd9ec702c90772753e70f297139956e3dcc8d1513c5bb499605a2d5eff43": { + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4:1": 1000000000 + }, "10c9f5284da963e6df4e4ea8cfe3777793c254dbdfbb1d11fcd6fd536c4b1ab8": { "be01b70f888c47b2031a23e85b7ded702c99fb6371e7b60255ffe539457973d0:0": 1250000000 }, @@ -3473,6 +3522,9 @@ "1bc529ebd7935eb9c2974cb827f889bbf009913f7898bdd526f3570919c19315": { "b0d5833bc0b04e9d59e319e2f5cc8ddf382d70208848ac6a0302c675a1ddb74d:0": 625000000 }, + "1e73e33e17e5a95f400dfc5107d689a2d69b7aad74089d6f639f049f0e5a7efa": { + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df:0": 999999780 + }, "2323c30ff2bea543f2b3f0feec2d21faa8269516b4712466b0eadc8f09a7e321": { "3e3c31ad2bb0d69050a4880982e1884ca56be999b14b53d14e4c60524dc224de:0": 625000000 }, @@ -3559,6 +3611,9 @@ "474a39c86e078d175c0159729d5ca68d0f22eed36d1fe2d89dc28eaffaf27346": { "856f4b1a6c2b8055e6b48686129878c68f83e0444a00430cb319fd7186e9c1c7:0": 1250000000 }, + "4798306b08b6046a3a632a488bdb6de0248b22d74b57df968c53adf403507d6d": { + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa:0": 999999890 + }, "479f49158924a137f80f41628743ff5df6f8d9e4ad3846ce8e965923a8d9ca35": { "2c8737fddbcf0d130dcfb72dc0fa000a7dc70dad88dee6aed2acac1320e8fd06:0": 1250000000 }, @@ -3663,6 +3718,9 @@ "5fb4dbd05379842b11a048ee73467fff68ef04500bf59e0e1a0388474b0e259a": { "af9e9fbb8e65d724bbf483ed4b7577ff183b0e8ddba91ca40456d43b3135f81e:0": 1250000000 }, + "61d6068a9000df4a2d0c919d0845ceb7b9f690e609fc27238e38facecbe38d31": { + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91:2": 316818514 + }, "6232f0790f4bc993b8bd042828a1f626b0ae3ac20b1dcf5a479be57c1035de29": { "316b0ac8e6516b59633a46dd5f4fa19da180f799f871a57085f6a7e99bb561e7:0": 1250000000 }, @@ -3753,6 +3811,11 @@ "8cebd66ae223c28c1db0fbd9638668ae9a28c757b164c97e93a22c9b2ef9fb8c:0": 610351, "b3e3d7cba32180e7a72a2e66126d6b2bee9e386788ee099c5f032efaa8772fe3:0": 751351 }, + "7d1852bfc82939c14ac37a01ad558273fcdc304c50eb3dc87272643bb2ce36b6": { + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91:1": 40001, + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91:3": 336619671, + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91:4": 346520250 + }, "8017a32067302648ad152b7a87fdb1c2c4f19c174a5240e88666ba987063163c": { "1d30124dd0acb5c11a2cb8e607c50b5f402a7be178cafb264747fb0dab37c8cd:0": 1250000000 }, @@ -3822,6 +3885,9 @@ "95f4c10c5da0e35527c126f04f3229ee4d2e1d6df697f7b126897935a14905a2": { "b00e9f65ca32f0ab7a05666f21a78b461623c43d1297a731f09a807f2c9dc497:0": 78125482 }, + "97ad2180d9b8db98aefe25c0dedbccd8fd9f3b241417cadc54a417987fa081de": { + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4:0": 15078000 + }, "98a3edbd711be7cc7f12cfb13e7d0b9341fa703ee33ad61af53f0b20ff4acf55": { "83ff9433dece51bf3a37f60c81e196fac96cef88c587a2f97fb44be490482536:0": 207251786301 }, @@ -3902,6 +3968,9 @@ "b91972567d210359b966034cb9ff986d190a8c1abd178ac02f51933e15b09d4c": { "df14cc18d2b940b04984ed2a9e6b8f059d9672569b56fc4e335a300a3215eb7b:0": 625000000 }, + "b93ab2ba8a19b1e45fefc1503003d483a59b48f0a9ad9fc1e9b7d0906dfe042b": { + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c:0": 999999670 + }, "b99b44ee12d7ab5a76dcf34b2b798920e38d6e97f8fea96690768c46750af520": { "d4beda4e362e9a6c393af904fc0f6417add96b81566514e37217c017ce5c6a94:0": 1149996475 }, @@ -4199,6 +4268,9 @@ "078905c318abd82e947af0d3836f38852f3463fffcba279ccb5d5dce58bfcfde": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa": { + "0": "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df" + }, "0a9ade1ed72b0b394bbc3ac826146f2650122b01ff9db251b2ceef69ac4e3177": { "0": "d4beda4e362e9a6c393af904fc0f6417add96b81566514e37217c017ce5c6a94" }, @@ -4315,6 +4387,9 @@ "2af047c4a003fb7b90ce2c8fc49bcdb3eaee0fff213b14dc1645b9fb18b8f0f6": { "0": "d2d953e950d28c021a5ddf6f77cfb9551af2fd33ccc7473b20a31d208e7bd308" }, + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c": { + "0": "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91" + }, "2c6d3a41eb0f5589e2586138b2bee1cf453144fe48dc73c2a3b0f4d0a3bfd596": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, @@ -4352,6 +4427,9 @@ "360c08390022019fdd20d6c27415592cea239e8617bd97e6ff4209a317ede43a": { "0": "e2d2f23a7291b8cdbbffffc516e433a30c537f3c4f042f9a5aca924d57cc0ea3" }, + "366af56dbe15c77b6461b49597e6c6cd556721ee9f40aed811264ac551ea9850": { + "0": "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4" + }, "36ac1d694a8810ff6e930a2f7751191e72414ea832e8fa66ad4d087f09064cd8": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, @@ -4361,6 +4439,9 @@ "37c7f8e63152bad6ff3b704e4a60985cb25d877606c7c0514e7ada480f99ef1c": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4": { + "1": "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa" + }, "399231ca4ba596e68131aeb0b7c5e721c7b90a4e7c27e4848487fb0453c27d2e": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, @@ -4409,6 +4490,9 @@ "1": "fb650d0b87814550b9cc20e8fdfc72198bb13bbecd74ad29d9849aab039d0256", "2": "065cac0bb30c763b66c25e890be537589f24cb90219bf30da96a5d37e051e052" }, + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df": { + "0": "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c" + }, "506e0e953ba217afd18465980c16438947d86ebd74959c689b3b53d1dc2fe2d5": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, @@ -4654,6 +4738,9 @@ "8cebd66ae223c28c1db0fbd9638668ae9a28c757b164c97e93a22c9b2ef9fb8c": { "0": "d2d953e950d28c021a5ddf6f77cfb9551af2fd33ccc7473b20a31d208e7bd308" }, + "8d92004b3bb7730c3fccb14ffb900d5c8534e51b104341fba78152c5ee663baf": { + "0": "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4" + }, "8e8c64dbd7432177be9ee1b7c73169abb39e902f98ab8f0eb5fd178b8f3922cd": { "0": "b6e993fb2b0584bb35b8eefcfbe6ba7c9b9b7875e69389063c96f78dd1fca9e1" }, @@ -4728,6 +4815,9 @@ "a456463094a89018dd2252696ddf6c6a5a47797a5b3879418119d6d725ab3dca": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, + "a52ba8af4045b4e27e3b2e194e4f8f814dddf366a556dd71fa241f948c424e9a": { + "0": "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4" + }, "a89cb324d4efb56f9344fe323eba15835188e1eb6b6d52ae69cf2e3664fa3fc3": { "1": "83ff9433dece51bf3a37f60c81e196fac96cef88c587a2f97fb44be490482536" }, @@ -4821,6 +4911,9 @@ "ba67580f9ace0b3a538e32604bc26ac5c81c1f68e46e9d4cd81cc3f9cd7019ce": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, + "bac7b5bfc8b0adba152bda164616d47e3e9ca8c95d4dbbcb50545cd0af23216f": { + "0": "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4" + }, "bad9c1fc8345c31df5af1bafc8ff4f6301be18297ce09696d9a599f2bc4a369f": { "0": "e2d2f23a7291b8cdbbffffc516e433a30c537f3c4f042f9a5aca924d57cc0ea3" }, @@ -4916,6 +5009,9 @@ "d4beda4e362e9a6c393af904fc0f6417add96b81566514e37217c017ce5c6a94": { "1": "e2d2f23a7291b8cdbbffffc516e433a30c537f3c4f042f9a5aca924d57cc0ea3" }, + "d4d3f08fefd664c769c70b73bd9b33b28804987f5480f563ee230384d23aee5f": { + "0": "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4" + }, "d5ac3c8472df87a653bb8ccd5697d94e3525db64b3580822dfd092adc74b0cd2": { "0": "5c6d7a19cfb35a98949a25c5fede1dd798a7e1509e0067d000319126c34f8c15" }, @@ -4946,6 +5042,9 @@ "dd1c0eff0557aeaa33fcfea188ee17770a708cb810896786b90293a254b20cdd": { "0": "8a52e590d4dcf4a04bb46c4064adebed67556bfcf621f7775fd85d4a9c9c8132" }, + "dd5ff67809bac17da548fea22d2f3df60fc9c27d3cee3e9f4962fdea19717bd1": { + "0": "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4" + }, "ddd6e44aa204ad1e4fca370602b67bd07395a04b5330b37ed5bbed3648ca823b": { "0": "b6e993fb2b0584bb35b8eefcfbe6ba7c9b9b7875e69389063c96f78dd1fca9e1" }, @@ -5028,6 +5127,9 @@ "f1634626cdc61a8f9db3698f98838655f88a0b5110a9302f4921566a056e101e": { "0": "e2d2f23a7291b8cdbbffffc516e433a30c537f3c4f042f9a5aca924d57cc0ea3" }, + "f1a68bec50b173fda18d5c1123bd6b6816e3012d11098771ef86ad35aede7912": { + "0": "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4" + }, "f1c2d97b87ec7ed1b58e8b2b70dd3263e958e6b02a45477ef9b2c5f98f53df25": { "1": "0fb9da0e14c71d8b85bf00d23fa3b599972ea48f94e567e0f9733ed7357ba686", "2": "0fb9da0e14c71d8b85bf00d23fa3b599972ea48f94e567e0f9733ed7357ba686" @@ -5077,7 +5179,7 @@ "2": "8cebb4ea3a63829a2787c3a473f14b4f73ced07552f73ff8c2b1152169042550" } }, - "stored_height": 2247, + "stored_height": 2336, "submarine_swaps": {}, "transactions": { "00fda0d8fdc53b1f95410bafca884c5dff8ac1e5c168ace5f09a32d872b7413a": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402460100ffffffff02807c814a00000000160014b51c529851d6140f1a37f2aa46bafe171a549f210000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", @@ -5087,6 +5189,7 @@ "030a1ef891f4a7179a5d050fb5160c361ffe7a5ba72211e4181de177224ea25c": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402c70100ffffffff0240be402500000000160014eb682f6e8ed2413ac72b90eb83a6d86aa07f361b0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "065cac0bb30c763b66c25e890be537589f24cb90219bf30da96a5d37e051e052": "02000000000102d7f753992071964c78195565ac823026657e6380a743db303ebcba257320044b0200000000fdffffff56029d03ab9a84d929ad74cdbe3bb18b1972fcfde820ccb9504581870b0d65fb0100000000fdffffff0350c30000000000001600141c9d87b9c736188e6ca276c07a1dd36811c7780a400d030000000000160014bab078939f643d6439e8d023fb180e857134bf860fe5ea2830000000160014e8bf9d2bc3aeba1863e43fe5505306499035f32e024730440220556aebbd144c33caaf83b65a0ac05cbc26e3a281420b9b54ded51f39de2f1ded022047b2413105dafdd7740e6a8363cea7d6ae72a52b0cbe462483f886b8512bd493012103f68bf5a54ded5d076face45ccd0755452a3cfc8ad7244f295f92ab5c1161e4dc02473044022056a51e2768c9615ceeea4858adf3e840dba681e46051a96e331544966067270a02207a1988fa1d2ea53011fbcf583273a316afb56243e67633c9b71eddd40aa4dd910121034b01b6a31c2fc9fd02004dc82e8cb33aece5a3bbbe476d22bffbb8fb49d3310c2112a267", "078905c318abd82e947af0d3836f38852f3463fffcba279ccb5d5dce58bfcfde": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402260100ffffffff0200f9029500000000160014a441a4329c0bb05f87532549930c13cc19b0c85b0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa": "02000000000101f4151ca6323735f70fdada3447c2edabccdbb1986e89b0b6d3b042d0d40203380100000000fdffffff0192c99a3b00000000160014cec5d5b90a439829ecfca49150f79e299575acee0247304402201fac7adcd07fb85e3d19ec1caca12446e6767115fac56c3987168f1b97e0def502206af3742affdf9b97d8e8faac80cbb99d59a891ae95e2864ed44a585c2a84a8ce01210299b10a0c71697c89fb2cd340b4e45fb2299c63f1bd3fa646b94a579e01891c3dc9080000", "0b2d3c47e2f34a34baf07ed83308aab05101b3afd3a0494167e6db0c46bb7bf8": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402d60700ffffffff022f50090000000000160014ed834d872f7e3f5eaacab4fc5249df650e626fa60000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "0b34c01afa7c05d19c442b8a976679a3ba3c5192e2ea155d83ada16630dba360": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04021b0100ffffffff0200f90295000000001600141ee5d37ff99201cd74495c13c561b220c47748c20000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "0ef3b8b6a2c3d28126a7c79aa95b5bd1f091b7eb8c14c20fd5ee89c409462c94": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402680100ffffffff02807c814a000000001600147ad31bcf7f7c1f11c00990b7baaff3370b6797450000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", @@ -5120,6 +5223,7 @@ "2a35cd9588ee727bc532bcd185d8ef78d5d54ec168cdb2d49516e8042ddf05fb": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402d20700ffffffff022f50090000000000160014ed834d872f7e3f5eaacab4fc5249df650e626fa60000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "2ab0948cddb04221e6ab88d2d9ba5ac11db5e5d1e0ef89ea8cc69b19a17dc6c2": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402180100ffffffff0200f9029500000000160014bab078939f643d6439e8d023fb180e857134bf860000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "2af047c4a003fb7b90ce2c8fc49bcdb3eaee0fff213b14dc1645b9fb18b8f0f6": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402dc0700ffffffff022f5009000000000016001407749a73207db3a0d94dc786901b6e9fde1c2e560000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c": "02000000000101df16f0b4388013ebe0131d208160c6794aff4a66d87bfb5512b4b5d30ab2ca4e0000000000fdffffff01b6c89a3b000000001600147efb413d5bef80fc03d2a95c2d6b1f4e8a674520024730440220620d69a872b9aefb51e2a76d98100881587764488c2e6983aaa251c8e728e5270220695573645ed50e8b9d07e33c6bd9f7ef98b985717ec3e467476458b3ea0a9d48012103006091307fac87f747a53c9d79c8504964114cb990c3c0421ccafbbce6951ce6e6080000", "2c6d3a41eb0f5589e2586138b2bee1cf453144fe48dc73c2a3b0f4d0a3bfd596": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04025b0100ffffffff02807c814a00000000160014e1d31bad0570d8f5701e5442736655dd6f95c8050000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "2c7b1c375b27d7c61eebbb1d9feaeefe0133af1255ec705e4053a7ec66be732a": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402c50100ffffffff0240be402500000000160014e0bf824000df76064d492c58db28a31d2128bdc30000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "2c8737fddbcf0d130dcfb72dc0fa000a7dc70dad88dee6aed2acac1320e8fd06": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04023f0100ffffffff02807c814a00000000160014310fd17a0427d944b429ebc12203c0fa676223280000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", @@ -5134,6 +5238,7 @@ "36ac1d694a8810ff6e930a2f7751191e72414ea832e8fa66ad4d087f09064cd8": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04027d0100ffffffff02807c814a00000000160014642a225806d20e3e23b8659d0b1500fee55f877b0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "37b933d6b0d52746412a4bc06d9e7bf6b15cd42cafe8938f6f686e416547322f": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04026e0100ffffffff02807c814a00000000160014e4aef3250955663a2c8d95c3fa6c80c222c2aae20000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "37c7f8e63152bad6ff3b704e4a60985cb25d877606c7c0514e7ada480f99ef1c": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402710100ffffffff02807c814a00000000160014badb5fcf371baa082ddcab46af07b49334e45c880000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4": "020000000001079a4e428c941f24fa71dd56a566f3dd4d818f4f4e192e3b7ee2b44540afa82ba50000000000fdffffff5fee3ad2840323ee63f580547f980488b2339bbd730bc769c764d6ef8ff0d3d40000000000fdffffffd17b7119eafd62499f3eee3c7dc2c90ff63d2f2da2fe48a57dc1ba0978f65fdd0000000000fdffffff1279deae35ad86ef718709112d01e316686bbd23115c8da1fd73b150ec8ba6f10000000000fdffffffaf3b66eec55281a7fb4143101be534855c0d90fb4fb1cc3f0c73b73b4b00928d0000000000fdffffff6f2123afd05c5450cbbb4d5dc9a89c3e7ed4164616da2b15baadb0c8bfb5c7ba0000000000fdffffff5098ea51c54a2611d8ae409fee216755cdc6e69795b461647bc715be6df56a360000000000fdffffff027012e60000000000160014f38cdafdf2eeda1d19f5169d76f8d7c797bc012400ca9a3b0000000016001435e821f94c423e1b4170ac5bd6ebc3eeac23624402473044022021a9c286fde6672722a3fda68ca7f694f5ac5c6fd84f9fda2eca8046d8cc700a02204d831d2db6151c57e6a23dc1c0de3bba465d85e28effa3e42bc3be6179e7781601210377a75fa3e4aadab173d006b686e415b130a0fe06ba2bdd5c09b1f66aaf9fd9fa024730440220074c6e2b07f5d81305796dc80030be29f40a16a94185947b836c57202ac20cd802203e53eeb8da3434563c8f22fd7e7d9c4fa319e507cb4b575be1f8fc7865bac98e012103b54619c3d2230e1d0010cc3427d457e4604ea7d2accab077455a8b493a24b19b024730440220367212e1626f1bfc4a205aa62c7febbc420635ddebe23ee361bf4f7e579bd6d002203fac3c3ccdd2bef2b9f336aa2c4ba6570cf969c6ce62087306fbed85944b85b30121023027c965dadadb8b2c76fb41982f56aafeb6f66781249cbc02839a6cdf808ee102473044022075ae6a65c50f5043769b532cabf56cda420061c657b22bc20e57e4dc40a6e04d02204fd280992c15d3ddbdf019bb71d586a642427194a29081fb9bb7352ad023704d01210360461e681e4d2b5a95a7340340dcfbe95c7f27eff313e7ab7cd4e3e2dc62cd630247304402203ef6bed57ff660788ca04ad4be530a7e544baa8b5d00d859b8b600e6bef566d302202a920a0c485ed04b916c34c8cafed16b98546d6bf10efee8c920b31d6543e4a7012102bdaffccef9ad85ccf5cad9ad214a5bcbf46d798e8090e7f9dd2a3b9b02f7780502473044022001b63d248ef0df664669f02c4d4f2ba186a0b6dab3338f7088f984b136ad78e902206f348740b4c8efd565d0ff6221b01bfdb4c69502e7785a5b14017fbb7e0c2578012102e9ad2e0c01a44d5161bf6dcbcc13122dde8b4e8e4d48ab168f530bcd61f7c9e6024730440220050a87856a4df819ae71de98d540c10c1de09ca0dec98efea6f92ab232fc490c022027dbdff883d4c8d754644a9530d93456b83518d230fec083bf51533c383e78a20121033b15293dd9ab03b8fd679cb7cb5292da683b4de3634f4e235b7a99d7de4136b6c7080000", "399231ca4ba596e68131aeb0b7c5e721c7b90a4e7c27e4848487fb0453c27d2e": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402350100ffffffff02807c814a0000000016001469f72233edfcf876716dc36996e12d4b6dbc9d0e0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "3d2f9eb3a839a98bb53c6df9b9df3af9420f487955329009ebe6d5ce35c0d7de": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402ee0200ffffffff02902f5009000000001600142d2371c62e745b404020d5fa6ce912ab65b8f57c0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "3e3c31ad2bb0d69050a4880982e1884ca56be999b14b53d14e4c60524dc224de": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402cf0100ffffffff0240be402500000000160014dac28adafaa609950ebb725204ab208faf5706620000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", @@ -5149,6 +5254,7 @@ "44461d09d9fdd865944d6557405134f19cfd817bc293270223c7a98c862d03a5": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402740100ffffffff02807c814a00000000160014d5f553de51cb9c48656f05f5cabc6abe4cdc8b8a0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "467dd15b59be876ef199d91d606ba0ed1adec119ced785509457da0a53450842": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04026b0100ffffffff02807c814a000000001600145c8f0c250fdaabafc4993a4e214ba672e73ea5990000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "4b04207325babc3e30db43a780637e65263082ac655519784c9671209953f7d7": "02000000000102faa05a4dd87dfbd0de33ce15cd27103ed4f52a62727ffacba9655da13f805a5a0100000000fdfffffffaa05a4dd87dfbd0de33ce15cd27103ed4f52a62727ffacba9655da13f805a5a0200000000fdffffff0350c30000000000001600141c9d87b9c736188e6ca276c07a1dd36811c7780a400d030000000000160014bab078939f643d6439e8d023fb180e857134bf86cf0aec2830000000160014e8bf9d2bc3aeba1863e43fe5505306499035f32e0247304402204903acd35481d4ffee29084294699abc3d16239e201fa130a491f3b0ce275946022061230dcbc2b9efc7b719411973e5594f5f367c2463201cff3dff9180db4945ef0121038d7efb75df24733109903da38632c3a3404bc80451d29524a96b623a7af5921e0247304402204fcac2aa0d0504ef065a5e51c4c6e1fc0133b03a39a4ed801da4df03859e93be022078ec0784a78e997cf72623c89aab690eda8fd369426070da46cc0169c3a4861b012103f68bf5a54ded5d076face45ccd0755452a3cfc8ad7244f295f92ab5c1161e4dc2112a267", + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df": "02000000000101aa5901e7c8c6d1cb1fe94365805910805f7e9c340472242be127d21cce43970a0000000000fdffffff0124c99a3b0000000016001471ff5c0248b0ed23b40b01c3b146b149c6f266bd02473044022025787f470a657bd6ea434f4c14fbef780c38f2081de2e2a20f2615f86ea3d612022070ccf848540519006255921d76e4c0cc22100656bda383e6dfe0038c713df4bc01210335ada2564d806b8e413465cb8ecebbc583e0ed6d2b19f9b882c3c6416be711aad9080000", "506e0e953ba217afd18465980c16438947d86ebd74959c689b3b53d1dc2fe2d5": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04027a0100ffffffff02807c814a00000000160014e53b4aa15b60aa472582cdeda806cdf48fbf7d950000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "516145bf354893ca80ccfaecf05620a4d99698a515d135cfbfc4e12f80f617fa": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402480100ffffffff02807c814a0000000016001448a412cc5ca059212a9f13c7ac495c0b80f87c3d0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "52803985df4cb02bd28ddbf089671e5ae130b366bc348fcb4db0002c3d2a0756": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402f70200ffffffff02902f5009000000001600142c28ee341a583a16df22ccb6ae1b36d24f5938cf0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", @@ -5254,6 +5360,7 @@ "ab1012eb232c0070b566b6620a8b839e8fa78831cf8216337c1db71e35b89953": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402d70100ffffffff0240be402500000000160014ddc0da8880db383448d588868cd34ab79779dd7c0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "ab74079fca87f186df5676c62de63d8b21e98746c8c8888f108310bc7849e70e": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04021e0100ffffffff0200f9029500000000160014178787a67beeadb99e36f6c5513fe8e2b02e800b0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "ad3f3d67b74bb4b9b4457f1ccb99ea3dad02ad24c0f4d2383ff1fd12f7c0a684": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402f20200ffffffff02902f500900000000160014ea779f0f8e203034ae25cd71e52c1a81ac488e2b0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91": "cHNidP8BAM4CAAAAAUxP/9q+TSeosGYYSSpf6Cg0VVGz2oEHuMraX0UplWUsAAAAAAD9////BegDAAAAAAAAFgAUHJ2Hucc2GI5sonbAeh3TaBHHeApBnAAAAAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8UkTiEgAAAAAWABR+Ga8pbyXQkvI6DCCII+ZcgaKvnZdoEBQAAAAAFgAUjh/DmWHFR4f917XPPlnAlblzT7y6eqcUAAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8wKJObAABAR+2yJo7AAAAABYAFH77QT1b74D8A9KpXC1rH06KZ0UgAQC/AgAAAAABAd8W8LQ4gBPr4BMdIIFgxnlK/0pm2Hv7VRK0tdMKsspOAAAAAAD9////AbbImjsAAAAAFgAUfvtBPVvvgPwD0qlcLWsfTopnRSACRzBEAiBiDWmocrmu+1Hip22YEAiBWHdkSIwuaYOqolHI5yjlJwIgaVVzZF7VDoudB+M8a9n375i5hXF+w+RnR2RYs+oKnUgBIQMAYJEwf6yH90elPJ15yFBJZBFMuZDDwEIcyvu85pUc5uYIAAAiBgMlkmiTZPOkeW/DWh8YN5HrfF2yXm82+g8ekmVO1nnHkRBZSzQGAAAAgAEAAAARAAAAAAAAAAAA", "ae8e62c0ec616466782a4f861d108ad06e593af6c8a4fe3d2c9ca1386fd559bd": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402200100ffffffff0200f9029500000000160014d2b8a4b410689315e81b354969e36584451f85530000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "af9e9fbb8e65d724bbf483ed4b7577ff183b0e8ddba91ca40456d43b3135f81e": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff04023a0100ffffffff02807c814a00000000160014b2da19ab366b4a2f09d6dd233192d9339444ca980000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", "b00e9f65ca32f0ab7a05666f21a78b461623c43d1297a731f09a807f2c9dc497": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff0402850300ffffffff02aa19a8040000000016001439690f010cb6ee0ab0180e01d1a9f6b3417af06b0000000000000000266a24aa21a9edb6b7b0b5bded40786dbf759cf9af7726000596503854971f2cc19c3fed02f4a10120000000000000000000000000000000000000000000000000000000000000000000000000", @@ -5390,6 +5497,11 @@ false, 1 ], + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa": [ + 110, + true, + 1 + ], "0b2d3c47e2f34a34baf07ed83308aab05101b3afd3a0494167e6db0c46bb7bf8": [ null, false, @@ -5555,6 +5667,11 @@ false, 1 ], + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c": [ + 110, + true, + 1 + ], "2c6d3a41eb0f5589e2586138b2bee1cf453144fe48dc73c2a3b0f4d0a3bfd596": [ null, false, @@ -5625,6 +5742,11 @@ false, 1 ], + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4": [ + null, + false, + 7 + ], "399231ca4ba596e68131aeb0b7c5e721c7b90a4e7c27e4848487fb0453c27d2e": [ null, false, @@ -5700,6 +5822,11 @@ true, 2 ], + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df": [ + 110, + true, + 1 + ], "506e0e953ba217afd18465980c16438947d86ebd74959c689b3b53d1dc2fe2d5": [ null, false, @@ -6225,6 +6352,11 @@ false, 1 ], + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91": [ + 234, + true, + 1 + ], "ae8e62c0ec616466782a4f861d108ad06e593af6c8a4fe3d2c9ca1386fd559bd": [ null, false, @@ -6733,6 +6865,11 @@ "fb650d0b87814550b9cc20e8fdfc72198bb13bbecd74ad29d9849aab039d0256:1": 198800 } }, + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa": { + "bcrt1qxh5zr72vgglpksts43dad67ra6kzxcjyj6dkq2": { + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4:1": 1000000000 + } + }, "0fb9da0e14c71d8b85bf00d23fa3b599972ea48f94e567e0f9733ed7357ba686": { "bcrt1qazle627r46apscly8lj4q5cxfxgrtuew879jde": { "f1c2d97b87ec7ed1b58e8b2b70dd3263e958e6b02a45477ef9b2c5f98f53df25:1": 68897364936 @@ -6754,6 +6891,11 @@ "335d73d9f9295b283a07187652a587830eb1eb2ee2829ef566dde8445946e474:2": 137794495414 } }, + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c": { + "bcrt1qw8l4cqjgkrkj8dqtq8pmz343f8r0ye4aldvvgm": { + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df:0": 999999780 + } + }, "2cc443dfe5831031e0d5ccd82ee269d80ae8e0944b578c6acdb784205c943033": { "bcrt1qh2c83yulvs7kgw0g6q3lkxqws4cnf0uxpcgcpt": { "065cac0bb30c763b66c25e890be537589f24cb90219bf30da96a5d37e051e052:1": 200000 @@ -6780,6 +6922,11 @@ "5a5a803fa15d65a9cbfa7f72622af5d43e1027cd15ce33ded0fb7dd84d5aa0fa:1": 200000 } }, + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df": { + "bcrt1qemzatwg2gwvznm8u5jg4pau79x2htt8wus8usd": { + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa:0": 999999890 + } + }, "581c27a00de5773b917c838e7060519b4502a40522e3fb846a8ded340d1f72e8": { "bcrt1qad5z7m5w6fqn43etjr4c8fkcd2s87dsmkkkk5v": { "030a1ef891f4a7179a5d050fb5160c361ffe7a5ba72211e4181de177224ea25c:0": 625000000 @@ -7248,6 +7395,11 @@ "88a56990a69eb1a7c11595b7a88097d799fb6070bb2f77ea34e7f2fe8d54d867:1": 100000 } }, + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91": { + "bcrt1q0ma5z02ma7q0cq7j49wz66clf69xw3fq2zl05n": { + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c:0": 999999670 + } + }, "b020ccbf483abab08d6553b7fec974ce9f8ab43654863757e3ec7694c399f86c": { "bcrt1qazle627r46apscly8lj4q5cxfxgrtuew879jde": { "335d73d9f9295b283a07187652a587830eb1eb2ee2829ef566dde8445946e474:1": 68897247707 @@ -7641,6 +7793,14 @@ ] } }, + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa": { + "bcrt1qemzatwg2gwvznm8u5jg4pau79x2htt8wus8usd": { + "0": [ + 999999890, + false + ] + } + }, "0b2d3c47e2f34a34baf07ed83308aab05101b3afd3a0494167e6db0c46bb7bf8": { "bcrt1qakp5mpe00cl4a2k2kn79yjwlv58xymaxwft77d": { "0": [ @@ -7917,6 +8077,14 @@ ] } }, + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c": { + "bcrt1q0ma5z02ma7q0cq7j49wz66clf69xw3fq2zl05n": { + "0": [ + 999999670, + false + ] + } + }, "2c6d3a41eb0f5589e2586138b2bee1cf453144fe48dc73c2a3b0f4d0a3bfd596": { "bcrt1qu8f3htg9wrv02uq723p8xej4m4hetjq9rjd4xs": { "0": [ @@ -8035,6 +8203,14 @@ ] } }, + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4": { + "bcrt1qxh5zr72vgglpksts43dad67ra6kzxcjyj6dkq2": { + "1": [ + 1000000000, + false + ] + } + }, "399231ca4ba596e68131aeb0b7c5e721c7b90a4e7c27e4848487fb0453c27d2e": { "bcrt1qd8mjyvldlnu8vutdcd5edcfdfdkme8gw2q29mk": { "0": [ @@ -8161,6 +8337,14 @@ ] } }, + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df": { + "bcrt1qw8l4cqjgkrkj8dqtq8pmz343f8r0ye4aldvvgm": { + "0": [ + 999999780, + false + ] + } + }, "506e0e953ba217afd18465980c16438947d86ebd74959c689b3b53d1dc2fe2d5": { "bcrt1qu5a54g2mvz4ywfvzehk6spkd7j8m7lv4svnms5": { "0": [ @@ -9924,6 +10108,12 @@ 0, "7258a88cbd16231d88af0122bb53d7f538c526000fcee56a2dd5d3f17faaa268" ], + "0a9743ce1cd227e12b247204349c7e5f801059806543e91fcbd1c6c8e70159aa": [ + 2250, + 1785450817, + 1, + "3318530796a1aa5f3c22ae8f838a2dbcc08fe6665108c2072063ee71bba7a284" + ], "0b2d3c47e2f34a34baf07ed83308aab05101b3afd3a0494167e6db0c46bb7bf8": [ 2006, 1784292077, @@ -10122,6 +10312,12 @@ 0, "5f614ad1d66044ce3d5421b2caed5355a623881bf72508a6c646e63b2a820931" ], + "2c659529455fdacab80781dab351553428e85f2a491866b0a8274dbedaff4f4c": [ + 2279, + 1785528330, + 1, + "4a23ab804d195b0d49c3d78424eb9b9bcac59e0769f5139c3011335722b32efd" + ], "2c6d3a41eb0f5589e2586138b2bee1cf453144fe48dc73c2a3b0f4d0a3bfd596": [ 347, 1761910321, @@ -10206,6 +10402,12 @@ 0, "73f2e05e902b298c0e9430092bca5eac244acdc97771cffc22596cc057b275ad" ], + "380302d4d042b0d3b6b0896e98b1dbccabedc24734dada0ff7353732a61c15f4": [ + 2248, + 1785449617, + 1, + "7b0ceb478f225f6219d72a2158526f597e91225b34d78f4cdd74e80d6310d899" + ], "399231ca4ba596e68131aeb0b7c5e721c7b90a4e7c27e4848487fb0453c27d2e": [ 309, 1761910244, @@ -10296,6 +10498,12 @@ 1, "2ca4a9be0d60efc96abbab8b5ff4e90d4934f54db1914a1f71d3552db2a2d1b4" ], + "4ecab20ad3b5b41255fb7bd8664aff4a79c66081201d13e0eb138038b4f016df": [ + 2266, + 1785460417, + 1, + "5de1c1885359854fedf91e6a26a4859a2780978e5a1f3678311ddf2a2ef45c67" + ], "506e0e953ba217afd18465980c16438947d86ebd74959c689b3b53d1dc2fe2d5": [ 378, 1761910383, @@ -11530,6 +11738,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -11573,6 +11782,8 @@ 20000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Updated.Replaced.Invalidated", "time": 1781960175.6909134, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff05204e000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc9a0ab5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d445bc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc445bc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc02473044022036cb123a7272c69e1bb485912617d0ae587c01fbbbeba14a2d77803a6119ca5c022068db54ab23b601f494c31c6922fcda7b5074d7d20510c4a32f2acde3df8d5039012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3404cbc6b", @@ -11608,6 +11819,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -11644,6 +11856,8 @@ 34539289663 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Replaced.Invalidated", "time": 1781888638.550394, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff043fbcb30a080000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc6660ecdd0c0000001600147e19af296f25d092f23a0c208823e65c81a2af9d6c26cbab0d0000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc6c26cbab0d0000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402206cbd2596c9f7f855e7f64338b8e098c33da609b01e124f4ba56083536a354992022075b419be3b8fe981ecea5281ef007eeb49b688d50cf6147186c3db1b444de43b012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c040c06b", @@ -11662,6 +11876,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": false, @@ -11699,6 +11914,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Updated.Firmato.Invalidated", "time": 1781968114.0018213, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff04419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc7a0eb5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d625fc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc625fc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220352e03b4c6bd7659078562b7a38e6fe0110995a14d7c1a8752de590c10cf7b7702202899f837837a63c697024e1c88fa77c868c9c71fd41c3f39ed9cfbce0fcc18bc012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c0f2d26a", @@ -11717,6 +11934,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -11760,6 +11978,8 @@ 30000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Updated.Replaced.Invalidated", "time": 1781962486.7599566, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff05214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc3075000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d1afeb4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9dfc4dc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcfc4dc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402204806fcb89b45a541d7a48355beece24af53ce4a0d40c94390929e8bfd393a9b5022000f1d4d4ad4bed3d323773f8bff93481c1a125cfa1e67d737c7e83519be6fdf8012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3404cbc6b", @@ -11786,6 +12006,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -11823,6 +12044,8 @@ 20000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Replaced.Invalidated", "time": 1781960091.7051795, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff04214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc7a27b5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9df279c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcf279c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402204da23c14a00c8c13adbfafb9e8c709841a0dc0c6ae5a8f7e83d5b5d8765c73a002200c0488fd746623633072628b3d09e78b44ed845f4b50bacbe92c2ba3e2177cd2012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c09dbd6b", @@ -11841,6 +12064,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -11884,6 +12108,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Updated.Firmato.Push failed.Replaced.Invalidated", "time": 1781964056.280334, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff05214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d9af1b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9db440c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcb440c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220340774663c70c09f523e56049fa09d189dea98faf701df8eaa59ae6b8144be8c022028e4546176a6445c5ed6b4573c78dd15aa7ac04c3a177e6c00f40b1821237147012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340bc3d6b", @@ -11915,6 +12141,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": false, @@ -11952,6 +12179,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Updated.Firmato.Invalidated", "time": 1781968538.3178709, "tx": "02000000000101d048cc969bc0a6ecea52609029b4c1c547afcf94ad0d7fb66166b15824e8fb3f0000000000fdffffff04419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcd010b5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9dde61c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcde61c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc02473044022076dd8e78312b62c0daa890d62eb70add3e5a6542afb65695eaa1e0aeeb042dad0220127037d614d01fdd9e3bf21a049931b6f1113d4521b06b7357b978b507ee0515012102b72cc9ba68640a476abe173da069c5d37a1091346d9242fd794b470d04a236cd4044d46a", @@ -11970,6 +12199,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12013,6 +12243,8 @@ 10000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Updated.Replaced.Invalidated", "time": 1781960130.4184484, "tx": "cHNidP8BAM4CAAAAATwyIk7fo395BQp+yWQkAi3cbJvLbRmda5/tvOr9l4OrAAAAAAD9////BRAnAAAAAAAAFgAU7TE18rdHWuOJsZ6gBUxSJGsDNW0hTgAAAAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8Ghe1cA8AAAAWABR+Ga8pbyXQkvI6DCCII+ZcgaKvnYxowGcQAAAAFgAUjh/DmWHFR4f917XPPlnAlblzT7yMaMBnEAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8QEy8awABAR/LuDZAMAAAABYAFGBQRfMEYDJh1QX/1tBRR+VvkLw1AQC/AgAAAAABAdBIzJabwKbs6lJgkCm0wcVHr8+UrQ1/tmFmsVgk6Ps/AAAAAAD9////Acu4NkAwAAAAFgAUYFBF8wRgMmHVBf/W0FFH5W+QvDUCRzBEAiBsSSpHFP2Gh940TxVArbX+kwXOWOx+h6QMC6uGd1BJ2AIgRAA9Mt59IRdg35PVtTzAiXxpRBbNtC5iFkrG8a3JpwkBIQK3LMm6aGQKR2q+Fz2gacXTehCRNG2SQv15S0cNBKI2zd0FAAAiBgPqVPo0IcmIzsGP4vghpv7ElJA+HX0ubSzmFtmxSKqQsxBZSzQGAAAAgAEAAAALAAAAAAAAAAAA", @@ -12046,6 +12278,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -12083,6 +12316,8 @@ 30000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Updated.Firmato.Replaced.Invalidated", "time": 1781964114.071311, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0431750000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcfa1ab5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9daa6cc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcaa6cc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc02473044022059045766338bac0862f8b3d70fd5f33682ad976df5d390cb7d6d241bd8839fe002203742da94d9a7145737288303030e1197fe38629d56840c48c220ab2061f2dda2012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c06a3c6b", @@ -12101,6 +12336,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -12138,6 +12374,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Updated.Firmato.Replaced.Invalidated", "time": 1781969519.6341054, "tx": "020000000001018d2e8abfce92293e83c639dd0371e5fecefefb7b980ed54a2239a01624892e640000000000fdffffff04419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc50f5b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9da644c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbca644c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc02473044022023cc71809c27b03f64d317dec85cb5be9291f12af999dbb33a7ca3271123e32b02200d78246d7fd23c857f408fc393f6e79a4459257ca601ada4630cac806e5bf49c012102ad2e7f599fec06e794dabe7d7c97100c8a16a5a8ee7a473e6b877629521ecae04092c16b", @@ -12156,6 +12394,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12199,6 +12438,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Replaced.Invalidated", "time": 1781962665.8778772, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff05214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d9af1b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9db440c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcb440c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402200e8ed269cd0443b9259ac666ca8a349f44aa4607a935fdf6f9bceebe1cc69bfd0220450cf86be10e9454abeb2815ed84960e1d45e5d489a43fe4c84bb2f943ae6975012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3404cbc6b", @@ -12225,6 +12466,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -12262,6 +12504,8 @@ 10000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Updated.Replaced.Invalidated", "time": 1781888681.1007006, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0411270000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcfa33b5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d3a87c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc3a87c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402207b3c0b9fb449501283229d37ab20fa8ecf1ba65835532ce570b62c98cee78ea002202813d74f280f9d04f90d91e87d97f772656fdcce20246f6fec59f7188e984aee012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340efbe6b", @@ -12280,6 +12524,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -12317,6 +12562,8 @@ 20000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Replaced.Invalidated", "time": 1781964056.280334, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff04214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc7a27b5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9df279c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcf279c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220195ddae3a7de4959daf7d89105692600d1e0164ce0fa535251ee06b47adb11780220046455aedf9b97c4ea38abc52c35798e5b194a7d971547b79fab7e1cd7b174f0012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340bc3d6b", @@ -12335,6 +12582,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -12372,6 +12620,8 @@ 20000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Updated.Firmato.Replaced.Invalidated", "time": 1781960130.4184484, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff04214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc7a27b5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9df279c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcf279c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220281673a776da91290720d87a3c23756097c865fc9bf332f343ed52a9bf7adebe0220689a09e1968a213475f0f96214f0ca49a8b52d167c104a9c1a1db369ef458e41012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3404cbc6b", @@ -12390,6 +12640,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": false, @@ -12427,6 +12678,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Updated.Firmato.Invalidated", "time": 1781969271.933921, "tx": "02000000000101609438ba82251a8cf3acfd1f189369029290cf2f8793b2b8f3c4c363c8e6c1a30000000000fdffffff04419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc1003b5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d4253c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc4253c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402202f3bb3b9a7b540b553cd3b78d8ece4b5aaf9d2c47fd0077540eea803a063306e02202b43c2812e9deef1e2446a3e1e55745282881410060c41ee3c8fb59937a08e66012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340bc3d6b", @@ -12445,6 +12698,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12488,6 +12742,8 @@ 100000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Updated.Firmato.Push failed.Replaced.Invalidated", "time": 1781960091.7051795, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff05214e0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbca086010000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d9aa6b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d04f1bf67100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc04f1bf67100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220646865bede38ec96e9c7c02666bdb5a8807db670703e2549a227f97ec900f5e8022001e255b37bc5a09d170f2c74487cf1952a9a3a38a709da2c741ebff2933a1ecf012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c09dbd6b", @@ -12519,6 +12775,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12561,6 +12818,8 @@ 100000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Push failed.Replaced.Invalidated", "time": 1781888638.550394, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff05a086010000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d2079b30a080000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc00f5ebdd0c0000001600147e19af296f25d092f23a0c208823e65c81a2af9d50b4caab0d0000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc50b4caab0d0000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc02473044022036a3c5f2cb80a9118d1af8d9a25eb33092fff8e0e9ceda3055148366fc7aef8902206532d305fa20385516860c527b822e493aaac7930a33ec114639b7428ea5cfd4012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c040c06b", @@ -12592,6 +12851,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12635,6 +12895,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Updated.Firmato.Push failed.Replaced.Invalidated", "time": 1781968091.2853925, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0531750000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d1ae5b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d6c33c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc6c33c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc02473044022043294051a4a7eed0875f3737f0f4c74b3679a203c40e6ef54d740b6ff48e0ba10220521c2ebf00cecc6c756f60352e9842d9de757895728f1ec5a90ffad3693f64d5012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b34044d46a", @@ -12666,6 +12928,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12709,6 +12972,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Replaced.Invalidated", "time": 1781964114.071311, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0531750000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d1ae5b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d6c33c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc6c33c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220233d22dabe66850fcd5cea525646981cc7f3e24bafb7a0ead451bcb6b8345f1c02203e82f071e534f9379f6fc893914ac8fc2a129848f8ea460c752e409e96b33c71012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c06a3c6b", @@ -12740,6 +13005,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12783,6 +13049,8 @@ 100000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Replaced.Invalidated", "time": 1781888681.1007006, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0511270000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbca086010000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d1ab3b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d4cfebf67100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc4cfebf67100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc02473044022043491efd8be6c3d2d7b37af6823e7d5c2e0d15086e01cd5fe36935fc119674080220652135dadf0d85ad5d647e32ad7dd7e039fd8f35ed03d5ae8ff8cd0cb56ac9e9012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340efbe6b", @@ -12814,6 +13082,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12857,6 +13126,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Updated.Firmato.Push failed.Replaced.Invalidated", "time": 1781967421.724049, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0531750000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d1ae5b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d6c33c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc6c33c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220578abf0bcc0606c2ca89062daa5b59cf2a1adfb4732e166a0485c31c8b1b7e07022058e1f2baa2a4ccccfa460fb569c97df13930eb1d98a463eb8b9062e7b9989ba7012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340d1fb6a", @@ -12875,6 +13146,89 @@ "url": "https://we.bitcoin-after.life" } }, + "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91": { + "ANTICIPATED": false, + "BROADCASTED": false, + "CHECKED": false, + "CHECK_FAIL": false, + "COMPLETE": false, + "CONFIRMED": false, + "ERROR": false, + "EXPIRED": false, + "EXPORTED": false, + "IMPORTED": false, + "INVALIDATED": true, + "MEMPOOL": false, + "PARTIALLY_SIGNED": false, + "PUSHED": false, + "PUSH_FAIL": false, + "REPLACED": false, + "RESTORED": false, + "UPDATED": false, + "VALID": false, + "_id": "adb13f7887359732c227e0d0523090de7d6cef7e7c73fb3b0a8b67844efbcd91", + "baltx_fees": 1, + "change": null, + "description": "w!ll3x3c\"http://localhost:9133\"1817092800\nmario2\naaaa\nlucia\nmario", + "heirs": { + "aaaa": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + "34%", + "1y", + 336619671 + ], + "lucia": [ + "bcrt1q0cv672t0yhgf9u36pssgsglxtjq69tuakdzy2j", + "32%", + "1y", + 316818514 + ], + "mario": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + "35%", + "1y", + 346520250 + ], + "mario2": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + 40000, + "1y", + 40001, + 40000 + ], + "w!ll3x3c\"http://localhost:9133\"1817092800": [ + "bcrt1qrjwc0ww8xcvgum9zwmq858wndqguw7q27ek3sk", + 1000, + 1817092800, + 1000 + ] + }, + "sigs_have": 0, + "sigs_required": 1, + "status": "New.Invalidated", + "time": 1785562346.7776835, + "tx": "cHNidP8BAM4CAAAAAUxP/9q+TSeosGYYSSpf6Cg0VVGz2oEHuMraX0UplWUsAAAAAAD9////BegDAAAAAAAAFgAUHJ2Hucc2GI5sonbAeh3TaBHHeApBnAAAAAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8UkTiEgAAAAAWABR+Ga8pbyXQkvI6DCCII+ZcgaKvnZdoEBQAAAAAFgAUjh/DmWHFR4f917XPPlnAlblzT7y6eqcUAAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8wKJObAABAR+2yJo7AAAAABYAFH77QT1b74D8A9KpXC1rH06KZ0UgAQC/AgAAAAABAd8W8LQ4gBPr4BMdIIFgxnlK/0pm2Hv7VRK0tdMKsspOAAAAAAD9////AbbImjsAAAAAFgAUfvtBPVvvgPwD0qlcLWsfTopnRSACRzBEAiBiDWmocrmu+1Hip22YEAiBWHdkSIwuaYOqolHI5yjlJwIgaVVzZF7VDoudB+M8a9n375i5hXF+w+RnR2RYs+oKnUgBIQMAYJEwf6yH90elPJ15yFBJZBFMuZDDwEIcyvu85pUc5uYIAAAiBgMlkmiTZPOkeW/DWh8YN5HrfF2yXm82+g8ekmVO1nnHkRBZSzQGAAAAgAEAAAARAAAAAAAAAAAA", + "willexecutor": { + "address": "bcrt1qrjwc0ww8xcvgum9zwmq858wndqguw7q27ek3sk", + "balance": 90000, + "base_fee": 1000, + "chain": "regtest", + "count_win": 0, + "id": 66, + "info": "BAL devel willexecutor server", + "last_block": 0, + "last_update": 1785557736.334134, + "onion_url": null, + "points": 0, + "promo_code": null, + "selected": true, + "status": 200, + "tld": "localhost", + "unconfirmed_balance": 0, + "url": "http://localhost:9133", + "version": "0.3.2" + } + }, "b708916e8f8ab4562bb415f6876276e3901231649a9ed7f2d22ed0bae65cf0b9": { "ANTICIPATED": true, "BROADCASTED": false, @@ -12888,6 +13242,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -12931,6 +13286,8 @@ 10000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Replaced.Invalidated", "time": 1781960130.4184484, "tx": "cHNidP8BAM4CAAAAATwyIk7fo395BQp+yWQkAi3cbJvLbRmda5/tvOr9l4OrAAAAAAD9////BRAnAAAAAAAAFgAU7TE18rdHWuOJsZ6gBUxSJGsDNW0hTgAAAAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8Ghe1cA8AAAAWABR+Ga8pbyXQkvI6DCCII+ZcgaKvnYxowGcQAAAAFgAUjh/DmWHFR4f917XPPlnAlblzT7yMaMBnEAAAABYAFI4fw5lhxUeH/de1zz5ZwJW5c0+8QEy8awABAR/LuDZAMAAAABYAFGBQRfMEYDJh1QX/1tBRR+VvkLw1AQC/AgAAAAABAdBIzJabwKbs6lJgkCm0wcVHr8+UrQ1/tmFmsVgk6Ps/AAAAAAD9////Acu4NkAwAAAAFgAUYFBF8wRgMmHVBf/W0FFH5W+QvDUCRzBEAiBsSSpHFP2Gh940TxVArbX+kwXOWOx+h6QMC6uGd1BJ2AIgRAA9Mt59IRdg35PVtTzAiXxpRBbNtC5iFkrG8a3JpwkBIQK3LMm6aGQKR2q+Fz2gacXTehCRNG2SQv15S0cNBKI2zd0FAAAiBgPqVPo0IcmIzsGP4vghpv7ElJA+HX0ubSzmFtmxSKqQsxBZSzQGAAAAgAEAAAALAAAAAAAAAAAA", @@ -12963,6 +13320,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -13000,6 +13358,8 @@ 30000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Replaced.Invalidated", "time": 1781968091.2853925, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0431750000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcfa1ab5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9daa6cc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcaa6cc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402207c5a4fe19fc15204abc89058ce988eeb499d52b906a51f6347d69581903b2b0c022001c7b4396bcd9b8ff0bd13bd9014468d75fd140a19703c0876f91912b9596104012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b34044d46a", @@ -13018,6 +13378,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -13055,6 +13416,8 @@ 30000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Replaced.Invalidated", "time": 1781967421.724049, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff0431750000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcfa1ab5700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9daa6cc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcaa6cc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402207dc67b956e892ae5a44a0d015501f8e0f39f6a6987e750a77ee6bc73a6fc3d7f02205d358971ecf087d7d9e8c424f63e3c5fa0c17b52b31b88a243d4aa9d8cd2f1f8012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340d1fb6a", @@ -13073,6 +13436,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": false, @@ -13116,6 +13480,8 @@ 100000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Invalidated", "time": 1782556752.5544994, "tx": "020000000001018d2e8abfce92293e83c639dd0371e5fecefefb7b980ed54a2239a01624892e640000000000fdffffff05419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbca086010000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d65d491490f0000001600147e19af296f25d092f23a0c208823e65c81a2af9dabf12a3e100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc4e8077b8100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220459f471c2befd1bd528e5059675f12aba5d2117d8d3bd5005aca401cab307aa002201e50c4b3da8f15c62e33f9fd9cd99645046b4eb0348d77beeeac6c1f929a3568012102ad2e7f599fec06e794dabe7d7c97100c8a16a5a8ee7a473e6b877629521ecae0c040c06b", @@ -13149,6 +13515,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": false, "REPLACED": true, @@ -13192,6 +13559,8 @@ 100000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Replaced.Invalidated", "time": 1782556767.004284, "tx": "020000000001018d2e8abfce92293e83c639dd0371e5fecefefb7b980ed54a2239a01624892e640000000000fdffffff05419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbca086010000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d65d491490f0000001600147e19af296f25d092f23a0c208823e65c81a2af9dabf12a3e100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc4e8077b8100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220381a8ccc9edba0d8b8d304493103639e58a4c19d79525976ab09f990f8c104440220720b86e4c80b61068195dd62f0e28d4cd11793b2683acacfeb2b507e5b7fa360012102ad2e7f599fec06e794dabe7d7c97100c8a16a5a8ee7a473e6b877629521ecae0407e206c", @@ -13219,6 +13588,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": false, @@ -13262,6 +13632,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Push failed.Invalidated", "time": 1781968538.3178709, "tx": "02000000000101d048cc969bc0a6ecea52609029b4c1c547afcf94ad0d7fb66166b15824e8fb3f0000000000fdffffff05409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbcf0dab4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9da028c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbca028c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402205df4e886280853547a595e4aebf4bc04fe71c6b0e5b81f765492637183e3e5b902202deae6e1b3707f4a9242638509af4411f1347bf6e2c8c8f02308880f338f9c09012102b72cc9ba68640a476abe173da069c5d37a1091346d9242fd794b470d04a236cd4044d46a", @@ -13293,6 +13665,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": false, @@ -13336,6 +13709,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Anticipated.Firmato.Push failed.Invalidated", "time": 1781968114.0018213, "tx": "020000000001013c32224edfa37f79050a7ec96424022ddc6c9bcb6d199d6b9fedbceafd9783ab0000000000fdffffff05409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc9ad8b4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d2426c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc2426c067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc024730440220046f713d5ff3427a3839a54fad776840265d32a008c0132dd067eb925b42d9a30220386a1bd590869e913898856c81df9a1aa1e2ea1fb11f76d418324b3f6ba770b2012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b3c0f2d26a", @@ -13367,6 +13742,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": false, @@ -13410,6 +13786,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Push failed.Invalidated", "time": 1781969271.933921, "tx": "02000000000101609438ba82251a8cf3acfd1f189369029290cf2f8793b2b8f3c4c363c8e6c1a30000000000fdffffff05409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc30cdb4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d041ac067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc041ac067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402207723e0a35a030b4032c5961482c14dfa4bc6c7c09e71d83e5b487742825100eb02205f3f81b10a592fc367b8bf3ed055d4d8849413c8d4c16cd89a555f5186bd9fbc012103ea54fa3421c988cec18fe2f821a6fec494903e1d7d2e6d2ce616d9b148aa90b340bc3d6b", @@ -13441,6 +13819,7 @@ "IMPORTED": false, "INVALIDATED": true, "MEMPOOL": false, + "PARTIALLY_SIGNED": false, "PUSHED": false, "PUSH_FAIL": true, "REPLACED": true, @@ -13484,6 +13863,8 @@ 40000 ] }, + "sigs_have": 0, + "sigs_required": 0, "status": "New.Firmato.Replaced.Push failed.Invalidated", "time": 1781969519.6341054, "tx": "020000000001018d2e8abfce92293e83c639dd0371e5fecefefb7b980ed54a2239a01624892e640000000000fdffffff05409c000000000000160014ed3135f2b7475ae389b19ea0054c52246b03356d419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc70bfb4700f0000001600147e19af296f25d092f23a0c208823e65c81a2af9d680bc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc680bc067100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402206fc53acf1f67198ce0b36e5624e8d509f301ab4729e9e0776b59ea7afce75322022050a99933c34b6189266b4e234b48368fe9178caf915c63f21aa720fe5b7798bf012102ad2e7f599fec06e794dabe7d7c97100c8a16a5a8ee7a473e6b877629521ecae04092c16b", diff --git a/tests/samanta7 b/tests/samanta7 index f7372d3..4effc3f 100644 --- a/tests/samanta7 +++ b/tests/samanta7 @@ -65,7 +65,8 @@ "bcrt1qw3m44vlmx08xzpnus22yfa8k5exnq9wk9uncrl": [], "bcrt1qwhjyltkpkyqj2k8zd6zjq2uu49tpu03f4sm7j0": [], "bcrt1qygktltj8sjkus96k0atzgr8srcy658axnpj82x": [], - "bcrt1qyjpmlr7qehqedzhkzsx6xrvr9ey6455yur2pqx": [] + "bcrt1qyjpmlr7qehqedzhkzsx6xrvr9ey6455yur2pqx": [], + "bcrt1qyx3e4qwguyr70g9wdva4dgpp2pdpauj9dht5ga": [] }, "addresses": { "change": [ @@ -80,7 +81,8 @@ "bcrt1q59nyxchtw4eajltn97sds6whdj4p2rg6uaygfq", "bcrt1qw3ljqdscx64qk9lxke3evz8scukj4zf8l5mptt", "bcrt1qrkwdul9mws0hu298hvtd5usce9haypwmt8zhts", - "bcrt1qw3m44vlmx08xzpnus22yfa8k5exnq9wk9uncrl" + "bcrt1qw3m44vlmx08xzpnus22yfa8k5exnq9wk9uncrl", + "bcrt1qyx3e4qwguyr70g9wdva4dgpp2pdpauj9dht5ga" ], "receiving": [ "bcrt1qkxnsj8xymkk4mahea5ry8lgpau0w5dqgs0ezxc", @@ -121,22 +123,22 @@ "aaaa": [ "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", "34%", - "5y" + 1813204800 ], "lucia": [ "bcrt1q0cv672t0yhgf9u36pssgsglxtjq69tuakdzy2j", "32%", - "5y" + 1813204800 ], "mario": [ "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", "35%", - "5y" + 1813204800 ], "mario2": [ "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", 40000, - "5y" + 1813204800 ] }, "imported_channel_backups": {}, @@ -152,6 +154,7 @@ "xpub": "vpub5VpWCxVNP1qcP3aDcBMryrhaCECvFte8T3eT8BAUSGW4LuHoVEUNaKKPGsuN8H2MFTMmtZHnokVMB6c46MFiCzyYHwmsgAfr5B9DtFAEtjo" }, "labels": { + "11a0107ec7ffb703ca32d1f558007dc64539eb9d8287e77f81fd7380f258dddd": "BAL Inheritance transaction", "4c955222d31ed19318841b7f3dd0e95edd6198ebddc0dbea1c249c38c609aeac": "BAL Invalidate transaction", "5833dd0e77cc9e22d981b92e71e5f5804deb5294dfa4dc666cfe4fa015afee8d": "BAL Inheritance transaction", "5ca07e947509bea249d18591358c13a9c55e02b897390bab7ce0d2c6b2b394ec": "BAL Inheritance transaction", @@ -161,6 +164,7 @@ "a45935c22968fe56dfdc329420336682cebfce0691ed764ab5891199ed91a158": "BAL Inheritance transaction", "a526a6c1a74951df5bf5845f64dc97c3c9410935e2d05f6622a8f4ba31ec6811": "BAL Inheritance transaction", "b45102630bd6f7b8774dff3ed39b393097e7eb6ae4714716f6e925497db4d6d9": "BAL Inheritance transaction", + "dee1233628873d01de73cf0d8eabc19375f9a1713408ec9d5ef0661bd03a7519": "BAL Inheritance transaction", "e89b2329d4fe2b9349739b54720404f22d6b49ce94c297ce793f5e4e1f51672a": "BAL Invalidate transaction", "f54f6c6a4bf6820b428ca5f6b7200545dacfd5e671e001c524888e502d007eaa": "BAL Invalidate transaction" }, @@ -282,7 +286,7 @@ "0": "7cb49f910a6db5d95436943456293aefbcccf0bbcc78b1dfe0adec430997fe07" } }, - "stored_height": 2249, + "stored_height": 2336, "submarine_swaps": {}, "transactions": { "4c955222d31ed19318841b7f3dd0e95edd6198ebddc0dbea1c249c38c609aeac": "0200000000010108d37b8e201da3203b47c7cc33fdf21a55b9cf776fdf5d1a028cd250e953d9d20000000000fdffffff01ef56e44130000000160014f0df9afc9fdd8ec20c46eb6af56d3c4934063343024730440220625a4a9944af68bd2b4c4146ae9db95f052725b8ca4fc8e8fca541d1c863d37a022004a705b8974e5fea3707acd0d8cc5f9a9c898a508fd0ae348b81043c4a86ad0b0121021e753044de0b2d3894751fae6e62c0627d379752809e522a5d0d4e3689e4ba9fa9080000", @@ -393,6 +397,91 @@ }, "wallet_type": "standard", "will": { + "11a0107ec7ffb703ca32d1f558007dc64539eb9d8287e77f81fd7380f258dddd": { + "ANTICIPATED": false, + "BROADCASTED": false, + "CHECKED": true, + "CHECK_FAIL": false, + "COMPLETE": true, + "CONFIRMED": false, + "ERROR": false, + "EXPIRED": false, + "EXPORTED": false, + "IMPORTED": false, + "INVALIDATED": false, + "MEMPOOL": false, + "PUSHED": true, + "PUSH_FAIL": false, + "REPLACED": false, + "RESTORED": false, + "UPDATED": false, + "VALID": true, + "_id": "11a0107ec7ffb703ca32d1f558007dc64539eb9d8287e77f81fd7380f258dddd", + "baltx_fees": 1, + "change": null, + "description": "w!ll3x3c\"http://localhost:9133\"1813204800\nmario2\naaaa\nlucia\nmario", + "heirs": { + "aaaa": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + "34%", + 1813204800, + 69771996601 + ], + "lucia": [ + "bcrt1q0cv672t0yhgf9u36pssgsglxtjq69tuakdzy2j", + "32%", + 1813204800, + 65667761507 + ], + "mario": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + "35%", + 1813204800, + 71824114148 + ], + "mario2": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + 40000, + 1813204800, + 40001, + 40000 + ], + "w!ll3x3c\"http://localhost:9133\"1813204800": [ + "bcrt1qrjwc0ww8xcvgum9zwmq858wndqguw7q27ek3sk", + 1000, + 1813204800, + 1000 + ] + }, + "status": "New.Firmato.Pushed.Checked", + "time": 1785536273.0165532, + "tx": "0200000000010107fe970943ecade0dfb178ccbbf0ccbcef3a295634943654d9b56d0a919fb47c0000000000fdffffff05e8030000000000001600141c9d87b9c736188e6ca276c07a1dd36811c7780a419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc63851a4a0f0000001600147e19af296f25d092f23a0c208823e65c81a2af9db92dbc3e100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbce4010db9100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402201ce57302608a734f3de1b0d3b0eca08efb78f0d886180e7adf4897bfe4b4a49302202120e6f4308cef294094fa347ae7369eb0b0bb9b1bb64c8f03f50b349f01d7e1012102c5e7ef3fc3902156154cd1a2238e80bc15f1bb253f3c973b3b87f9ed6fe1c7b7404f136c", + "willexecutor": { + "address": "bcrt1qrjwc0ww8xcvgum9zwmq858wndqguw7q27ek3sk", + "balance": 90000, + "base_fee": 1000, + "broadcast_status": "Riuscito", + "chain": "regtest", + "count_win": 0, + "id": 66, + "info": "BAL devel willexecutor server", + "last_block": 0, + "last_update": 1785463888.38115, + "onion_url": null, + "points": 0, + "promo_code": null, + "selected": true, + "status": 200, + "tld": "localhost", + "txs": "0200000000010107fe970943ecade0dfb178ccbbf0ccbcef3a295634943654d9b56d0a919fb47c0000000000fdffffff05e8030000000000001600141c9d87b9c736188e6ca276c07a1dd36811c7780a419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc63851a4a0f0000001600147e19af296f25d092f23a0c208823e65c81a2af9db92dbc3e100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbce4010db9100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402201ce57302608a734f3de1b0d3b0eca08efb78f0d886180e7adf4897bfe4b4a49302202120e6f4308cef294094fa347ae7369eb0b0bb9b1bb64c8f03f50b349f01d7e1012102c5e7ef3fc3902156154cd1a2238e80bc15f1bb253f3c973b3b87f9ed6fe1c7b7404f136c\n", + "txsids": [ + "11a0107ec7ffb703ca32d1f558007dc64539eb9d8287e77f81fd7380f258dddd" + ], + "unconfirmed_balance": 0, + "url": "http://localhost:9133", + "version": "0.3.2" + } + }, "6ad1383bb5d358c4b7bcebbf009d642e7a78b923cf3b764b196904c68ffa116d": { "ANTICIPATED": false, "BROADCASTED": false, @@ -493,10 +582,10 @@ "MEMPOOL": false, "PUSHED": true, "PUSH_FAIL": false, - "REPLACED": false, + "REPLACED": true, "RESTORED": false, "UPDATED": false, - "VALID": true, + "VALID": false, "_id": "a45935c22968fe56dfdc329420336682cebfce0691ed764ab5891199ed91a158", "baltx_fees": 1, "change": null, @@ -534,7 +623,7 @@ 1000 ] }, - "status": "New.Firmato.Pushed.Checked", + "status": "New.Firmato.Pushed.Checked.Replaced", "time": 1785449918.3222272, "tx": "0200000000010107fe970943ecade0dfb178ccbbf0ccbcef3a295634943654d9b56d0a919fb47c0000000000fdffffff05e8030000000000001600141c9d87b9c736188e6ca276c07a1dd36811c7780a419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc63851a4a0f0000001600147e19af296f25d092f23a0c208823e65c81a2af9db92dbc3e100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbce4010db9100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402200b71f679baef994767c58cea234f2a2baa83cc029f64dae4ff6fe451aaffc369022051b2a70fd5c33fcc175d6dca4b052b4a7bc168c761434139560bee66ed965b18012102c5e7ef3fc3902156154cd1a2238e80bc15f1bb253f3c973b3b87f9ed6fe1c7b7c0cdd073", "willexecutor": { @@ -562,6 +651,91 @@ "url": "http://localhost:9133", "version": "0.3.2" } + }, + "dee1233628873d01de73cf0d8eabc19375f9a1713408ec9d5ef0661bd03a7519": { + "ANTICIPATED": false, + "BROADCASTED": false, + "CHECKED": true, + "CHECK_FAIL": false, + "COMPLETE": true, + "CONFIRMED": false, + "ERROR": false, + "EXPIRED": false, + "EXPORTED": false, + "IMPORTED": false, + "INVALIDATED": false, + "MEMPOOL": false, + "PUSHED": true, + "PUSH_FAIL": false, + "REPLACED": true, + "RESTORED": false, + "UPDATED": false, + "VALID": false, + "_id": "dee1233628873d01de73cf0d8eabc19375f9a1713408ec9d5ef0661bd03a7519", + "baltx_fees": 1, + "change": null, + "description": "w!ll3x3c\"http://localhost:9133\"1817006400\nmario2\naaaa\nlucia\nmario", + "heirs": { + "aaaa": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + "34%", + "1y", + 69771996601 + ], + "lucia": [ + "bcrt1q0cv672t0yhgf9u36pssgsglxtjq69tuakdzy2j", + "32%", + "1y", + 65667761507 + ], + "mario": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + "35%", + "1y", + 71824114148 + ], + "mario2": [ + "bcrt1q3c0u8xtpc4rc0lwhkh8nukwqjkuhxnaurd45ju", + 40000, + "1y", + 40001, + 40000 + ], + "w!ll3x3c\"http://localhost:9133\"1817006400": [ + "bcrt1qrjwc0ww8xcvgum9zwmq858wndqguw7q27ek3sk", + 1000, + 1817006400, + 1000 + ] + }, + "status": "New.Firmato.Pushed.Checked.Replaced", + "time": 1785529939.6807153, + "tx": "0200000000010107fe970943ecade0dfb178ccbbf0ccbcef3a295634943654d9b56d0a919fb47c0000000000fdffffff05e8030000000000001600141c9d87b9c736188e6ca276c07a1dd36811c7780a419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc63851a4a0f0000001600147e19af296f25d092f23a0c208823e65c81a2af9db92dbc3e100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbce4010db9100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402203e830a47f218826e2d1e4b708fdf4dc4a8826ac5c8a37a79fec5fbf1c6aa287002205df204067685f8874211ad092b0dad17a725f82a51e16f5c52db778135ecfbab012102c5e7ef3fc3902156154cd1a2238e80bc15f1bb253f3c973b3b87f9ed6fe1c7b740514d6c", + "willexecutor": { + "address": "bcrt1qrjwc0ww8xcvgum9zwmq858wndqguw7q27ek3sk", + "balance": 90000, + "base_fee": 1000, + "broadcast_status": "Riuscito", + "chain": "regtest", + "count_win": 0, + "id": 66, + "info": "BAL devel willexecutor server", + "last_block": 0, + "last_update": 1785463888.38115, + "onion_url": null, + "points": 0, + "promo_code": null, + "selected": true, + "status": 200, + "tld": "localhost", + "txs": "0200000000010107fe970943ecade0dfb178ccbbf0ccbcef3a295634943654d9b56d0a919fb47c0000000000fdffffff05e8030000000000001600141c9d87b9c736188e6ca276c07a1dd36811c7780a419c0000000000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc63851a4a0f0000001600147e19af296f25d092f23a0c208823e65c81a2af9db92dbc3e100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbce4010db9100000001600148e1fc39961c54787fdd7b5cf3e59c095b9734fbc0247304402203e830a47f218826e2d1e4b708fdf4dc4a8826ac5c8a37a79fec5fbf1c6aa287002205df204067685f8874211ad092b0dad17a725f82a51e16f5c52db778135ecfbab012102c5e7ef3fc3902156154cd1a2238e80bc15f1bb253f3c973b3b87f9ed6fe1c7b740514d6c\n", + "txsids": [ + "dee1233628873d01de73cf0d8eabc19375f9a1713408ec9d5ef0661bd03a7519" + ], + "unconfirmed_balance": 0, + "url": "http://localhost:9133", + "version": "0.3.2" + } } }, "winpos-qt": [ diff --git a/tests/test_core_will_extra.py b/tests/test_core_will_extra.py index 71fffaf..19f9234 100644 --- a/tests/test_core_will_extra.py +++ b/tests/test_core_will_extra.py @@ -10,12 +10,25 @@ Run: import os import sys +from binascii import unhexlify from unittest.mock import MagicMock, patch sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) -from electrum.transaction import Transaction +from electrum import crypto +from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL +from electrum.bitcoin import public_key_to_p2wpkh +from electrum.descriptor import parse_descriptor +from electrum.transaction import ( + PartialTransaction, + PartialTxInput, + PartialTxOutput, + Sighash, + Transaction, + TxOutpoint, +) +from bal.core.util import Util from bal.core.will import Will, WillItem _VALID_TX_HEX = ( @@ -171,6 +184,8 @@ def test_mempool_status_clears_valid(): def test_check_invalidated_invalidated(): wallet = MagicMock() wallet.get_tx_info.return_value.tx_mined_status.height.return_value = -1 + # The funding is really consumed by a broadcast tx -> the will is dead. + wallet.adb.get_spender.return_value = "ab" * 32 item = WillItem({"tx": _VALID_TX_HEX, "heirs": {"a": ["addr", 100, "30d"]}, "willexecutor": None, "status": "", "description": "", "time": 0, "change": "", "baltx_fees": 100}) @@ -186,6 +201,9 @@ def test_check_invalidated_invalidated(): def test_check_will(): wallet = MagicMock() wallet.get_tx_info.return_value.tx_mined_status.height.return_value = 0 + # No broadcast tx spends the funding: the missing UTXO is only a local + # (history) artifact, so the will is not invalidated by it. + wallet.adb.get_spender.return_value = None item = WillItem({"tx": _VALID_TX_HEX, "heirs": {"a": ["addr", 100, "30d"]}, "willexecutor": None, "status": "", "description": "", "time": 0, "change": "", "baltx_fees": 100}) @@ -197,6 +215,141 @@ def test_check_will(): assert item.get_status("MEMPOOL") is True +# ------------------------------------------------------------------ # +# WillItem signature counts + PARTIALLY_SIGNED status +# ------------------------------------------------------------------ # + +def _multisig_descriptor(): + """Return a 2-of-3 wsh multisig descriptor and its three pubkeys.""" + pubs = [] + for seed in (1, 2, 3): + pubs.append(crypto.privkey_to_pubkey(bytes([seed] * 32)).hex()) + return parse_descriptor("wsh(multi(2,{}))".format(",".join(pubs))), pubs + + +def _make_multisig_ptx(nsigs, locktime=None): + """A 2-of-3 wsh multisig PartialTransaction carrying ``nsigs`` signatures.""" + desc, pubs = _multisig_descriptor() + txin = PartialTxInput(prevout=TxOutpoint(b"\x11" * 32, 0), script_sig=b"") + txin.script_descriptor = desc + txin._trusted_value_sats = 100000 + txin.sighash = Sighash.ALL + sig = b"\x30\x44\x02\x20" + b"\x01" * 32 + b"\x02\x20" + b"\x02" * 32 + for i in range(nsigs): + txin.sigs_ecdsa[unhexlify(pubs[i])] = sig + addr = public_key_to_p2wpkh(bytes.fromhex(pubs[0])) + txout = PartialTxOutput.from_address_and_value(addr, 50000) + ptx = PartialTransaction() + if locktime is not None: + ptx.locktime = locktime + ptx.add_inputs([txin]) + ptx.add_outputs([txout]) + return ptx + + +def _make_multisig_willitem(nsig): + """A WillItem wrapping an unsigned 2-of-3 partial tx with ``nsig`` sigs. + + The script descriptor is re-attached after the WillItem round-trips the tx + through serialization (PSBT serialization drops the descriptor but keeps + the signatures), mirroring the real load-from-wallet flow. + """ + w = {"tx": _make_multisig_ptx(nsig), "heirs": {"a": ["addr", 100, "30d"]}, + "willexecutor": None, "status": "", "description": "", + "time": 0, "change": "", "baltx_fees": 100} + item = WillItem(w, _id="mswill") + item.tx.inputs()[0].script_descriptor = _multisig_descriptor()[0] + return item + + +def test_willitem_sigs_fields_roundtrip(): + item = _make_multisig_willitem(1) + assert item.sigs_required == 0 + assert item.sigs_have == 0 + item.sigs_required = 2 + item.sigs_have = 1 + item.set_status("PARTIALLY_SIGNED", True) + d = item.to_dict() + assert d["sigs_required"] == 2 + assert d["sigs_have"] == 1 + assert d["PARTIALLY_SIGNED"] is True + item2 = WillItem(d, _id="mswill") + assert item2.sigs_required == 2 + assert item2.sigs_have == 1 + assert item2.get_status("PARTIALLY_SIGNED") is True + + +def test_willitem_legacy_dict_defaults_sig_fields(): + # A will saved before the signature-tracking feature has no sig fields: + # they must default to 0 and the flag to False. + item = WillItem({"tx": _VALID_TX_HEX, "heirs": {"a": ["addr", 100, "30d"]}, + "willexecutor": None, "status": "", "description": "", + "time": 0, "change": "", "baltx_fees": 100}, _id="legacy") + assert item.sigs_required == 0 + assert item.sigs_have == 0 + assert item.get_status("PARTIALLY_SIGNED") is False + + +def test_willitem_partial_signed_keeps_valid(): + item = _make_multisig_willitem(1) + item.set_status("PARTIALLY_SIGNED", True) + assert item.get_status("PARTIALLY_SIGNED") is True + assert item.get_status("VALID") is True + assert "Partially Signed" in item.status + + +def test_willitem_complete_clears_partially_signed(): + item = _make_multisig_willitem(1) + item.set_status("PARTIALLY_SIGNED", True) + item.set_status("COMPLETE", True) + assert item.get_status("COMPLETE") is True + assert item.get_status("PARTIALLY_SIGNED") is False + + +def test_check_signatures_partial(): + item = _make_multisig_willitem(1) + Will.check_signatures({"mswill": item}) + assert item.sigs_have == 1 + assert item.sigs_required == 2 + assert item.get_status("PARTIALLY_SIGNED") is True + assert item.get_status("VALID") is True + + +def test_check_signatures_unsigned_not_partial(): + item = _make_multisig_willitem(0) + Will.check_signatures({"mswill": item}) + assert item.sigs_have == 0 + assert item.sigs_required == 2 + assert item.get_status("PARTIALLY_SIGNED") is False + + +def test_check_signatures_fully_signed_clears_flag(): + item = _make_multisig_willitem(2) + item.set_status("PARTIALLY_SIGNED", True) + Will.check_signatures({"mswill": item}) + assert item.get_status("PARTIALLY_SIGNED") is False + + +def test_check_signatures_complete_item_clears_flag(): + item = _make_multisig_willitem(1) + item.set_status("PARTIALLY_SIGNED", True) + item.set_status("COMPLETE", True) + Will.check_signatures({"mswill": item}) + assert item.get_status("PARTIALLY_SIGNED") is False + + +def test_check_signatures_single_sig_required(): + # A single-signature (P2WPKH) will needs exactly 1 signature: 0 present is + # "New", not "partially signed". + pub = crypto.privkey_to_pubkey(bytes([7] * 32)).hex() + item = _make_multisig_willitem(0) + item.tx.inputs()[0].script_descriptor = parse_descriptor("wpkh({})".format(pub)) + Will.check_signatures({"mswill": item}) + assert item.sigs_required == 1 + assert item.sigs_have == 0 + assert item.get_status("PARTIALLY_SIGNED") is False + + # ------------------------------------------------------------------ # # WillItem.__init__ with wallet # ------------------------------------------------------------------ # @@ -218,6 +371,472 @@ def test_willitem_init_without_wallet(): assert item is not None +# ------------------------------------------------------------------ # +# Will.save_valid_transactions_to_history (history persistence) +# ------------------------------------------------------------------ # + +class FakeTxMinedStatus: + def __init__(self, height): + self._height = height + + def height(self): + return self._height + + +class FakeTxInfo: + def __init__(self, height): + self.tx_mined_status = FakeTxMinedStatus(height) + + +class FakeWallet: + """Minimal stand-in for an Electrum wallet used by history persistence.""" + + def __init__(self, stored_txs=None, spenders=None, heights=None, outputs=None, + addresses=None): + self.adb = FakeADB( + stored_txs or {}, + spenders=spenders, + heights=heights, + outputs=outputs, + ) + self.db = self.adb.db + self.labels = {} + self.save_db_called = 0 + self.addresses = list(addresses or []) + + def set_label(self, txid, label): + if label is None: + self.labels.pop(txid, None) + else: + self.labels[txid] = label + + def get_all_labels(self): + return dict(self.labels) + + def save_db(self): + self.save_db_called += 1 + + def get_addresses(self): + return self.addresses + + def get_label_for_txid(self, txid): + return self.labels.get(txid, "") + + def get_tx_info(self, tx): + height = self.adb.heights.get(tx.txid(), TX_HEIGHT_LOCAL) + return FakeTxInfo(height) + + def get_utxos(self): + utxos = [] + for outs in self.adb.outputs.values(): + for utxo in outs.values(): + if utxo.spent_height is None: + utxos.append(utxo) + return utxos + + +class FakeADB: + def __init__(self, stored_txs, spenders=None, heights=None, outputs=None): + self.db = FakeDB(stored_txs) + self.added = [] + self.removed = [] + self.spenders = dict(spenders or {}) + self.heights = dict(heights or {}) + self.outputs = dict(outputs or {}) + + def add_transaction(self, tx, *, allow_unrelated=False, is_new=True): + self.added.append((tx, allow_unrelated)) + return True + + def remove_transaction(self, txid): + self.removed.append(txid) + + def get_spender(self, outpoint): + txid = self.spenders.get(outpoint) + if txid is None: + return None + height = self.heights.get(txid, TX_HEIGHT_LOCAL) + if height in (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE): + return None + return txid + + def get_tx_height(self, txid): + return FakeTxMinedStatus(self.heights.get(txid, TX_HEIGHT_LOCAL)) + + def get_addr_outputs(self, addr): + return self.outputs.get(addr, {}) + + +class FakeDB: + def __init__(self, stored_txs): + self.stored = dict(stored_txs) + + def get_transaction(self, txid): + return self.stored.get(txid) + + +def _make_simple_willitem(tx, valid=True, we_url=None): + """A WillItem wrapping *tx* with an optional VALID status and executor.""" + w = {"tx": tx, "heirs": {"a": ["addr", 100, "30d"]}, + "willexecutor": {"url": we_url} if we_url else None, "status": "", + "description": "", "time": 0, "change": "", "baltx_fees": 100, + "VALID": valid} + return WillItem(w, _id="wid") + + +def _exec_label(template, url): + return template.replace("{willexecutor}", url) + + +def test_save_incomplete_valid_tx_to_history_adds_and_labels(): + # A still-unsigned / partially-signed ("New") partial tx is stored in the + # local history, tagged with the decoded label. + tx = _make_multisig_ptx(1) + item = _make_simple_willitem(tx, valid=True, we_url="https://we.example") + wallet = FakeWallet() + Will.save_valid_transactions_to_history( + {"wid": item}, wallet, "BitcoinAfterLife inheritance transaction - {willexecutor}" + ) + assert [t.txid() for t, _ in wallet.adb.added] == [tx.txid()] + txid = tx.txid() + assert wallet.labels[txid] == "BitcoinAfterLife inheritance transaction - https://we.example" + assert wallet.save_db_called == 1 + + +def test_save_skips_complete_and_invalid_items(): + # A fully-signed ("Complete") tx must NOT be stored: it is removed from the + # local history instead. Invalid items are never touched. + complete = _make_multisig_willitem(2) + complete.set_status("VALID", True) + invalid = _make_simple_willitem(Transaction(_VALID_TX_HEX), valid=False) + new = _make_simple_willitem(_make_multisig_ptx(1), valid=True) + wallet = FakeWallet() + Will.save_valid_transactions_to_history( + {"complete": complete, "invalid": invalid, "new": new}, + wallet, + "BitcoinAfterLife inheritance transaction - {willexecutor}", + ) + assert [t.txid() for t, _ in wallet.adb.added] == [new.tx.txid()] + assert len(wallet.labels) == 1 + assert wallet.labels[new.tx.txid()] == ( + "BitcoinAfterLife inheritance transaction - " + ) + + +def test_save_combines_sigs_when_stored_partial(): + # The tx to save is already present in the wallet as an incomplete partial + # PSBT: the signatures are combined into it instead of blindly overwriting. + # (Exercised on raw PartialTransactions; through WillItem a complete tx + # round-trips to a plain Transaction, which overwrites instead - see + # test_save_complete_tx_overwrites_stored_partial.) + our = _make_multisig_ptx(2) + stored_partial = _make_multisig_ptx(1) + wallet = FakeWallet(stored_txs={our.txid(): stored_partial}) + Will._add_transaction_to_history(wallet, our, our.txid()) + assert len(wallet.adb.added) == 1 + saved, _ = wallet.adb.added[0] + # The combine path was taken (the stored partial was re-added, not our tx). + assert saved is stored_partial + assert saved.is_complete() + + +def test_save_removes_complete_item_from_history(): + # A fully-signed item is removed from the local history: its matching + # entry (exact label) is deleted, and it is never re-added. + our = _make_multisig_ptx(2) + txid = our.txid() + label = "BitcoinAfterLife inheritance transaction - https://we.example" + item = _make_simple_willitem(our, valid=True, we_url="https://we.example") + wallet = FakeWallet(stored_txs={txid: our}) + wallet.labels[txid] = label + Will.save_valid_transactions_to_history({"wid": item}, wallet, label) + assert wallet.adb.added == [] + assert txid in wallet.adb.removed + assert txid not in wallet.labels + + +def test_save_cleanup_removes_stale_exact_label(): + tx = _make_multisig_ptx(1) + txid = tx.txid() + stale_txid = "ab" * 32 + other_txid = "cd" * 32 + label = "BitcoinAfterLife inheritance transaction - https://we.example" + wallet = FakeWallet() + # Pre-existing wallet labels: one current, one stale (same label), one with + # a different executor URL that must be kept. + wallet.labels[txid] = label + wallet.labels[stale_txid] = label + wallet.labels[other_txid] = "BitcoinAfterLife inheritance transaction - https://other.example" + item = _make_simple_willitem(tx, valid=True, we_url="https://we.example") + Will.save_valid_transactions_to_history({"wid": item}, wallet, label) + assert stale_txid in wallet.adb.removed + assert other_txid not in wallet.adb.removed + assert txid not in wallet.adb.removed + # The stale tx's label is dropped with it. + assert stale_txid not in wallet.labels + assert other_txid in wallet.labels + assert wallet.labels[txid] == label + + +def test_save_no_wallet_or_no_adb_is_noop(): + tx = Transaction(_VALID_TX_HEX) + item = _make_simple_willitem(tx, valid=True) + Will.save_valid_transactions_to_history({"wid": item}, None, "LBL") + Will.save_valid_transactions_to_history({"wid": item}, object(), "LBL") + + +def test_save_never_raises_on_adb_failure(): + tx = Transaction(_VALID_TX_HEX) + item = _make_simple_willitem(tx, valid=True) + + class BoomWallet(FakeWallet): + class BoomADB: + def add_transaction(self, tx, *, allow_unrelated=False, is_new=True): + raise RuntimeError("boom") + + def remove_transaction(self, txid): + raise RuntimeError("boom") + + def __init__(self): + self.adb = self.BoomADB() + self.labels = {} + + def get_all_labels(self): + return {"stale": "BitcoinAfterLife inheritance transaction - "} + + wallet = BoomWallet() + Will.save_valid_transactions_to_history( + {"wid": item}, wallet, "BitcoinAfterLife inheritance transaction - {willexecutor}" + ) + # No exception propagates; a fresh fake works afterwards. + assert True + + +def test_check_will_does_not_save_to_history(): + # History persistence is no longer triggered from check_will: it runs after + # the will is signed (see the GUI hooks). check_will must not touch it. + with patch.object(Will, "save_valid_transactions_to_history") as save_mock: + Will.check_will({}, [], None, 9999999999) + save_mock.assert_not_called() + + +def test_is_will_valid_calls_check_will_without_history_label(): + with patch.object(Will, "check_will") as cw_mock: + Will.is_will_valid({}, 9999999999, 100, []) + assert len(cw_mock.call_args[0]) == 4 + assert cw_mock.call_args[0] == ({}, [], False, 9999999999) + + +# ------------------------------------------------------------------ # +# Signature absorption + status fixes (local-history will tx) +# ------------------------------------------------------------------ # + +def _make_willitem_keyed_by_txid(tx, valid=True, we_url=None): + """A WillItem whose ``_id`` equals its txid (as in a real built will). + + The script descriptor is re-attached after the WillItem round-trips the tx + through serialization, mirroring the real load-from-wallet flow. + """ + w = {"tx": tx, "heirs": {"a": ["addr", 100, "30d"]}, + "willexecutor": {"url": we_url} if we_url else None, "status": "", + "description": "", "time": 0, "change": "", "baltx_fees": 100, + "VALID": valid} + item = WillItem(w, _id=tx.txid()) + if isinstance(tx, PartialTransaction) and tx.inputs(): + desc = getattr(tx.inputs()[0], "script_descriptor", None) + if desc is not None and isinstance(item.tx, PartialTransaction): + item.tx.inputs()[0].script_descriptor = desc + return item + + +def _make_local_wallet(tx, stored, funding): + """A FakeWallet where *tx* is stored locally and consumes *funding*.""" + return FakeWallet( + stored_txs={tx.txid(): stored}, + spenders={funding: tx.txid()}, + heights={tx.txid(): TX_HEIGHT_LOCAL}, + ) + + +def test_absorb_history_signatures_merges_and_completes(): + # The wallet's stored local copy of the will tx carries more signatures + # than the in-memory item: they are merged in and the item becomes COMPLETE. + item = _make_multisig_willitem(1) + stored = _make_multisig_ptx(2) + assert stored.txid() == item.tx.txid() + wallet = FakeWallet(stored_txs={item._id: stored}) + Will._absorb_history_signatures({item._id: item}, wallet) + assert item.tx.is_complete() is True + assert item.get_status("COMPLETE") is True + assert item.get_status("VALID") is True + + +def test_absorb_history_signatures_noop_without_stored_copy(): + # Nothing stored in the wallet: the in-memory item is left untouched. + item = _make_multisig_willitem(1) + wallet = FakeWallet() + Will._absorb_history_signatures({item._id: item}, wallet) + assert item.tx.is_complete() is False + assert item.get_status("COMPLETE") is False + + +def test_check_invalidated_keeps_valid_on_local_spend(): + # The funding is missing from the wallet's UTXOs only because the will tx + # itself was saved into the local history: a wallet-local spender must not + # invalidate the will. + tx = _make_multisig_ptx(1) + item = _make_willitem_keyed_by_txid(tx) + will = {tx.txid(): item} + funding = tx.inputs()[0].prevout.to_str() + wallet = _make_local_wallet(tx, tx, funding) + Will.check_invalidated(will, [], wallet) + assert item.get_status("INVALIDATED") is False + assert item.get_status("VALID") is True + + +def test_check_invalidated_invalidates_on_real_spend(): + # The funding is consumed by a broadcast transaction: the will is dead. + tx = _make_multisig_ptx(1) + item = _make_willitem_keyed_by_txid(tx) + will = {tx.txid(): item} + funding = tx.inputs()[0].prevout.to_str() + ext_spender = "ab" * 32 + wallet = FakeWallet( + spenders={funding: ext_spender}, + heights={ext_spender: 100}, + ) + Will.check_invalidated(will, [], wallet) + assert item.get_status("INVALIDATED") is True + assert item.get_status("VALID") is False + + +def test_search_rai_local_artifact_keeps_valid(): + tx = _make_multisig_ptx(1) + item = _make_willitem_keyed_by_txid(tx) + will = {tx.txid(): item} + funding = tx.inputs()[0].prevout.to_str() + wallet = _make_local_wallet(tx, tx, funding) + Will.search_rai(Will.get_all_inputs(will, only_valid=True), [], will, wallet) + assert item.get_status("VALID") is True + assert item.get_status("INVALIDATED") is False + assert item.get_status("CONFIRMED") is False + + +def test_search_rai_confirmed_on_broadcast_spender(): + # The will tx is broadcast/confirmed: its own (real) spender marks it + # CONFIRMED, not INVALIDATED. + tx = _make_multisig_ptx(1) + item = _make_willitem_keyed_by_txid(tx) + will = {tx.txid(): item} + funding = tx.inputs()[0].prevout.to_str() + wallet = FakeWallet( + stored_txs={tx.txid(): tx}, + spenders={funding: tx.txid()}, + heights={tx.txid(): 100}, + ) + Will.search_rai(Will.get_all_inputs(will, only_valid=True), [], will, wallet) + assert item.get_status("CONFIRMED") is True + assert item.get_status("INVALIDATED") is False + + +def test_check_will_merges_history_sigs_and_stays_valid(): + # End-to-end: a will tx stored in the local history gained a signature. + # check_will must absorb it, not invalidate the will for the local spend. + now = 1700000000 + locktime = 2000000000 + tx = _make_multisig_ptx(1, locktime) + stored = _make_multisig_ptx(2, locktime) + assert stored.txid() == tx.txid() + item = _make_willitem_keyed_by_txid(tx) + will = {tx.txid(): item} + funding = tx.inputs()[0].prevout.to_str() + wallet = _make_local_wallet(tx, stored, funding) + Will.check_will(will, [], wallet, now) + assert item.get_status("COMPLETE") is True + assert item.get_status("VALID") is True + assert item.get_status("INVALIDATED") is False + + +# ------------------------------------------------------------------ # +# Util.get_available_utxos (UTXO-view restoration) +# ------------------------------------------------------------------ # + +def _make_utxo(prevout_hex="22", idx=0, value=100000, + spent_txid=None, spent_height=None): + txin = PartialTxInput( + prevout=TxOutpoint(bytes.fromhex(prevout_hex) * 32, idx), script_sig=b"" + ) + txin._trusted_value_sats = value + txin.spent_txid = spent_txid + txin.spent_height = spent_height + return txin + + +_HISTORY_TEMPLATE = "BitcoinAfterLife inheritance transaction - {willexecutor}" +_HISTORY_LABEL = _HISTORY_TEMPLATE.replace("{willexecutor}", "https://we.example") + + +def _wallet_with_local_spend(locktime): + addr = "bcrt1qexample" + spender = "ab" * 32 + utxo = _make_utxo(spent_txid=spender, spent_height=TX_HEIGHT_LOCAL) + wallet = FakeWallet( + stored_txs={spender: _make_multisig_ptx(0, locktime=locktime)}, + heights={spender: TX_HEIGHT_LOCAL}, + outputs={addr: {utxo.prevout.to_str(): utxo}}, + addresses=[addr], + ) + wallet.labels[spender] = _HISTORY_LABEL + return wallet, utxo + + +def test_get_available_utxos_restores_future_bal_local_spend(): + # A later-locktime BAL history tx locally spent the coin: it is restored. + wallet, utxo = _wallet_with_local_spend(locktime=2000) + result = Util.get_available_utxos(wallet, _HISTORY_TEMPLATE, 1000) + assert [u.prevout.to_str() for u in result] == [utxo.prevout.to_str()] + + +def test_get_available_utxos_does_not_restore_unlabeled_spend(): + wallet, utxo = _wallet_with_local_spend(locktime=2000) + wallet.labels["ab" * 32] = "some other label" + result = Util.get_available_utxos(wallet, _HISTORY_TEMPLATE, 1000) + assert result == [] + + +def test_get_available_utxos_does_not_restore_not_later_locktime(): + # The stored spender's locktime equals the will's locktime (same will): + # its spend is NOT ignored. + wallet, utxo = _wallet_with_local_spend(locktime=1000) + result = Util.get_available_utxos(wallet, _HISTORY_TEMPLATE, 1000) + assert result == [] + + +def test_get_available_utxos_does_not_restore_confirmed_spend(): + # A broadcast (confirmed) spender is never ignored. + addr = "bcrt1qexample" + spender = "ab" * 32 + utxo = _make_utxo(spent_txid=spender, spent_height=100) + wallet = FakeWallet( + stored_txs={spender: _make_multisig_ptx(0, locktime=2000)}, + heights={spender: 100}, + outputs={addr: {utxo.prevout.to_str(): utxo}}, + addresses=[addr], + ) + wallet.labels[spender] = _HISTORY_LABEL + result = Util.get_available_utxos(wallet, _HISTORY_TEMPLATE, 1000) + assert result == [] + + +def test_get_available_utxos_none_locktime_is_raw_view(): + # No reference locktime: the raw wallet.get_utxos() view is returned, so a + # locally-spent coin stays hidden. + wallet, utxo = _wallet_with_local_spend(locktime=2000) + result = Util.get_available_utxos(wallet, _HISTORY_TEMPLATE, None) + assert result == [] + assert Util.get_available_utxos(None, _HISTORY_TEMPLATE, 1000) == [] + + # ------------------------------------------------------------------ # # Main # ------------------------------------------------------------------ # diff --git a/tests/test_group_c_settings.py b/tests/test_group_c_settings.py index f284431..3a770dd 100644 --- a/tests/test_group_c_settings.py +++ b/tests/test_group_c_settings.py @@ -64,6 +64,47 @@ def test_editable_dates_can_be_enabled(): assert BalConfig(cfg, "bal_editable_dates", False).get() is True +# ------------------------------------------------------------------ # +# History persistence settings (SAVE_HISTORY / HISTORY_LABEL) +# ------------------------------------------------------------------ # + +def test_save_history_defaults_on(): + """History persistence is opt-out: the flag defaults to ON.""" + cfg = FakeConfig() + save_history = BalConfig(cfg, "bal_save_history", True) + assert save_history.get() is True + + +def test_save_history_can_be_disabled_and_read_back(): + cfg = FakeConfig() + save_history = BalConfig(cfg, "bal_save_history", True) + save_history.set(False) + assert BalConfig(cfg, "bal_save_history", True).get() is False + + +def test_history_label_default_template(): + """The default label contains the {willexecutor} variable.""" + cfg = FakeConfig() + history_label = BalConfig( + cfg, "bal_history_label", "BitcoinAfterLife inheritance transaction - {willexecutor}" + ) + assert history_label.get() == ( + "BitcoinAfterLife inheritance transaction - {willexecutor}" + ) + assert "{willexecutor}" in history_label.get() + + +def test_history_label_can_be_changed_and_read_back(): + cfg = FakeConfig() + history_label = BalConfig( + cfg, "bal_history_label", "BitcoinAfterLife inheritance transaction - {willexecutor}" + ) + history_label.set("My custom label for {willexecutor}") + assert BalConfig( + cfg, "bal_history_label", "BitcoinAfterLife inheritance transaction - {willexecutor}" + ).get() == "My custom label for {willexecutor}" + + # ------------------------------------------------------------------ # # C4b - Reset to defaults # ------------------------------------------------------------------ # @@ -82,9 +123,10 @@ def _reset_to_defaults(configs): def test_reset_restores_all_dialog_settings(): """C4b: Reset restores every dialog setting to its factory default. - The dialog exposes seven settings: the original six plus the Group C - "Editable dates" checkbox, which the Reset button must also restore (this - was a follow-up fix after the first test round). + The dialog exposes nine settings: the original six, the Group C + "Editable dates" checkbox, and the Group H "Save inheritance transactions + in history" checkbox + "History label" field, which the Reset button must + also restore. """ cfg = FakeConfig() @@ -102,6 +144,10 @@ def test_reset_restores_all_dialog_settings(): "bal_event_description", "BAL will execution of $wallet_name\r\n heirs list: \r\n$heirs_complete", ) + save_history = BalConfig(cfg, "bal_save_history", True) + history_label = BalConfig( + cfg, "bal_history_label", "BitcoinAfterLife inheritance transaction - {willexecutor}" + ) settings = [ hide_replaced, hide_invalidated, @@ -110,6 +156,8 @@ def test_reset_restores_all_dialog_settings(): calendar_app, event_summary, event_description, + save_history, + history_label, ] # Mutate every setting away from its default. @@ -120,11 +168,15 @@ def test_reset_restores_all_dialog_settings(): calendar_app.set("/custom/app") event_summary.set("custom summary") event_description.set("custom description") + save_history.set(False) + history_label.set("custom label") # Sanity: the values really changed. assert hide_replaced.get() is False assert editable_dates.get() is True assert calendar_app.get() == "/custom/app" + assert save_history.get() is False + assert history_label.get() == "custom label" # Reset and verify each one is back to its declared default. _reset_to_defaults(settings) @@ -132,6 +184,11 @@ def test_reset_restores_all_dialog_settings(): assert s.get() == s.default # In particular the "Editable dates" flag is back OFF. assert editable_dates.get() is False + # And the history persistence flag is back ON with the default template. + assert save_history.get() is True + assert history_label.get() == ( + "BitcoinAfterLife inheritance transaction - {willexecutor}" + ) def test_reset_does_not_touch_unrelated_settings(): diff --git a/tests/test_gui_prepare_will_history.py b/tests/test_gui_prepare_will_history.py new file mode 100644 index 0000000..6bf527a --- /dev/null +++ b/tests/test_gui_prepare_will_history.py @@ -0,0 +1,216 @@ +""" +Tests for the history-save hook in ``BalWindow``. + +Verifies that every successful prepare (including the "Prepare" menu action) +persists the freshly prepared transactions into the wallet's local history, +while abort paths (which return ``None``) skip the save. Also verifies that +after any local-history change (saving or removing will transactions) the +wallet tabs are re-rendered through ``update_tabs`` (all tabs) plus +``update_status`` (status-bar balance), and that the rebuild path uses the same +full refresh. + +Run: + source electrum/env/bin/activate + QT_QPA_PLATFORM=offscreen python3 tests/test_gui_prepare_will_history.py +""" + +import sys + +sys.path.insert(0, __file__.rsplit("/", 2)[0]) + +from unittest.mock import Mock, patch + +import bal.gui.qt.window as win_mod +from bal.core.util import Util +from bal.core.will import NotCompleteWillException, Will +from bal.gui.qt.window import BalWindow + +# ------------------------------------------------------------------ # +# prepare_will -> _save_will_to_history +# ------------------------------------------------------------------ # + +def test_prepare_will_saves_to_history_on_success(): + win = object.__new__(BalWindow) + will = {"wid": object()} + with ( + patch.object(BalWindow, "build_inheritance_transaction", return_value=will) + as build_mock, + patch.object(BalWindow, "_save_will_to_history") as save_mock, + ): + result = BalWindow.prepare_will(win) + assert result is will + build_mock.assert_called_once_with(ignore_duplicate=False, keep_original=False) + save_mock.assert_called_once_with() + + +def test_prepare_will_skips_save_when_build_aborted(): + win = object.__new__(BalWindow) + with ( + patch.object(BalWindow, "build_inheritance_transaction", return_value=None) + as build_mock, + patch.object(BalWindow, "_save_will_to_history") as save_mock, + ): + result = BalWindow.prepare_will(win) + assert result is None + build_mock.assert_called_once_with(ignore_duplicate=False, keep_original=False) + save_mock.assert_not_called() + + +# ------------------------------------------------------------------ # +# history refresh: _save_will_to_history -> update_tabs + update_status +# ------------------------------------------------------------------ # + +class _Cfg: + def __init__(self, value): + self._value = value + + def get(self): + return self._value + + +class _CfgBag: + def __init__(self, **kwargs): + for name, value in kwargs.items(): + setattr(self, name, value) + + +class _Wallet: + def dust_threshold(self): + return 546 + + +class _FakeWindow: + def __init__(self, *, with_update_tabs=True): + self.show_message = Mock() + self.update_status = Mock() + self.history_list = Mock() + self.history_list.update = Mock() + if with_update_tabs: + self.update_tabs = Mock() + + +class _FakeQTimer: + calls = [] + + @classmethod + def singleShot(cls, delay, callable_): + cls.calls.append((delay, callable_)) + + +def _make_save_window(save_enabled): + win = object.__new__(BalWindow) + win.bal_plugin = _CfgBag( + SAVE_HISTORY=_Cfg(save_enabled), HISTORY_LABEL=_Cfg("LBL") + ) + win.willitems = {"wid": object()} + win.wallet = object() + return win + + +def test_save_will_to_history_schedules_refresh_when_enabled(): + win = _make_save_window(save_enabled=True) + with ( + patch.object(Will, "save_valid_transactions_to_history") as save_mock, + patch.object(BalWindow, "_schedule_history_refresh") as schedule_mock, + ): + BalWindow._save_will_to_history(win) + save_mock.assert_called_once_with(win.willitems, win.wallet, "LBL") + schedule_mock.assert_called_once_with() + + +def test_save_will_to_history_skips_save_and_refresh_when_disabled(): + win = _make_save_window(save_enabled=False) + with ( + patch.object(Will, "save_valid_transactions_to_history") as save_mock, + patch.object(BalWindow, "_schedule_history_refresh") as schedule_mock, + ): + BalWindow._save_will_to_history(win) + save_mock.assert_not_called() + schedule_mock.assert_not_called() + + +def test_save_will_to_history_schedules_refresh_even_on_error(): + win = _make_save_window(save_enabled=True) + with ( + patch.object( + Will, + "save_valid_transactions_to_history", + side_effect=RuntimeError("boom"), + ), + patch.object(BalWindow, "_schedule_history_refresh") as schedule_mock, + ): + BalWindow._save_will_to_history(win) + schedule_mock.assert_called_once_with() + + +def test_schedule_history_refresh_marshals_to_gui_thread(): + win = object.__new__(BalWindow) + with patch.object(win_mod, "QTimer", _FakeQTimer): + _FakeQTimer.calls.clear() + BalWindow._schedule_history_refresh(win) + assert _FakeQTimer.calls == [(0, win._refresh_after_history_save)] + + +def test_refresh_after_history_save_calls_update_tabs_and_status(): + win = object.__new__(BalWindow) + win.window = _FakeWindow(with_update_tabs=True) + BalWindow._refresh_after_history_save(win) + win.window.update_tabs.assert_called_once_with() + win.window.update_status.assert_called_once_with() + win.window.history_list.update.assert_not_called() + + +def test_refresh_after_history_save_falls_back_to_history_list(): + win = object.__new__(BalWindow) + win.window = _FakeWindow(with_update_tabs=False) + BalWindow._refresh_after_history_save(win) + win.window.history_list.update.assert_called_once_with() + win.window.update_status.assert_called_once_with() + + +def test_rebuild_path_schedules_full_refresh(): + win = object.__new__(BalWindow) + win.disable_plugin = False + win.heirs = {"h": object()} + win.willexecutors = {} + win.no_willexecutor = True + win.willitems = {} + win.will = {} + win.date_to_check = 1_800_000_000 + win.will_settings = {"baltx_fees": 1, "locktime": "1 month"} + win.bal_plugin = _CfgBag( + MAX_WILLEXECUTOR_FEE=_Cfg(1), + SAVE_HISTORY=_Cfg(True), + HISTORY_LABEL=_Cfg("LBL"), + ) + win.window = _FakeWindow() + win.window.wallet = _Wallet() + with ( + patch.object(Util, "get_available_utxos", return_value=[]), + patch.object(Util, "parse_locktime_string", return_value=1_800_000_001), + patch.object(Will, "get_min_locktime", return_value=0), + patch.object(Will, "check_amounts"), + patch.object(BalWindow, "init_class_variables"), + patch.object(BalWindow, "build_will"), + patch.object( + BalWindow, + "check_will", + side_effect=[NotCompleteWillException(), None], + ), + patch.object(BalWindow, "update_all"), + patch.object(BalWindow, "_schedule_history_refresh") as schedule_mock, + ): + BalWindow.build_inheritance_transaction(win) + schedule_mock.assert_called_once_with() + + +# ------------------------------------------------------------------ # +# Main +# ------------------------------------------------------------------ # + +if __name__ == "__main__": + for name in sorted(dir()): + if name.startswith("test_"): + globals()[name]() + print(f" [OK] {name}") + print("[OK] All prepare-will history tests passed") diff --git a/tests/test_gui_theme.py b/tests/test_gui_theme.py index 72f2827..d79dcc9 100644 --- a/tests/test_gui_theme.py +++ b/tests/test_gui_theme.py @@ -11,13 +11,16 @@ import sys sys.path.insert(0, __file__.rsplit("/", 2)[0]) -from bal.gui.qt.theme import status_color +from bal.gui.qt.theme import signature_suffix, status_color # ------------------------------------------------------------------ # # Helpers # ------------------------------------------------------------------ # class FakeWillItem: + sigs_required = 0 + sigs_have = 0 + def __init__(self, **status_flags): self._status = dict(status_flags) def get_status(self, name): @@ -97,6 +100,54 @@ def test_color_check_fail_overrides_push_fail(): assert status_color(item) == "#e83845" +# ------------------------------------------------------------------ # +# PARTIALLY_SIGNED +# ------------------------------------------------------------------ # + +def test_color_partially_signed(): + assert status_color(FakeWillItem(PARTIALLY_SIGNED=True)) == "#ffb347" + + +def test_color_partially_signed_overridden_by_higher_priority(): + item = FakeWillItem(PARTIALLY_SIGNED=True, PUSHED=True) + assert status_color(item) == "#73f3c8" + + +# ------------------------------------------------------------------ # +# signature_suffix +# ------------------------------------------------------------------ # + +def test_signature_suffix_partial(): + item = FakeWillItem(PARTIALLY_SIGNED=True) + item.sigs_required = 2 + item.sigs_have = 1 + assert signature_suffix(item) == " (1/2)" + + +def test_signature_suffix_new(): + item = FakeWillItem() + item.sigs_required = 2 + item.sigs_have = 0 + assert signature_suffix(item) == " (0/2)" + + +def test_signature_suffix_complete_empty(): + item = FakeWillItem(COMPLETE=True) + item.sigs_required = 2 + item.sigs_have = 1 + assert signature_suffix(item) == "" + + +def test_signature_suffix_unknown_required_empty(): + item = FakeWillItem() + item.sigs_have = 1 + assert signature_suffix(item) == "" + + +def test_signature_suffix_missing_fields_empty(): + assert signature_suffix(FakeWillItem()) == "" + + # ------------------------------------------------------------------ # # Main # ------------------------------------------------------------------ # diff --git a/tests/test_import_will_details.py b/tests/test_import_will_details.py new file mode 100644 index 0000000..2f6846b --- /dev/null +++ b/tests/test_import_will_details.py @@ -0,0 +1,559 @@ +""" +Tests for the "Import" (read-only will preview) flow. + +Covers: + - ``BalWindow._load_will_file`` round-trip (file -> WillItems). + - ``import_will_into_details`` normalization + IMPORTED status. + - ``BalWindow.sign_transactions`` operating ONLY on the passed (imported) + will, never on the live wallet state. + - ``WillWidget`` honouring an explicit ``will`` argument. + - ``WillDetailDialog`` external-will mode (threshold + isolated buttons). + +Run: + source /home/steal/devel/bal/electrum/env/bin/activate + QT_QPA_PLATFORM=offscreen python3 tests/test_import_will_details.py +""" + +import sys +import tempfile +from types import MethodType, SimpleNamespace + +sys.path.insert(0, __file__.rsplit("/", 2)[0]) + +from PyQt6.QtWidgets import QApplication, QWidget + +from bal.core.will import Will, WillItem +from bal.gui.qt import window as window_mod + +_app = QApplication.instance() or QApplication(sys.argv) + +# A valid serialized Bitcoin transaction hex (1 input + 1 P2PKH output, version 2) +_VALID_TX_HEX = ( + "01000000012a5c9a94fcde98f5581cd00162c60a13936ceb75389ea65b" + "f38633b424eb4031000000006c493046022100a82bbc57a0136751e543" + "3f41cf000b3f1a99c6744775e76ec764fb78c54ee100022100f9e80b7d" + "e89de861dc6fb0c1429d5da72c2b6b2ee2406bc9bfb1beedd729d98501" + "2102e61d176da16edd1d258a200ad9759ef63adf8e14cd97f53227bae3" + "5cdb84d2f6ffffffff0140420f00000000001976a914230ac37834073a" + "42146f11ef8414ae929feaafc388ac00000000" +) + + +def _make_willitem_dict(**overrides): + """Return a minimal dict that can construct a WillItem.""" + d = { + "tx": _VALID_TX_HEX, + "heirs": {}, + "willexecutor": None, + "status": "", + "description": "", + "time": 0, + "change": "", + "baltx_fees": 100, + } + d.update(overrides) + return d + + +def _make_willitems(n, prefix="w"): + """Create ``n`` WillItems with distinct txids/locktimes.""" + willitems = {} + for i in range(n): + wid = f"{prefix}{i}" + wi = WillItem(_make_willitem_dict()) + wi.tx.locktime = 1000 + i + wi._id = wid + willitems[wid] = wi + return willitems + + +# ------------------------------------------------------------------ # +# _load_will_file round-trip +# ------------------------------------------------------------------ # + +def test_load_will_file_roundtrip(): + from bal.gui.qt.common import write_json_file + + src = _make_willitems(2) + data = {wid: wi.to_dict() for wid, wi in src.items()} + with tempfile.NamedTemporaryFile( + mode="w", suffix=".json", delete=False + ) as f: + path = f.name + write_json_file(path, data) + try: + loaded = window_mod.BalWindow._load_will_file(None, path) + assert set(loaded) == {"w0", "w1"} + for wid, wi in loaded.items(): + assert isinstance(wi, WillItem) + assert wi.heirs == {} + assert wi._id == wid + assert wi.tx is not None + finally: + import os + + os.unlink(path) + + +# ------------------------------------------------------------------ # +# import_will_into_details normalization + IMPORTED status +# ------------------------------------------------------------------ # + +def test_imported_will_is_normalized_and_marked_imported(): + willitems = _make_willitems(1) + Will.normalize_will(willitems, None) + for wi in willitems.values(): + wi.set_status("IMPORTED", True) + assert all(wi.get_status("IMPORTED") for wi in willitems.values()) + assert all(wi.get_status("VALID") for wi in willitems.values()) + + +# ------------------------------------------------------------------ # +# sign_transactions operates only on the passed (imported) will +# ------------------------------------------------------------------ # + +def test_sign_transactions_external_only(): + class FakeWallet: + def sign_transaction(self, tx, password, ignore_warnings=False): + # No-op: never marks the tx complete. + return None + + live = _make_willitems(1, prefix="live") + wid_live = next(iter(live)) + imported = _make_willitems(2, prefix="imp") + + fake = SimpleNamespace( + willitems=live, + wallet=FakeWallet(), + waiting_dialog=SimpleNamespace(update=lambda msg: None), + ) + + result = window_mod.BalWindow.sign_transactions(fake, None, will=imported) + + assert result is not None + assert set(result) == set(imported) + assert wid_live not in result + # The live will must be completely untouched by the external sign run. + assert live[wid_live].get_status("COMPLETE") is False + + +# ------------------------------------------------------------------ # +# WillWidget honours an explicit ``will`` argument +# ------------------------------------------------------------------ # + +def test_will_widget_explicit_will(): + from bal.gui.qt.widgets import WillWidget + + live = _make_willitems(1, prefix="live") + imported = _make_willitems(2, prefix="imp") + + fake_parent = SimpleNamespace( + decimal_point=8, + base_unit_name="BTC", + bal_window=SimpleNamespace( + willitems=live, + bal_plugin=SimpleNamespace( + _hide_replaced=False, _hide_invalidated=False + ), + show_transaction=lambda *a, **k: None, + ), + ) + + w = WillWidget(parent=fake_parent, will=imported) + assert w.will is imported + + w2 = WillWidget(parent=fake_parent) + assert w2.will is live + + +# ------------------------------------------------------------------ # +# WillDetailDialog external-will mode +# ------------------------------------------------------------------ # + +def _make_fake_bal_window(window_widget): + bal_plugin = SimpleNamespace(read_file=lambda path: b"") + return SimpleNamespace( + window=window_widget, + bal_plugin=bal_plugin, + wallet=SimpleNamespace(), + show_transaction=lambda *a, **k: None, + willitems=_make_willitems(1), + will_settings={"real_threshold": 9999}, + ) + + +def test_will_detail_dialog_external_threshold(): + from bal.gui.qt.dialogs import WillDetailDialog + + window_widget = QWidget() + bal_window = _make_fake_bal_window(window_widget) + bal_window.window.config = SimpleNamespace() + bal_window.window.format_amount = lambda *a, **k: "1.0" + bal_window.window.base_unit = "BTC" + bal_window.window.format_fiat_and_units = lambda *a, **k: "1.0" + bal_window.window.fx = None + bal_window.window.format_fee_rate = lambda *a, **k: "1.0" + bal_window.window.get_decimal_point = lambda: 8 + + imported = _make_willitems(2) + # locktimes 1000 and 1001 -> threshold must be the max (1001). + dialog = WillDetailDialog(bal_window, will=imported) + + assert dialog._external_will is True + assert dialog.will is imported + assert dialog.threshold == 1001 + + dialog2 = WillDetailDialog(bal_window) + assert dialog2._external_will is False + assert dialog2.will is bal_window.willitems + assert dialog2.threshold == 9999 + + +def test_will_detail_dialog_buttons_pass_will(): + from bal.gui.qt.dialogs import WillDetailDialog + + window_widget = QWidget() + bal_window = _make_fake_bal_window(window_widget) + bal_window.window.config = SimpleNamespace() + bal_window.window.format_amount = lambda *a, **k: "1.0" + bal_window.window.base_unit = "BTC" + bal_window.window.format_fiat_and_units = lambda *a, **k: "1.0" + bal_window.window.fx = None + bal_window.window.format_fee_rate = lambda *a, **k: "1.0" + bal_window.window.get_decimal_point = lambda: 8 + + imported = _make_willitems(1) + dialog = WillDetailDialog(bal_window, will=imported) + + calls = [] + + bal_window.ask_password_and_sign_transactions = lambda **k: calls.append( + ("sign", k.get("will")) + ) + bal_window.broadcast_transactions = lambda **k: calls.append( + ("broadcast", k.get("will")) + ) + bal_window.export_will = lambda **k: calls.append(("export", k.get("will"))) + bal_window.invalidate_will = lambda **k: calls.append( + ("invalidate", k.get("will")) + ) + + dialog.ask_password_and_sign_transactions() + dialog.broadcast_transactions() + dialog.export_will() + dialog.invalidate_will() + + assert len(calls) == 4 + for action, will in calls: + assert will is imported, f"{action} did not receive the imported will" + + +# ------------------------------------------------------------------ # +# Merge flow (BalWindow.merge_will) +# ------------------------------------------------------------------ # + +def _make_partial_tx(locktime=1000, signed=False): + """A PartialTransaction derived from _VALID_TX_HEX. + + When ``signed`` the input scriptSig of the raw tx is copied over, which + finalizes the (legacy P2PKH) input and makes ``is_complete()`` True. + """ + from electrum.transaction import PartialTransaction, Transaction + + ptx = PartialTransaction.from_tx(Transaction(_VALID_TX_HEX)) + ptx.locktime = locktime + if signed: + raw = Transaction(_VALID_TX_HEX) + ptx.inputs()[0].script_sig = raw.inputs()[0].script_sig + return ptx + + +def _make_willitem_with_tx(tx, key=None): + wi = WillItem(_make_willitem_dict()) + wi.tx = tx + wi._id = key if key is not None else tx.txid() + return wi + + +def _make_merge_fake(willitems): + """A BalWindow-like object with a wallet stub sufficient for the local + validity check that ``merge_will`` runs after merging. + """ + class FakeWallet: + def add_input_info(self, txin, **kwargs): + pass + + def add_output_info(self, txout, **kwargs): + pass + + def get_utxos(self): + return [] + + def get_tx_info(self, tx): + return SimpleNamespace( + tx_mined_status=SimpleNamespace(height=lambda: 0) + ) + + @property + def db(self): + return SimpleNamespace(get_transaction=lambda txid: None) + + calls = [] + fake = SimpleNamespace( + willitems=willitems, + will={}, + wallet=FakeWallet(), + bal_window=None, + date_to_check=1700000000, + bal_plugin=SimpleNamespace( + HISTORY_LABEL=SimpleNamespace( + get=lambda: "BAL will history ({willexecutor})" + ) + ), + update_all=lambda: calls.append("update_all"), + ) + fake.save_willitems = MethodType(window_mod.BalWindow.save_willitems, fake) + return fake, calls + + +def test_merge_will_same_id_unsigned_live_substitutes_signed_imported(): + # The realistic "signed on another machine, imported here" case: the live + # will holds an unsigned PSBT (txid() -> None), the imported one is signed + # (complete). The transaction must be substituted and COMPLETE set. + wid = "same_id" + live_tx = _make_partial_tx(locktime=1000, signed=False) + imported_tx = _make_partial_tx(locktime=1000, signed=True) + + live = {wid: _make_willitem_with_tx(live_tx, key=wid)} + imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)} + imported[wid].set_status("COMPLETE", True) + imported[wid].set_status("PUSHED", True) + imported[wid].set_status("CHECKED", True) + + fake, calls = _make_merge_fake(live) + live_item = live[wid] + + window_mod.BalWindow.merge_will(fake, imported) + + # The live WillItem object is kept (never replaced) and the signed tx + # substituted in. + assert live[wid] is live_item + assert live_item.tx is imported_tx + assert live_item.tx.is_complete() + assert live_item.get_status("COMPLETE") is True + # Operational statuses were carried over. + assert live_item.get_status("PUSHED") is True + assert live_item.get_status("CHECKED") is True + # The will was saved and the GUI refreshed. + assert wid in fake.will + assert fake.will[wid]["COMPLETE"] is True + assert "update_all" in calls + + +def test_merge_will_same_id_both_signed_combines(): + # Live tx is signed but the COMPLETE flag was not set yet; the imported + # signed tx with the same txid must be COMBINED into the live one (the live + # tx object is kept) rather than substituted. + from electrum.transaction import Transaction + + wid = "same_id_combine" + imported_tx = _make_partial_tx(locktime=1000, signed=True) + txid = imported_tx.txid() + assert txid is not None + + live_tx = _make_partial_tx(locktime=1000, signed=True) + live = {wid: _make_willitem_with_tx(live_tx, key=wid)} + imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)} + imported[wid].set_status("COMPLETE", True) + + fake, _ = _make_merge_fake(live) + live_item = live[wid] + + window_mod.BalWindow.merge_will(fake, imported) + + # Same txid -> combine_with_other_psbt: live tx object is preserved. + assert live_item.tx is live_tx + assert live_item.tx.is_complete() + assert live_item.get_status("COMPLETE") is True + raw_sig = Transaction(_VALID_TX_HEX).inputs()[0].script_sig + assert live_item.tx.inputs()[0].script_sig == raw_sig + + +def test_merge_will_same_id_already_complete_never_touches_live_tx(): + wid = "already_complete" + live_tx = _make_partial_tx(locktime=1000, signed=False) + live = {wid: _make_willitem_with_tx(live_tx, key=wid)} + live[wid].set_status("COMPLETE", True) + + imported_tx = _make_partial_tx(locktime=1000, signed=True) + imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)} + imported[wid].set_status("COMPLETE", True) + imported[wid].set_status("PUSHED", True) + + fake, _ = _make_merge_fake(live) + live_item = live[wid] + + window_mod.BalWindow.merge_will(fake, imported) + + # An already-signed live will is left untouched: no combine, no substitute. + assert live_item.tx is live_tx + assert live_item.tx.is_complete() is False + assert live_item.tx.inputs()[0].script_sig is None + assert live_item.get_status("COMPLETE") is True + assert live_item.get_status("PUSHED") is True + + +def test_merge_will_statuses_are_monotonic(): + # Statuses that are True in the live item must never be cleared by a False + # value coming from the imported item. + wid = "monotonic" + live_tx = _make_partial_tx(locktime=1000, signed=True) + live = {wid: _make_willitem_with_tx(live_tx, key=wid)} + live[wid].set_status("CONFIRMED", True) + + imported_tx = _make_partial_tx(locktime=1000, signed=True) + imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)} + imported[wid].set_status("MEMPOOL", True) + + fake, _ = _make_merge_fake(live) + live_item = live[wid] + + window_mod.BalWindow.merge_will(fake, imported) + + assert live_item.get_status("CONFIRMED") is True + assert live_item.get_status("MEMPOOL") is True + assert live_item.get_status("COMPLETE") is True + + +def test_merge_will_new_ids_added_wholesale(): + live_tx = _make_partial_tx(locktime=1000, signed=True) + live = {live_tx.txid(): _make_willitem_with_tx(live_tx)} + + imported_tx = _make_partial_tx(locktime=2000, signed=True) + new_id = imported_tx.txid() + imported = {new_id: _make_willitem_with_tx(imported_tx)} + imported[new_id].set_status("PUSHED", True) + + fake, _ = _make_merge_fake(live) + + window_mod.BalWindow.merge_will(fake, imported) + + assert len(live) == 2 + assert live[new_id] is imported[new_id] + assert live[new_id].get_status("PUSHED") is True + assert new_id in fake.will + + +def test_merge_will_from_file_invalid_file_raises(): + from bal.gui.qt.common import FileImportFailed + + def bad_load(path): + raise ValueError("bad file") + + fake = SimpleNamespace(_load_will_file=bad_load, wallet=None) + + try: + window_mod.BalWindow.merge_will_from_file(fake, "/nonexistent.json") + except FileImportFailed as e: + assert "bad file" in str(e) + else: + raise AssertionError("expected FileImportFailed") + + +def test_merge_will_partial_signatures_update_counts(): + # A live unsigned 2-of-3 multisig will merged with an imported copy that + # carries 1 signature must end up PARTIALLY_SIGNED with the sig counts + # refreshed on the live item (check_signatures runs after the merge). + from binascii import unhexlify + + from electrum import crypto + from electrum.descriptor import parse_descriptor + from electrum.transaction import ( + PartialTransaction, + PartialTxInput, + PartialTxOutput, + Sighash, + TxOutpoint, + ) + + def make_multisig_tx(nsigs): + pubs = [] + for seed in (1, 2, 3): + pubs.append(crypto.privkey_to_pubkey(bytes([seed] * 32)).hex()) + desc = parse_descriptor("wsh(multi(2,{}))".format(",".join(pubs))) + txin = PartialTxInput(prevout=TxOutpoint(b"\x11" * 32, 0), script_sig=b"") + txin.script_descriptor = desc + txin._trusted_value_sats = 100000 + txin.sighash = Sighash.ALL + sig = b"\x30\x44\x02\x20" + b"\x01" * 32 + b"\x02\x20" + b"\x02" * 32 + for i in range(nsigs): + txin.sigs_ecdsa[unhexlify(pubs[i])] = sig + from electrum.bitcoin import public_key_to_p2wpkh + + addr = public_key_to_p2wpkh(bytes.fromhex(pubs[0])) + ptx = PartialTransaction() + ptx.add_inputs([txin]) + ptx.add_outputs([PartialTxOutput.from_address_and_value(addr, 50000)]) + ptx.locktime = 1000 + return ptx, desc + + wid = "multisig_will" + live_tx, _ = make_multisig_tx(0) + imported_tx, _ = make_multisig_tx(1) + + live = {wid: _make_willitem_with_tx(live_tx, key=wid)} + imported = {wid: _make_willitem_with_tx(imported_tx, key=wid)} + + fake, _ = _make_merge_fake(live) + live_item = live[wid] + + window_mod.BalWindow.merge_will(fake, imported) + + assert live_item.get_status("PARTIALLY_SIGNED") is True + assert live_item.sigs_have == 1 + assert live_item.sigs_required == 2 + + +def test_will_detail_dialog_merge_switches_to_live(): + from bal.gui.qt.dialogs import WillDetailDialog + + window_widget = QWidget() + bal_window = _make_fake_bal_window(window_widget) + bal_window.window.config = SimpleNamespace() + bal_window.window.format_amount = lambda *a, **k: "1.0" + bal_window.window.base_unit = "BTC" + bal_window.window.format_fiat_and_units = lambda *a, **k: "1.0" + bal_window.window.fx = None + bal_window.window.format_fee_rate = lambda *a, **k: "1.0" + bal_window.window.get_decimal_point = lambda: 8 + + live = bal_window.willitems + imported = _make_willitems(2) + merged = [] + bal_window.merge_will = lambda will: merged.append(will) + + dialog = WillDetailDialog(bal_window, will=imported) + + assert dialog._external_will is True + assert dialog.merge_button is not None + assert dialog.merge_button.isHidden() is False + + dialog.merge_will() + + assert merged == [imported] + assert dialog._external_will is False + assert dialog.will is live + assert dialog.threshold == bal_window.will_settings["real_threshold"] + assert dialog.merge_button.isHidden() is True + + +# ------------------------------------------------------------------ # +# Main +# ------------------------------------------------------------------ # + +if __name__ == "__main__": + for name in sorted(dir()): + if name.startswith("test_"): + globals()[name]() + print(f" [OK] {name}") + print("[OK] All import-will-details tests passed") diff --git a/tests/test_settings_history_dialog.py b/tests/test_settings_history_dialog.py new file mode 100644 index 0000000..5e10274 --- /dev/null +++ b/tests/test_settings_history_dialog.py @@ -0,0 +1,136 @@ +""" +Tests for the "Save inheritance transactions in history" settings dialog rows. + +Covers: + - The history label line-edit is present and bound to the HISTORY_LABEL + config (its text is the configured label). + - The rows are hidden in BASIC mode and visible in ADVANCED mode (advanced + -only settings, mirroring the other advanced rows). + - The history label line-edit is disabled while the "Save inheritance + transactions in history" checkbox is off, and re-enabled when it is on. + +Run: + source /home/steal/devel/bal/electrum/env/bin/activate + QT_QPA_PLATFORM=offscreen python3 tests/test_settings_history_dialog.py +""" + +import sys +import tempfile + +sys.path.insert(0, __file__.rsplit("/", 2)[0]) + +from unittest.mock import patch + +from PyQt6.QtWidgets import QApplication + +_app = QApplication.instance() or QApplication(sys.argv) + +from electrum.simple_config import SimpleConfig + +import bal.gui.qt.plugin as plugin_mod +from bal.gui.qt.plugin import Plugin +from bal.gui.qt.widgets import BalCheckBox, BalLineEdit + +DEFAULT_LABEL = "BitcoinAfterLife inheritance transaction - {willexecutor}" + + +def _isolated_config(**overrides): + """An in-memory SimpleConfig that never touches the real Electrum config. + + A fresh ``electrum_path`` temp dir keeps every write isolated, so running + the tests cannot pollute the user's config files. + """ + opts = {"electrum_path": tempfile.mkdtemp(prefix="bal_test_")} + opts.update(overrides) + return SimpleConfig(opts) + + +def _build_dialog(user_type, **config_overrides): + """Build the plugin settings dialog for *user_type* and return it. + + ``settings_dialog`` ends with a blocking ``show_modal(d)`` call; we patch it + to capture the dialog and return immediately. + """ + cfg = _isolated_config(**config_overrides) + plugin = Plugin(None, cfg, "bal") + plugin.get_window_title = lambda s: s + plugin.read_file = lambda *a: b"" + plugin.broadcast_transactions = lambda *a, **k: None + plugin.update_all = lambda *a, **k: None + plugin.USER_TYPE.set(user_type) + + captured = [] + + def fake_show_modal(dlg): + captured.append(dlg) + return True + + with patch.object(plugin_mod, "show_modal", side_effect=fake_show_modal): + plugin.settings_dialog(None, None) + assert captured, "settings_dialog did not build a dialog" + return plugin, captured[0] + + +def _history_label_edit(dialog): + edits = [ + w for w in dialog.findChildren(BalLineEdit) if w.text() == DEFAULT_LABEL + ] + assert len(edits) == 1, f"expected exactly one history label edit, got {len(edits)}" + return edits[0] + + +def test_history_label_row_hidden_in_basic_visible_in_advanced(): + plugin, basic = _build_dialog("basic") + assert _history_label_edit(basic).isHidden() is True + basic.close() + + plugin, advanced = _build_dialog("advanced") + edit = _history_label_edit(advanced) + assert edit.isHidden() is False + advanced.close() + + +def test_history_label_field_follows_checkbox(): + # Default: SAVE_HISTORY is ON, so the label field starts enabled. + plugin, dialog = _build_dialog("advanced") + edit = _history_label_edit(dialog) + assert edit.isEnabled() is True + + # Find the checkbox that controls the label field's enabled state: it must + # be the "Save inheritance transactions in history" checkbox (the only one + # whose off-state disables the label field). + toggler = None + for box in dialog.findChildren(BalCheckBox): + if not box.isChecked(): + continue + box.setChecked(False) + if not edit.isEnabled(): + toggler = box + break + assert toggler is not None, "no checkbox disables the history label field" + + # Toggling it back on re-enables the field. + toggler.setChecked(True) + assert edit.isEnabled() is True + assert plugin.SAVE_HISTORY.get() is True + dialog.close() + + +def test_history_label_field_disabled_from_start_when_off(): + plugin, dialog = _build_dialog( + "advanced", **{"bal_save_history": False} + ) + assert _history_label_edit(dialog).isEnabled() is False + dialog.close() + + +# ------------------------------------------------------------------ # +# Main +# ------------------------------------------------------------------ # + +if __name__ == "__main__": + for name in sorted(dir()): + if name.startswith("test_"): + globals()[name]() + print(f" [OK] {name}") + print("[OK] All settings-history dialog tests passed")