From be38c9b5892852852e114a4a8db134ad784c8347 Mon Sep 17 00:00:00 2001 From: kaibot Date: Thu, 9 Apr 2026 01:28:47 +0000 Subject: [PATCH 1/4] docs(qt.py): Add comprehensive documentation for HeirsLockTimeEdit class --- qt.py | 3898 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3898 insertions(+) create mode 100644 qt.py diff --git a/qt.py b/qt.py new file mode 100644 index 0000000..87ba6a1 --- /dev/null +++ b/qt.py @@ -0,0 +1,3898 @@ +""" + +Bal + +Bitcoin after life + + +""" + +import copy +import enum +import sys +import time +from datetime import datetime +from decimal import Decimal +from functools import partial +from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Union +import traceback +try: + QT_VERSION = sys._GUI_QT_VERSION +except Exception: + QT_VERSION = 6 + +if QT_VERSION == 5: + from PyQt5.QtCore import QThread, QCoreApplication + from PyQt5.QtCore import ( + QDateTime, + QModelIndex, + QPersistentModelIndex, + Qt, + pyqtSignal, + ) + from PyQt5.QtGui import ( + QColor, + QPainter, + QPalette, + QStandardItem, + QStandardItemModel, + ) + from PyQt5.QtWidgets import ( + QAbstractItemView, + QCheckBox, + QComboBox, + QDateTimeEdit, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMenu, + QMenuBar, + QPushButton, + QScrollArea, + QSizePolicy, + QSpinBox, + QStyle, + QStyleOptionFrame, + QVBoxLayout, + QWidget, + ) +else: # QT6 + from PyQt6.QtCore import QThread, QCoreApplication + from PyQt6.QtCore import ( + QDateTime, + QModelIndex, + QPersistentModelIndex, + Qt, + pyqtSignal, + ) + from PyQt6.QtGui import ( + QColor, + QPainter, + QPalette, + QStandardItem, + QStandardItemModel, + ) + from PyQt6.QtWidgets import ( + QAbstractItemView, + QCheckBox, + QComboBox, + QDateTimeEdit, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMenu, + QMenuBar, + QPushButton, + QScrollArea, + QSizePolicy, + QSpinBox, + QStyle, + QStyleOptionFrame, + QVBoxLayout, + QWidget, + ) + +from electrum.bitcoin import ( + NLOCKTIME_BLOCKHEIGHT_MAX, + NLOCKTIME_MAX, + NLOCKTIME_MIN, +) +from electrum.gui.qt.amountedit import ( + BTCAmountEdit, +) + +if TYPE_CHECKING: + from electrum.gui.qt.main_window import ElectrumWindow + +from electrum.gui.qt.main_window import StatusBarButton +from electrum.gui.qt.my_treeview import MyTreeView +from electrum.gui.qt.password_dialog import PasswordDialog +from electrum.gui.qt.transaction_dialog import TxDialog +from electrum.gui.qt.util import ( + Buttons, + CancelButton, + ColorScheme, + EnterButton, + HelpButton, + MessageBoxMixin, + OkButton, + TaskThread, + WindowModalDialog, + char_width_in_lineedit, + import_meta_gui, + read_QIcon_from_bytes, + read_QPixmap_from_bytes, +) +from electrum.i18n import _ +from electrum.logging import Logger, get_logger +from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError +from electrum.payment_identifier import PaymentIdentifier +from electrum.plugin import hook, run_hook +from electrum.transaction import SerializationError, Transaction, tx_from_any +from electrum.util import ( + DECIMAL_POINT, + FileImportFailed, + UserCancelled, + decimal_point_to_base_unit_name, + read_json_file, + write_json_file, +) + +from .bal import BalPlugin +from .heirs import Heirs, HEIR_REAL_AMOUNT, HEIR_DUST_AMOUNT +from .util import Util +from .will import ( + AmountException, + HeirChangeException, + HeirNotFoundException, + NoHeirsException, + NotCompleteWillException, + NoWillExecutorNotPresent, + TxFeesChangedException, + Will, + WillexecutorChangeException, + WillExecutorNotPresent, + WillExpiredException, + WillItem, +) +from .willexecutors import Willexecutors + +_logger = get_logger(__name__) + + +class Plugin(BalPlugin, Logger): + def __init__(self, parent, config, name): + Logger.__init__(self) + self.logger.info("INIT BALPLUGIN") + BalPlugin.__init__(self, parent, config, name) + self.bal_windows = {} + + @hook + def init_qt(self, gui_object): + self.logger.info("HOOK bal init qt") + try: + self.gui_object = gui_object + for window in gui_object.windows: + wallet = window.wallet + if wallet: + window.show_warning( + _("Please restart Electrum to activate the BAL plugin"), + title=_("Success"), + ) + return + w = BalWindow(self, window) + self.bal_windows[window.winId] = w + for child in window.children(): + if isinstance(child, QMenuBar): + for menu_child in child.children(): + if isinstance(menu_child, QMenu): + try: + if menu_child.title() == _("&Tools"): + w.init_menubar_tools(menu_child) + + except Exception as e: + self.logger.error( + ("init_qt except:", menu_child.text()) + ) + raise e + + except Exception as e: + self.logger.error("Error loading plugini {}".format(e)) + raise e + + @hook + def create_status_bar(self, sb): + self.logger.info("HOOK create status bar") + return + b = StatusBarButton( + read_QIcon_from_bytes(self.bal_plugin.read_file("icons/bal32x32.png")), + "Bal " + _("Bitcoin After Life"), + partial(self.setup_dialog, sb), + sb.height(), + ) + sb.addPermanentWidget(b) + + @hook + def init_menubar(self, window): + self.logger.info("HOOK init_menubar") + w = self.get_window(window) + w.init_menubar_tools(window.tools_menu) + + @hook + def load_wallet(self, wallet, main_window): + self.logger.debug("HOOK load wallet") + w = self.get_window(main_window) + # havetoupdate = Util.fix_will_settings_tx_fees(wallet.db) + w.wallet = wallet + w.init_will() + w.willexecutors = Willexecutors.get_willexecutors( + self, update=False, bal_window=w + ) + w.disable_plugin = False + w.ok = True + + @hook + def close_wallet(self, wallet): + self.logger.debug("HOOK close wallet") + for winid, win in self.bal_windows.items(): + if win.wallet == wallet: + win.on_close() + + @hook + def init_keystore(self): + self.logger.debug("init keystore") + + @hook + def daemon_wallet_loaded(self, boh, wallet): + self.logger.debug("daemon wallet loaded") + + def get_window(self, window): + w = self.bal_windows.get(window.winId, None) + if w is None: + w = BalWindow(self, window) + self.bal_windows[window.winId] = w + return w + + def requires_settings(self): + return True + + def settings_widget(self, window): + + w = self.get_window(window.window) + widget = QWidget() + enterbutton = EnterButton(_("Settings"), partial(w.settings_dialog, window)) + + widget.setLayout(Buttons(enterbutton, widget)) + return widget + + def password_dialog(self, msg=None, parent=None): + parent = parent or self + d = PasswordDialog(parent, msg) + return d.run() + + def get_seed(self): + password = None + if self.wallet.has_keystore_encryption(): + password = self.password_dialog(parent=self.d.parent()) + if not password: + raise UserCancelled() + + keystore = self.wallet.get_keystore() + if not keystore or not keystore.has_seed(): + return + self.extension = bool(keystore.get_passphrase(password)) + return keystore.get_seed(password) + + def settings_dialog(self, window, wallet): + + d = BalDialog(window, self, self.get_window_title("Settings")) + d.setMinimumSize(100, 200) + qicon = read_QPixmap_from_bytes(self.read_file("icons/bal32x32.png")) + lbl_logo = QLabel() + lbl_logo.setPixmap(qicon) + + # heir_ping_willexecutors = bal_checkbox(self.PING_WILLEXECUTORS) + # heir_ask_ping_willexecutors = bal_checkbox(self.ASK_PING_WILLEXECUTORS) + # heir_no_willexecutor = bal_checkbox(self.NO_WILLEXECUTOR) + + def on_multiverse_change(): + self.update_all() + + # heir_enable_multiverse = bal_checkbox(self.ENABLE_MULTIVERSE,on_multiverse_change) + + heir_hide_replaced = bal_checkbox(self.HIDE_REPLACED, on_multiverse_change) + + heir_hide_invalidated = bal_checkbox( + self.HIDE_INVALIDATED, on_multiverse_change + ) + + heir_repush = QPushButton("Rebroadcast transactions") + heir_repush.clicked.connect(partial(self.broadcast_transactions, True)) + grid = QGridLayout(d) + add_widget( + grid, + "Hide Replaced", + heir_hide_replaced, + 1, + "Hide replaced transactions from will detail and list", + ) + add_widget( + grid, + "Hide Invalidated", + heir_hide_invalidated, + 2, + "Hide invalidated transactions from will detail and list", + ) + # add_widget( + # grid, + # "Ping Willexecutors", + # heir_ping_willexecutors, + # 3, + # "Ping willexecutors to get payment info before compiling will", + # ) + # add_widget( + # grid, + # " - Ask before", + # heir_ask_ping_willexecutors, + # 4, + # "Ask before to ping willexecutor", + # ) + # add_widget( + # grid, + # "Backup Transaction", + # heir_no_willexecutor, + # 5, + # "Add transactions without willexecutor", + # ) + # add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ") + grid.addWidget(heir_repush, 7, 0) + grid.addWidget( + HelpButton( + "Broadcast all transactions to willexecutors including those already pushed" + ), + 7, + 2, + ) + + if ret := bool(d.exec()): + try: + self.update_all() + return ret + except Exception: + pass + return False + + def broadcast_transactions(self, force): + for k, w in self.bal_windows.items(): + w.broadcast_transactions(force) + + def update_all(self): + for k, w in self.bal_windows.items(): + w.update_all() + + def get_window_title(self, title): + return _("BAL - ") + _(title) + + +class shown_cv: + _type = bool + + def __init__(self, value): + self.value = value + + def get(self): + return self.value + + def set(self, value): + self.value = value + + +class BalWindow(Logger): + def __init__(self, bal_plugin: "BalPlugin", window: "ElectrumWindow"): + Logger.__init__(self) + self.bal_plugin = bal_plugin + self.window = window + self.heirs = {} + self.will = {} + self.willitems = {} + self.willexecutors = {} + self.will_settings = None + self.ok = False + self.disable_plugin = True + self.bal_plugin.get_decimal_point = self.window.get_decimal_point + + if self.window.wallet: + self.heirs_tab = self.create_heirs_tab() + self.will_tab = self.create_will_tab() + self.wallet = self.window.wallet + self.heirs_tab.wallet = self.wallet + self.will_tab.wallet = self.wallet + + def init_menubar_tools(self, tools_menu): + self.tools_menu = tools_menu + + def add_optional_tab(tabs, tab, icon, description): + tab.tab_icon = icon + tab.tab_description = description + tab.tab_pos = len(tabs) + if tab.is_shown_cv.get(): + tabs.addTab(tab, icon, description.replace("&", "")) + + def add_toggle_action(tab): + is_shown = tab.is_shown_cv.get() + tab.menu_action = self.window.view_menu.addAction( + tab.tab_description, lambda: self.window.toggle_tab(tab) + ) + tab.menu_action.setCheckable(True) + tab.menu_action.setChecked(is_shown) + + add_optional_tab( + self.window.tabs, + self.heirs_tab, + read_QIcon_from_bytes(self.bal_plugin.read_file("icons/heir.png")), + _("&Heirs"), + ) + add_optional_tab( + self.window.tabs, + self.will_tab, + read_QIcon_from_bytes(self.bal_plugin.read_file("icons/will.png")), + _("&Will"), + ) + tools_menu.addSeparator() + self.tools_menu.willexecutors_action = tools_menu.addAction( + _("&Will-Executors"), self.show_willexecutor_dialog + ) + self.window.view_menu.addSeparator() + add_toggle_action(self.heirs_tab) + add_toggle_action(self.will_tab) + + def load_willitems(self): + self.willitems = {} + for wid, w in self.will.items(): + self.willitems[wid] = WillItem(w, wallet=self.wallet) + if self.willitems: + self.will_list.will = self.willitems + self.will_list.update_will(self.willitems) + self.will_tab.update() + + def save_willitems(self): + keys = list(self.will.keys()) + for k in keys: + del self.will[k] + for wid, w in self.willitems.items(): + self.will[wid] = w.to_dict() + + def init_will(self): + self.logger.info("********************init_____will____________**********") + if not self.willexecutors: + self.willexecutors = Willexecutors.get_willexecutors( + self.bal_plugin, update=False, bal_window=self + ) + if not self.heirs: + self.heirs = Heirs._validate(Heirs(self.wallet)) + if not self.will: + self.will = self.wallet.db.get_dict("will") + Util.fix_will_tx_fees(self.will) + if self.will: + self.willitems = {} + try: + self.load_willitems() + except Exception: + self.disable_plugin = True + self.show_warning( + _("Please restart Electrum to activate the BAL plugin"), + title=_("Success"), + ) + self.close_wallet() + return + + if not self.will_settings: + self.will_settings = self.wallet.db.get_dict("will_settings") + Util.fix_will_settings_tx_fees(self.will_settings) + + self.logger.info("will_settings: {}".format(self.will_settings)) + if not self.will_settings: + Util.copy(self.will_settings, self.bal_plugin.default_will_settings()) + self.logger.debug("not_will_settings {}".format(self.will_settings)) + self.bal_plugin.validate_will_settings(self.will_settings) + self.heir_list_widget.update_will_settings() + self.heir_list_widget.update() + + def init_wizard(self): + wizard_dialog = BalWizardDialog(self) + wizard_dialog.exec() + + def show_willexecutor_dialog(self): + self.willexecutor_dialog = WillExecutorDialog(self) + self.willexecutor_dialog.show() + + def create_heirs_tab(self): + self.heir_list_widget = HeirListWidget(self, self.window) + + tab = self.window.create_list_tab(self.heir_list_widget) + tab.is_shown_cv = shown_cv(False) + return tab + + def create_will_tab(self): + self.will_list = PreviewList(self, self.window, None) + tab = self.window.create_list_tab(self.will_list) + tab.is_shown_cv = shown_cv(True) + return tab + + def new_heir_dialog(self, heir_key=None): + heir = self.heirs.get(heir_key) + title = "New heir" + if heir: + title = f"Edit: {heir_key}" + + d = BalDialog( + self.window, self.bal_plugin, self.bal_plugin.get_window_title(_(title)) + ) + + vbox = QVBoxLayout(d) + grid = QGridLayout() + + heir_name = QLineEdit() + heir_name.setFixedWidth(32 * char_width_in_lineedit()) + if heir: + heir_name.setText(str(heir_key)) + heir_address = QLineEdit() + heir_address.setFixedWidth(32 * char_width_in_lineedit()) + if heir: + heir_address.setText(str(heir[0])) + heir_amount = PercAmountEdit(self.window.get_decimal_point) + if heir: + heir_amount.setText( + str(Util.decode_amount(heir[1], self.window.get_decimal_point())) + ) + self.heir_locktime = HeirsLockTimeEdit(self.window, 0) + if heir: + self.heir_locktime.set_locktime(heir[2]) + + # heir_is_xpub = QCheckBox() + + new_heir_button = QPushButton(_("Add another heir")) + self.add_another_heir = False + + def new_heir(): + self.add_another_heir = True + d.accept() + + new_heir_button.clicked.connect(new_heir) + new_heir_button.setDefault(True) + + grid.addWidget(QLabel(_("Name")), 1, 0) + grid.addWidget(heir_name, 1, 1) + grid.addWidget(HelpButton(_("Unique name or description about heir")), 1, 2) + + grid.addWidget(QLabel(_("Address")), 2, 0) + grid.addWidget(heir_address, 2, 1) + grid.addWidget(HelpButton(_("heir bitcoin address")), 2, 2) + + grid.addWidget(QLabel(_("Amount")), 3, 0) + grid.addWidget(heir_amount, 3, 1) + grid.addWidget(HelpButton(_("Fixed or Percentage amount if end with %")), 3, 2) + + locktime_label = QLabel(_("Locktime")) + enable_multiverse = self.bal_plugin.ENABLE_MULTIVERSE.get() + if enable_multiverse: + grid.addWidget(locktime_label, 4, 0) + grid.addWidget(self.heir_locktime, 4, 1) + grid.addWidget(HelpButton(_("locktime")), 4, 2) + + vbox.addLayout(grid) + buttons = [CancelButton(d), OkButton(d)] + if not heir: + buttons.append(new_heir_button) + vbox.addLayout(Buttons(*buttons)) + while d.exec(): + # TODO SAVE HEIR + heir = [ + heir_name.text(), + heir_address.text(), + Util.encode_amount(heir_amount.text(), self.window.get_decimal_point()), + str(self.heir_locktime.get_locktime()), + ] + try: + self.set_heir(heir) + if self.add_another_heir: + self.new_heir_dialog() + break + except Exception as e: + self.show_error(str(e)) + + def set_heir(self, heir): + heir = list(heir) + if not self.bal_plugin.ENABLE_MULTIVERSE.get(): + heir[3] = self.will_settings["locktime"] + + h = Heirs.validate_heir(heir[0], heir[1:]) + self.heirs[heir[0]] = h + self.heir_list_widget.update() + return True + + def delete_heirs(self, heirs): + for heir in heirs: + try: + del self.heirs[heir] + except Exception as e: + _logger.debug(f"error deleting heir: {heir} {e}") + pass + self.heirs.save() + self.heir_list_widget.update() + return True + + def import_heirs(self): + import_meta_gui( + self.window, _("heirs"), self.heirs.import_file, self.heir_list_widget.update + ) + + def export_heirs(self): + Util.export_meta_gui(self.window, _("heirs"), self.heirs.export_file) + + def prepare_will(self, ignore_duplicate=False, keep_original=False): + will = self.build_inheritance_transaction( + ignore_duplicate=ignore_duplicate, keep_original=keep_original + ) + return will + + def delete_not_valid(self, txid, s_utxo): + raise NotImplementedError() + + def update_will(self, will): + Will.update_will(self.willitems, will) + self.willitems.update(will) + Will.normalize_will(self.willitems, self.wallet) + + def build_will(self, ignore_duplicate=True, keep_original=True): + _logger.debug("building will...") + will = {} + # willtodelete = [] + # willtoappend = {} + try: + self.init_class_variables() + self.willexecutors = Willexecutors.get_willexecutors( + self.bal_plugin, update=False, bal_window=self + ) + if not self.no_willexecutor: + + f = False + for u, w in self.willexecutors.items(): + if Willexecutors.is_selected(w): + f = True + if not f: + _logger.error("No Will-Executor or backup transaction selected") + raise NoWillExecutorNotPresent( + "No Will-Executor or backup transaction selected" + ) + txs = self.heirs.get_transactions( + self.bal_plugin, + self.window.wallet, + self.will_settings["baltx_fees"], + None, + self.date_to_check, + ) + + self.logger.info(f"txs built: {txs}") + creation_time = time.time() + if txs: + for txid in txs: + # txtodelete = [] + _break = False + tx = {} + tx["tx"] = txs[txid] + tx["my_locktime"] = txs[txid].my_locktime + tx["heirsvalue"] = txs[txid].heirsvalue + tx["description"] = txs[txid].description + tx["willexecutor"] = copy.deepcopy(txs[txid].willexecutor) + tx["status"] = _("New") + tx["baltx_fees"] = txs[txid].tx_fees + tx["time"] = creation_time + tx["heirs"] = copy.deepcopy(txs[txid].heirs) + tx["txchildren"] = [] + will[txid] = WillItem(tx, _id=txid, wallet=self.wallet) + self.update_will(will) + else: + self.logger.info("No transactions was built") + self.logger.info(f"will-settings: {self.will_settings}") + self.logger.info(f"date_to_check:{self.date_to_check}") + self.logger.info(f"heirs: {self.heirs}") + return {} + except Exception as e: + self.logger.info(f"Exception build_will: {e}") + raise e + pass + return self.willitems + + def check_will(self): + return Will.is_will_valid( + self.willitems, + self.block_to_check, + self.date_to_check, + self.will_settings["baltx_fees"], + self.window.wallet.get_utxos(), + heirs=self.heirs, + willexecutors=self.willexecutors, + self_willexecutor=self.no_willexecutor, + wallet=self.wallet, + callback_not_valid_tx=self.delete_not_valid, + ) + + def show_message(self, text): + self.window.show_message(text) + + def show_warning(self, text, parent=None): + self.window.show_warning(text, parent=None) + + def show_error(self, text): + self.window.show_error(text) + + def show_critical(self, text): + self.window.show_critical(text) + + def init_heirs_to_locktime(self, multiverse=False): + for heir in self.heirs: + h = self.heirs[heir] + if not multiverse: + self.heirs[heir] = [h[0], h[1], self.will_settings["locktime"]] + + def init_class_variables(self): + if not self.heirs: + raise NoHeirsException(_("Heirs are not defined")) + try: + self.date_to_check = Util.parse_locktime_string( + self.will_settings["threshold"] + ) + # found = False + self.locktime_blocks = self.bal_plugin.LOCKTIME_BLOCKS.get() + self.current_block = Util.get_current_height(self.wallet.network) + self.block_to_check=0 + self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get() + self.willexecutors = Willexecutors.get_willexecutors( + self.bal_plugin, update=True, bal_window=self, task=False + ) + if self.date_to_check < datetime.now().timestamp(): + raise CheckAliveException(self.date_to_check) + self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get()) + + except Exception as e: + self.logger.error(f"init_class_variables: {e}") + raise e + + def build_inheritance_transaction(self, ignore_duplicate=True, keep_original=True): + try: + if self.disable_plugin: + self.logger.info("plugin is disabled") + return + if not self.heirs: + self.logger.warning("not heirs {}".format(self.heirs)) + return + try: + self.init_class_variables() + Will.check_amounts( + self.heirs, + self.willexecutors, + self.window.wallet.get_utxos(), + self.date_to_check, + self.window.wallet.dust_threshold(), + ) + except AmountException as e: + self.show_warning( + _( + f"In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{e}" + ) + ) + except CheckAliveException: + self.show_error(_("CheckAlive is in the past please update it to a date in the future but less than locktime")) + return + locktime = Util.parse_locktime_string(self.will_settings["locktime"]) + if locktime < self.date_to_check: + self.show_error(_("locktime is lower than threshold")) + return + if not self.no_willexecutor: + f = False + for k, we in self.willexecutors.items(): + if Willexecutors.is_selected(we): + f = True + if not f: + self.show_error( + _(" no backup transaction or willexecutor selected") + ) + return + + try: + self.check_will() + except WillExpiredException: + self.invalidate_will() + return + except NoHeirsException: + return + except NotCompleteWillException as e: + self.logger.info("{}:{}".format(type(e), e)) + message = False + if isinstance(e, HeirChangeException): + message = "Heirs changed:" + elif isinstance(e, WillExecutorNotPresent): + message = "Will-Executor not present:" + elif isinstance(e, WillexecutorChangeException): + message = "Will-Executor changed" + elif isinstance(e, TxFeesChangedException): + message = "Txfees are changed" + elif isinstance(e, HeirNotFoundException): + message = "Heir not found" + + if message: + self.show_message( + f"{_(message)}:\n {e}\n{_('will have to be built')}" + ) + + self.logger.info("build will") + self.build_will(ignore_duplicate, keep_original) + + try: + self.check_will() + for wid, w in self.willitems.items(): + self.wallet.set_label(wid, "BAL Transaction") + except WillExpiredException as e: + self.invalidate_will() + except NotCompleteWillException as e: + self.show_error( + "Error:{}\n {}".format( + str(e), + _("Please, check your heirs, locktime and threshold!"), + ) + ) + + self.window.history_list.update() + self.window.utxo_list.update() + self.update_all() + return self.willitems + except Exception as e: + raise e + + def show_transaction_real( + self, + tx: Transaction, + *, + parent: "ElectrumWindow", + prompt_if_unsaved: bool = False, + external_keypairs: Mapping[bytes, bytes] = None, + payment_identifier: "PaymentIdentifier" = None, + ): + try: + d = TxDialog( + tx, + parent=parent, + prompt_if_unsaved=prompt_if_unsaved, + external_keypairs=external_keypairs, + # payment_identifier=payment_identifier, + ) + d.setWindowIcon( + read_QIcon_from_bytes(self.bal_plugin.read_file("icons/bal32x32.png")) + ) + except SerializationError as e: + self.logger.error("unable to deserialize the transaction") + parent.show_critical( + _("Electrum was unable to deserialize the transaction:") + "\n" + str(e) + ) + else: + d.show() + return d + + def show_transaction(self, tx=None, txid=None, parent=None): + if not parent: + parent = self.window + if txid is not None and txid in self.willitems: + tx = self.willitems[txid].tx + if not tx: + raise Exception(_("no tx")) + return self.show_transaction_real(tx, parent=parent) + + def invalidate_will(self): + def on_success(result): + if result: + self.show_message( + _( + "Please sign and broadcast this transaction to invalidate current will" + ) + ) + self.wallet.set_label(result.txid(), "BAL Invalidate") + self.show_transaction(result) + else: + self.show_message(_("No transactions to invalidate")) + + def on_failure(exec_info): + self.show_error(f"ERROR:{exec_info}") + + fee_per_byte = self.will_settings.get("baltx_fees", 1) + task = partial(Will.invalidate_will, self.willitems, self.wallet, fee_per_byte) + 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): + try: + txs = {} + signed = None + tosign = None + + def get_message(): + msg = "" + if signed: + msg = _(f"signed: {signed}\n") + return msg + _(f"signing: {tosign}") + + for txid in Will.only_valid(self.willitems): + wi = self.willitems[txid] + tx = copy.deepcopy(wi.tx) + if wi.get_status("COMPLETE"): + txs[txid] = tx + continue + tosign = txid + try: + self.waiting_dialog.update(get_message()) + except Exception: + 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]] + txin._trusted_value_sats = change.value + try: + txin.script_descriptor = change.script_descriptor + except Exception: + pass + txin.is_mine = True + txin._TxInput__address = change.address + txin._TxInput__scriptpubkey = change.scriptpubkey + txin._TxInput__value_sats = change.value + + self.wallet.sign_transaction(tx, password, ignore_warnings=True) + signed = tosign + # is_complete = False + if tx.is_complete(): + # is_complete = True + wi.set_status("COMPLETE", True) + txs[txid] = tx + except Exception: + return None + return txs + + def get_wallet_password(self, message=None, parent=None): + parent = self.window if not parent else parent + password = None + if self.wallet.has_keystore_encryption(): + password = self.bal_plugin.password_dialog(parent=parent, msg=message) + if password is None: + return False + try: + self.wallet.check_password(password) + except Exception as e: + self.show_error(str(e)) + password = self.get_wallet_password(message) + return password + + def on_close(self): + try: + if not self.disable_plugin: + close_window = BalBuildWillDialog(self) + close_window.build_will_task() + self.save_willitems() + self.heirs_tab.close() + self.will_tab.close() + self.tools_menu.removeAction(self.tools_menu.willexecutors_action) + self.window.toggle_tab(self.heirs_tab) + self.window.toggle_tab(self.will_tab) + self.window.tabs.update() + except Exception: + pass + + def ask_password_and_sign_transactions(self, callback=None): + 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() + try: + self.will_list.update() + except Exception: + pass + if callback: + try: + callback() + except Exception as e: + raise e + + def on_failure(exc_info): + self.logger.info("sign fail: {}".format(exc_info)) + self.show_error(exc_info) + + password = self.get_wallet_password() + task = partial(self.sign_transactions, password) + 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 on_success(sulcess): + self.will_list.update() + if sulcess: + self.logger.info("error, some transaction was not sent") + self.show_warning(_("Some transaction was not broadcasted")) + return + self.logger.debug("OK, sulcess transaction was sent") + self.show_message( + _("All transactions are broadcasted to respective Will-Executors") + ) + + def on_failure(err): + a,b,c = err + self.logger.error(f"fail to broadcast transactions:{err}") + self.logger.error(f"error: {b}") + self.logger.error(f"traceback ") + tb = c + while tb is not None: + frame = tb.tb_frame + self.logger.error("file:", frame.f_code.co_filename) + self.logger.error("name:", frame.f_code.co_name) + self.logger.error("line:", tb.tb_lineno) + self.logger.error("lasti:", tb.tb_lasti) + tb = tb.tb_next + + task = partial(self.push_transactions_to_willexecutors, force) + 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) + + def getMsg(willexecutors): + msg = "Broadcasting Transactions to Will-Executors:\n" + for url in willexecutors: + msg += f"{url}:\t{willexecutors[url]['broadcast_status']}\n" + return msg + + error = False + for url in willexecutors: + willexecutor = willexecutors[url] + self.waiting_dialog.update(getMsg(willexecutors)) + if "txs" in willexecutor: + try: + if Willexecutors.push_transactions_to_willexecutor( + willexecutors[url] + ): + for wid in willexecutors[url]["txsids"]: + self.willitems[wid].set_status("PUSHED", True) + willexecutors[url]["broadcast_status"] = _("Success") + else: + for wid in willexecutors[url]["txsids"]: + self.willitems[wid].set_status("PUSH_FAIL", True) + error = True + willexecutors[url]["broadcast_status"] = _("Failed") + del willexecutor["txs"] + except Willexecutors.AlreadyPresentException: + for wid in willexecutor["txsids"]: + self.waiting_dialog.update( + "checking {} - {} : {}".format( + self.willitems[wid].we["url"], wid, "Waiting" + ) + ) + w = self.willitems[wid] + w.set_check_willexecutor( + Willexecutors.check_transaction( + wid, + w.we["url"] + ) + ) + self.waiting_dialog.update( + "checked {} - {} : {}".format( + self.willitems[wid].we["url"], + wid, + self.willitems[wid].get_status("CHECKED"), + ) + ) + + if error: + 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_will(self): + try: + Util.export_meta_gui(self.window, _("will.json"), self.export_json_file) + except Exception as e: + self.show_error(str(e)) + raise e + + def import_will(self): + def sulcess(): + self.will_list.update_will(self.willitems) + + import_meta_gui(self.window, _("will"), self.import_json_file, sulcess) + + 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 check_transactions_task(self, will): + start = time.time() + for wid, w in will.items(): + self.waiting_dialog.update( + "checking transaction: {}\n willexecutor: {}".format(wid, w.we["url"]) + ) + + w.set_check_willexecutor(Willexecutors.check_transaction(wid, w.we["url"])) + + + if time.time() - start < 3: + time.sleep(3 - (time.time() - start)) + + def check_transactions(self, will): + def on_success(result): + del self.waiting_dialog + self.update_all() + pass + + def on_failure(e): + self.logger.error(f"error checking transactions {e}") + pass + + task = partial(self.check_transactions_task, will) + msg = _("Check Transaction") + self.waiting_dialog = BalWaitingDialog( + self, msg, task, on_success, on_failure, exe=False + ) + self.waiting_dialog.exe() + + def ping_willexecutors_task(self, wes): + self.logger.info("ping willexecutots task") + pinged = [] + failed = [] + + def get_title(): + msg = _("Ping Will-Executors:") + msg += "\n\n" + for url in wes: + urlstr = "{:<50}: ".format(url[:50]) + if url in pinged: + urlstr += "Ok" + elif url in failed: + urlstr += "Ko" + else: + urlstr += "--" + urlstr += "\n" + msg += urlstr + + return msg + + for url, we in wes.items(): + try: + self.waiting_dialog.update(get_title()) + except Exception: + pass + wes[url] = Willexecutors.get_info_task(url, we) + if wes[url]["status"] == "KO": + failed.append(url) + else: + pinged.append(url) + + def ping_willexecutors(self, wes, parent=None): + if not parent: + parent = self + + def on_success(result): + # del self.waiting_dialog + try: + parent.will_executor_list_widget.update() + except Exception as e: + pass + try: + parent.willexecutors_list.update() + except Exception as e: + pass + + def on_failure(e): + self.logger.error(f"fail to ping willexecutors: {e}") + pass + + self.logger.info("ping willexecutors") + task = partial(self.ping_willexecutors_task, wes) + msg = _("Ping Will-Executors") + self.waiting_dialog = BalWaitingDialog( + self, msg, task, on_success, on_failure, exe=False + ) + self.waiting_dialog.exe() + + def preview_modal_dialog(self): + self.dw = WillDetailDialog(self) + self.dw.show() + + def update_all(self): + try: + Will.add_willtree(self.willitems) + all_utxos = self.wallet.get_utxos() + utxos_list = Will.utxos_strs(all_utxos) + Will.check_invalidated( + self.willitems, utxos_list, self.wallet + ) + + self.will_list.update_will(self.willitems) + self.heirs_tab.update() + self.will_tab.update() + self.will_list.update() + except Exception as e: + _logger.error(f"error while updating window: {e}") + + +def add_widget(grid, label, widget, row, help_): + grid.addWidget(QLabel(_(label)), row, 0) + grid.addWidget(widget, row, 1) + grid.addWidget(HelpButton(help_), row, 2) + + +class _LockTimeEditor: + min_allowed_value = NLOCKTIME_MIN + max_allowed_value = NLOCKTIME_MAX + + def get_locktime(self) -> Optional[int]: + raise NotImplementedError() + + def set_locktime(self, x: Any, force=True) -> None: + raise NotImplementedError() + + def is_acceptable_locktime(cls, x: Any) -> bool: + if not x: # e.g. empty string + return True + try: + x = int(x) + except Exception: + return False + return cls.min_allowed_value <= x <= cls.max_allowed_value + + + """ + HeirsLockTimeEdit - A custom QWidget for editing locktime values in the context of heirs distribution. + + This widget combines raw locktime editing with date-based selection and provides + additional functionality for managing locktime values in a heir inheritance scenario. + + Features: + - Supports both raw locktime values and human-readable date formats + - Emits valueEdited signal when the locktime value changes + - Provides threshold-based validation for locktime values + - Integrates with heir distribution workflows + + Attributes: + valueEdited (pyqtSignal): Signal emitted when the locktime value is edited + locktime_threshold (int): Minimum threshold value for locktime (default: 50000000) + + Args: + parent: Optional parent QWidget + default_index (int): Default index for the combo box (default: 1) + """ + +class HeirsLockTimeEdit(QWidget, _LockTimeEditor): + valueEdited = pyqtSignal() + locktime_threshold = 50000000 + + def __init__(self, parent=None, default_index=1): + QWidget.__init__(self, parent) + + hbox = QHBoxLayout() + self.setLayout(hbox) + hbox.setContentsMargins(0, 0, 0, 0) + hbox.setSpacing(0) + + self.locktime_raw_e = LockTimeRawEdit(self, time_edit=self) + self.locktime_date_e = LockTimeDateEdit(self, time_edit=self) + self.editors = [self.locktime_raw_e, self.locktime_date_e] + + self.combo = QComboBox() + options = [_("Raw"), _("Date")] + self.option_index_to_editor_map = { + 0: self.locktime_raw_e, + 1: self.locktime_date_e, + } + self.combo.addItems(options) + + self.editor = self.option_index_to_editor_map[default_index] + self.combo.currentIndexChanged.connect(self.on_current_index_changed) + self.combo.setCurrentIndex(default_index) + self.on_current_index_changed(default_index) + + hbox.addWidget(self.combo) + for w in self.editors: + hbox.addWidget(w) + hbox.addStretch(1) + # spacer_widget = QWidget() + # spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + # hbox.addWidget(spacer_widget) + + self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit) + self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit) + self.combo.currentIndexChanged.connect(self.valueEdited.emit) + + def on_current_index_changed(self, i): + for w in self.editors: + w.setVisible(False) + w.setEnabled(False) + prev_locktime = self.editor.get_locktime() + self.editor = self.option_index_to_editor_map[i] + if self.editor.is_acceptable_locktime(prev_locktime): + self.editor.set_locktime(prev_locktime, force=True) + self.editor.setVisible(True) + self.editor.setEnabled(True) + + def get_locktime(self) -> Optional[str]: + return self.editor.get_locktime() + + def set_index(self, index): + self.combo.setCurrentIndex(index) + self.on_current_index_changed(index) + + def set_locktime(self, x: Any, force=True) -> None: + self.editor.set_locktime(x, force) + + +class LockTimeRawEdit(QLineEdit, _LockTimeEditor): + def __init__(self, parent=None, time_edit=None): + QLineEdit.__init__(self, parent) + self.setFixedWidth(14 * char_width_in_lineedit()) + self.textChanged.connect(self.numbify) + self.isdays = False + self.isyears = False + self.isblocks = False + self.time_edit = time_edit + + def replace_str(self, text): + return str(text).replace("d", "").replace("y", "").replace("b", "") + + def checkbdy(self, s, pos, appendix): + try: + charpos = pos - 1 + charpos = max(0, charpos) + charpos = min(len(s) - 1, charpos) + if appendix == s[charpos]: + s = self.replace_str(s) + appendix + pos = charpos + except Exception: + pass + return pos, s + + def numbify(self): + text = self.text().strip() + # chars = '0123456789bdy' removed the option to choose locktime by block + chars = "0123456789dy" + pos = self.cursorPosition() + pos = len("".join([i for i in text[:pos] if i in chars])) + s = "".join([i for i in text if i in chars]) + self.isdays = False + self.isyears = False + self.isblocks = False + + pos, s = self.checkbdy(s, pos, "d") + pos, s = self.checkbdy(s, pos, "y") + pos, s = self.checkbdy(s, pos, "b") + + if "d" in s: + self.isdays = True + if "y" in s: + self.isyears = True + if "b" in s: + self.isblocks = True + + if self.isdays: + s = self.replace_str(s) + "d" + if self.isyears: + s = self.replace_str(s) + "y" + if self.isblocks: + s = self.replace_str(s) + "b" + + self.set_locktime(s, force=False) + # setText sets Modified to False. Instead we want to remember + # if updates were because of user modification. + self.setModified(self.hasFocus()) + self.setCursorPosition(pos) + + def get_locktime(self) -> Optional[str]: + try: + return str(self.text()) + except Exception: + return None + + def set_locktime(self, x: Any, force=True) -> None: + out = str(x) + if "d" in out: + out = self.replace_str(x) + "d" + elif "y" in out: + out = self.replace_str(x) + "y" + elif "b" in out: + out = self.replace_str(x) + "b" + else: + try: + out = int(x) + except Exception: + self.setText("") + return + out = max(out, self.min_allowed_value) + out = min(out, self.max_allowed_value) + self.setText(str(out)) + + +class LockTimeHeightEdit(LockTimeRawEdit): + max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + + def __init__(self, parent=None, time_edit=None): + LockTimeRawEdit.__init__(self, parent) + self.setFixedWidth(20 * char_width_in_lineedit()) + self.time_edit = time_edit + + def paintEvent(self, event): + super().paintEvent(event) + panel = QStyleOptionFrame() + self.initStyleOption(panel) + textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self) + textRect.adjust(2, 0, -10, 0) + painter = QPainter(self) + painter.setPen(ColorScheme.GRAY.as_color()) + painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height") + + +def get_max_allowed_timestamp() -> int: + ts = NLOCKTIME_MAX + # Test if this value is within the valid timestamp limits (which is platform-dependent). + # see #6170 + try: + datetime.fromtimestamp(ts) + except (OSError, OverflowError): + ts = 2**31 - 1 # INT32_MAX + datetime.fromtimestamp(ts) # test if raises + return ts + + +class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor): + min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1 + max_allowed_value = get_max_allowed_timestamp() + + def __init__(self, parent=None, time_edit=None): + QDateTimeEdit.__init__(self, parent) + self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value)) + self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value)) + self.setDateTime(QDateTime.currentDateTime()) + self.time_edit = time_edit + + def get_locktime(self) -> Optional[int]: + dt = self.dateTime().toPyDateTime() + locktime = int(time.mktime(dt.timetuple())) + return locktime + + def set_locktime(self, x: Any, force=False) -> None: + if not self.is_acceptable_locktime(x): + self.setDateTime(QDateTime.currentDateTime()) + return + try: + x = int(x) + except Exception: + self.setDateTime(QDateTime.currentDateTime()) + return + dt = datetime.fromtimestamp(x) + self.setDateTime(dt) + + +_NOT_GIVEN = object() # sentinel value + + +class PercAmountEdit(BTCAmountEdit): + def __init__( + self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN + ): + super().__init__(decimal_point, is_int, parent, max_amount=max_amount) + + def numbify(self): + text = self.text().strip() + if text == "!": + self.shortcut.emit() + return + pos = self.cursorPosition() + chars = "0123456789%" + chars += DECIMAL_POINT + + s = "".join([i for i in text if i in chars]) + + if "%" in s: + self.is_perc = True + s = s.replace("%", "") + else: + self.is_perc = False + + if DECIMAL_POINT in s: + p = s.find(DECIMAL_POINT) + s = s.replace(DECIMAL_POINT, "") + s = s[:p] + DECIMAL_POINT + s[p : p + 8] + if self.is_perc: + s += "%" + + self.setText(s) + self.setModified(self.hasFocus()) + self.setCursorPosition(pos) + + def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]: + try: + text = text.replace(DECIMAL_POINT, ".") + text = text.replace("%", "") + return (Decimal)(text) + except Exception: + return None + + def _get_text_from_amount(self, amount): + out = super()._get_text_from_amount(amount) + if self.is_perc: + out += "%" + return out + + def paintEvent(self, event): + QLineEdit.paintEvent(self, event) + if self.base_unit: + panel = QStyleOptionFrame() + self.initStyleOption(panel) + textRect = self.style().subElementRect( + QStyle.SubElement.SE_LineEditContents, panel, self + ) + textRect.adjust(2, 0, -10, 0) + painter = QPainter(self) + painter.setPen(ColorScheme.GRAY.as_color()) + if len(self.text()) == 0: + painter.drawText( + textRect, + int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), + self.base_unit() + " or perc value", + ) + + +class BalDialog(WindowModalDialog): + def __init__(self, parent, bal_plugin, title=None, icon="icons/bal32x32.png"): + self.parent = parent + WindowModalDialog.__init__(self, parent, title) + # WindowModalDialog.__init__(self,parent) + self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon))) + + +class BalWizardDialog(BalDialog): + def __init__(self, bal_window: "BalWindow"): + assert bal_window + BalDialog.__init__( + self, bal_window.window, bal_window.bal_plugin, _("Bal Wizard Setup") + ) + self.setMinimumSize(800, 400) + self.bal_window = bal_window + self.parent = bal_window.window + self.layout = QVBoxLayout(self) + self.widget = BalWizardHeirsWidget( + bal_window, self, self.on_next_heir, None, self.on_cancel_heir + ) + self.layout.addWidget(self.widget) + + def next_widget(self, widget): + self.layout.removeWidget(self.widget) + self.widget.close() + self.widget = widget + self.layout.addWidget(self.widget) + # self.update() + # self.repaint() + + def on_next_heir(self): + self.next_widget( + BalWizardLocktimeAndFeeWidget( + self.bal_window, + self, + self.on_next_locktimeandfee, + self.on_previous_heir, + self.on_cancel_heir, + ) + ) + + def on_previous_heir(self): + self.next_widget( + BalWizardHeirsWidget( + self.bal_window, self, self.on_next_heir, None, self.on_cancel_heir + ) + ) + + def on_cancel_heir(self): + pass + + def on_next_wedonwload(self): + self.next_widget( + BalWizardWEWidget( + self.bal_window, + self, + self.on_next_we, + self.on_next_locktimeandfee, + self.on_cancel_heir, + ) + ) + + def on_next_we(self): + close_window = BalBuildWillDialog(self.bal_window) + close_window.build_will_task() + self.close() + # self.next_widget(BalWizardLocktimeAndFeeWidget(self.bal_window,self,self.on_next_locktimeandfee,self.on_next_wedonwload,self.on_next_wedonwload.on_cancel_heir)) + + def on_next_locktimeandfee(self): + self.next_widget( + BalWizardWEDownloadWidget( + self.bal_window, + self, + self.on_next_wedonwload, + self.on_next_heir, + self.on_cancel_heir, + ) + ) + + def on_accept(self): + self.bal_window.update_all() + pass + + def on_reject(self): + pass + + def on_close(self): + self.bal_window.update_all() + pass + + def closeEvent(self, event): + + self.bal_window.heir_list_widget.update_will_settings() + pass + + +class BalWizardWidget(QWidget): + title = None + message = None + + def __init__( + self, bal_window: "BalWindow", parent, on_next, on_previous, on_cancel + ): + QWidget.__init__(self, parent) + self.vbox = QVBoxLayout(self) + self.bal_window = bal_window + self.parent = parent + self.on_next = on_next + self.on_cancel = on_cancel + self.titleLabel = QLabel(self.title) + self.vbox.addWidget(self.titleLabel) + self.messageLabel = QLabel(_(self.message)) + self.vbox.addWidget(self.messageLabel) + + self.content = self.get_content() + self.content_container = QWidget() + self.containrelayout = QVBoxLayout(self.content_container) + self.containrelayout.addWidget(self.content) + + self.vbox.addWidget(self.content_container) + + spacer_widget = QWidget() + spacer_widget.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) + self.vbox.addWidget(spacer_widget) + + self.buttons = [] + if on_previous: + self.on_previous = on_previous + self.previous_button = QPushButton(_("Previous")) + self.previous_button.clicked.connect(self._on_previous) + self.buttons.append(self.previous_button) + + self.next_button = QPushButton(_("Next")) + self.next_button.clicked.connect(self._on_next) + self.buttons.append(self.next_button) + + self.abort_button = QPushButton(_("Cancel")) + self.abort_button.clicked.connect(self._on_cancel) + self.buttons.append(self.abort_button) + + self.vbox.addLayout(Buttons(*self.buttons)) + + def _on_cancel(self): + self.on_cancel() + self.parent.close() + + def _on_next(self): + if self.validate(): + self.on_next() + + def _on_previous(self): + self.on_previous() + + def get_content(self): + pass + + def validate(self): + return True + + +class BalWizardHeirsWidget(BalWizardWidget): + title = "Bitcoin After Life Heirs" + message = ( + "Please add your heirs\n remember that 100% of wallet balance will be spent" + ) + + def get_content(self): + self.heir_list_widget = HeirListWidget(self.bal_window, self) + button_add = QPushButton(_("Add")) + button_add.clicked.connect(self.add_heir) + button_import = QPushButton(_("Import")) + button_import.clicked.connect(self.import_from_file) + button_export = QPushButton(_("Export")) + button_export.clicked.connect(self.export_to_file) + widget = QWidget() + vbox = QVBoxLayout(widget) + vbox.addWidget(self.heir_list_widget) + vbox.addLayout(Buttons(button_add, button_import, button_export)) + return widget + + def import_from_file(self): + self.bal_window.import_heirs() + self.heir_list_widget.update() + + def export_to_file(self): + self.bal_window.export_heirs() + + def add_heir(self): + self.bal_window.new_heir_dialog() + self.heir_list_widget.update() + + def validate(self): + return True + + +class BalWizardWEDownloadWidget(BalWizardWidget): + title = _("Bitcoin After Life Will-Executors") + message = _("Choose willexecutors download method") + + def get_content(self): + # question = QLabel() + self.combo = QComboBox() + self.combo.addItems( + [ + "Automatically download and select willexecutors", + "Only download willexecutors list", + "Import willexecutor list from file", + "Manual", + ] + ) + # heir_name.setFixedWidth(32 * char_width_in_lineedit()) + return self.combo + + def validate(self): + return True + + def _on_next(self): + + index = self.combo.currentIndex() + _logger.debug(f"selected index:{index}") + if index < 3: + self.bal_window.willexecutors = Willexecutors.get_willexecutors( + self.bal_window.bal_plugin + ) + + if index == 2: + + def doNothing(): + self.bal_window.willexecutors.update(self.willexecutors) + Willexecutors.save( + self.bal_window.bal_plugin, self.bal_window.willexecutors + ) + pass + + import_meta_gui( + self.bal_window.window, + _("willexecutors"), + self.import_json_file, + doNothing, + ) + + if index < 2: + + def on_success(willexecutors): + self.bal_window.willexecutors.update(willexecutors) + self.bal_window.ping_willexecutors( + self.bal_window.willexecutors, False + ) + if index < 1: + for we in self.bal_window.willexecutors: + if self.bal_window.willexecutors[we]["status"] == 200: + self.bal_window.willexecutors[we]["selected"] = True + Willexecutors.save( + self.bal_window.bal_plugin, self.bal_window.willexecutors + ) + + def on_failure(fail): + _logger.debug(f"Failed to download willexecutors list {fail}") + pass + + task = partial(Willexecutors.download_list,self.bal_window.willexecutors) + msg = _("Downloading Will-Executors list") + self.waiting_dialog = BalWaitingDialog( + self.bal_window, msg, task, on_success, on_failure, exe=False + ) + self.waiting_dialog.exe() + + elif index == 3: + # TODO DO NOTHING + pass + + if self.validate(): + return self.on_next() + + def import_json_file(self, path): + data = read_json_file(path) + data = self._validate(data) + self.willexecutors = data + + def _validate(self, data): + return data + + +class BalWizardWEWidget(BalWizardWidget): + title = "Bitcoin After Life Will-Executors" + message = _("Configure and select your willexecutors") + + def get_content(self): + widget = QWidget() + vbox = QVBoxLayout(widget) + vbox.addWidget( + WillExecutorWidget( + self, + self.bal_window, + Willexecutors.get_willexecutors(self.bal_window.bal_plugin), + ) + ) + return widget + + +class BalWizardLocktimeAndFeeWidget(BalWizardWidget): + title = "Bitcoin After Life Will Settings" + message = _("") + + def get_content(self): + widget = QWidget() + self.heir_locktime = HeirsLockTimeEdit(widget, 0) + will_settings = self.bal_window.bal_plugin.WILL_SETTINGS.get() + will_settings = self.bal_window.will_settings + self.heir_locktime.set_locktime(will_settings["locktime"]) + + def on_heir_locktime(): + if not self.heir_locktime.get_locktime(): + self.heir_locktime.set_locktime("1y") + self.bal_window.will_settings["locktime"] = ( + self.heir_locktime.get_locktime() + if self.heir_locktime.get_locktime() + else "1y" + ) + self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings) + + self.heir_locktime.valueEdited.connect(on_heir_locktime) + + self.heir_threshold = HeirsLockTimeEdit(widget, 0) + self.heir_threshold.set_locktime(will_settings["threshold"]) + + def on_heir_threshold(): + if not self.heir_threshold.get_locktime(): + self.heir_threshold.set_locktime("180d") + + self.bal_window.will_settings["threshold"] = ( + self.heir_threshold.get_locktime() + ) + self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings) + + self.heir_threshold.valueEdited.connect(on_heir_threshold) + + self.heir_tx_fees = QSpinBox(widget) + + self.heir_tx_fees.setMinimum(1) + self.heir_tx_fees.setMaximum(10000) + self.heir_tx_fees.setValue(will_settings["baltx_fees"]) + + def on_heir_tx_fees(): + if not self.heir_tx_fees.value(): + self.heir_tx_fees.set_value(1) + self.bal_window.will_settings["baltx_fees"] = self.heir_tx_fees.value() + self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings) + + self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees) + + def make_hlayout(label, twidget, help_text): + tw = QWidget() + hlayout = QHBoxLayout(tw) + hlayout.addWidget(QLabel(label)) + hlayout.addWidget(twidget) + hlayout.addWidget(HelpButton(help_text)) + hlayout.addStretch(1) + spacer_widget = QWidget() + spacer_widget.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) + hlayout.addWidget(spacer_widget) + return tw + + layout = QVBoxLayout(widget) + + layout.addWidget( + make_hlayout( + _("Delivery Time:"), + self.heir_locktime, + _( + "Locktime* to be used in the transaction\n" + + "if you choose Raw, you can insert various options based on suffix:\n" + + " - d: number of days after current day(ex: 1d means tomorrow)\n" + + " - y: number of years after currrent day(ex: 1y means one year from today)\n" + + "* locktime can be anticipated to update will\n" + ), + ) + ) + layout.addWidget( + make_hlayout( + _("Check Alive:"), + self.heir_threshold, + _( + "Check to ask for invalidation.\n" + + "When less then this time is missing, ask to invalidate.\n" + + "If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n" + + "if you choose Raw, you can insert various options based on suffix:\n" + + " - d: number of days after current day(ex: 1d means tomorrow).\n" + + " - y: number of years after currrent day(ex: 1y means one year from today).\n\n" + ), + ) + ) + layout.addWidget( + make_hlayout( + _("Fees(sats/vbyte):"), + self.heir_tx_fees, + ("Fee to be used in the transaction"), + ) + ) + + spacer_widget = QWidget() + spacer_widget.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) + layout.addWidget(spacer_widget) + return widget + + +class BalWaitingDialog(BalDialog): + updatemessage = pyqtSignal([str], arguments=["message"]) + + def __init__( + self, + bal_window: "BalWindow", + message: str, + task, + on_success=None, + on_error=None, + on_cancel=None, + exe=True, + ): + assert bal_window + BalDialog.__init__( + self, bal_window.window, bal_window.bal_plugin, _("Please wait") + ) + self.message_label = QLabel(message) + vbox = QVBoxLayout(self) + vbox.addWidget(self.message_label) + self.updatemessage.connect(self.update_message) + if on_cancel: + self.cancel_button = CancelButton(self) + self.cancel_button.clicked.connect(on_cancel) + vbox.addLayout(Buttons(self.cancel_button)) + self.accepted.connect(self.on_accepted) + self.task = task + self.on_success = on_success + self.on_error = on_error + self.on_cancel = on_cancel + if exe: + self.exe() + + def exe(self): + self.thread = TaskThread(self) + self.thread.finished.connect(self.deleteLater) # see #3956 + self.thread.finished.connect(self.finished) + self.thread.add(self.task, self.on_success, self.accept, self.on_error) + self.exec() + + def hello(self): + pass + + def finished(self): + _logger.info("finished") + + def wait(self): + self.thread.wait() + + def on_accepted(self): + self.thread.stop() + + def update_message(self, msg): + self.message_label.setText(msg) + + def update(self, msg): + self.updatemessage.emit(msg) + + def getText(self): + return self.message_label.text() + + def closeEvent(self, event): + self.thread.stop() + + +class BalBlockingWaitingDialog(BalDialog): + def __init__(self, bal_window: "BalWindow", message: str, task: Callable[[], Any]): + BalDialog.__init__(self, bal_window, bal_window.bal_plugin, _("Please wait")) + self.message_label = QLabel(message) + vbox = QVBoxLayout(self) + vbox.addWidget(self.message_label) + self.finished.connect(self.deleteLater) # see #3956 + # show popup + self.show() + # refresh GUI; needed for popup to appear and for message_label to get drawn + # QCoreApplication.processEvents() + # QCoreApplication.processEvents() + try: + # block and run given task + task() + finally: + # close popup + self.accept() + + +class bal_checkbox(QCheckBox): + def __init__(self, variable, on_click=None): + QCheckBox.__init__(self) + self.setChecked(variable.get()) + self.on_click = on_click + + def on_check(v): + variable.set(v == 2) + variable.get() + if self.on_click: + self.on_click() + + self.stateChanged.connect(on_check) + + +class BalBuildWillDialog(BalDialog): + updatemessage = pyqtSignal() + COLOR_WARNING = "#cfa808" + COLOR_ERROR = "#ff0000" + COLOR_OK = "#05ad05" + + def __init__(self, bal_window, parent=None): + """Initialize the Build Will dialog. + + Args: + bal_window (BalWindow): The main application window + parent (QWidget, optional): Parent widget. Defaults to None. + + Initializes: + - Main UI components (message label, container widget) + - Message queue system with debounce timer + - Layout management + - Network connection + """ + if not parent: + parent = bal_window.window + BalDialog.__init__(self, parent, bal_window.bal_plugin, _("Building Will")) + self.parent = parent + self.updatemessage.connect(self.msg_update) + self.bal_window = bal_window + self.bal_plugin = bal_window.bal_plugin + + # Main message label + self.message_label = QLabel(_("Building Will:")) + self.vbox = QVBoxLayout(self) + self.vbox.addWidget(self.message_label, 0) + + # Container for dynamic messages + self.qwidget = QWidget(self) + self.vbox.addWidget(self.qwidget, 1) + + # Layout for messages with reduced spacing + self.labelsbox = QVBoxLayout(self.qwidget) + self.labelsbox.setContentsMargins(0, 0, 0, 0) + self.labelsbox.setSpacing(4) # Reduced spacing between messages + + # Set minimum dimensions + self.setMinimumWidth(600) + self.setMinimumHeight(100) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) + + # Message queue implementation for efficient updates + self._message_queue = [] # Thread-safe message queue + self._message_timer = QTimer(self) + self._message_timer.setSingleShot(True) + self._message_timer.setInterval(50) # Debounce interval: 50ms + self._message_timer.timeout.connect(self._process_message_queue) + + # Other initialization + self.labels = [] # Immediate message storage + self.check_row = None + self.inval_row = None + self.build_row = None + self.sign_row = None + self.push_row = None + self.network = Network.get_instance() + self._stopping = False + + self.check_row = None + self.inval_row = None + self.build_row = None + self.sign_row = None + self.push_row = None + self.network = Network.get_instance() + self._stopping = False + self.thread = TaskThread(self) + self.thread.finished.connect(self.task_finished) # see #3956 + + def task_finished(self): + pass + + def build_will_task(self): + _logger.debug("build will task to be started") + self.thread.add( + self.task_phase1, + on_success=self.on_success_phase1, + on_done=self.on_accept, + on_error=self.on_error_phase1, + ) + self.show() + self.exec() + + def task_phase1(self): + txs=None + _logger.debug("close plugin phase 1 started") + varrow = self.msg_set_status("checking variables") + try: + self.bal_window.init_class_variables() + except CheckAliveException 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 + ) + if tx: + _logger.debug("during phase1 CAE: {}, Continue to invalidate".format(cae)) + self.msg_set_checking(self.msg_warning("Check Alive Threshold Passed: you have to Invalidate your old Will")) + else: + raise cae + return None, tx + + + except Exception as e: + raise e + try: + _logger.debug("checking variables") + Will.check_amounts( + self.bal_window.heirs, + self.bal_window.willexecutors, + self.bal_window.window.wallet.get_utxos(), + self.bal_window.date_to_check, + self.bal_window.window.wallet.dust_threshold(), + ) + _logger.debug("variables ok") + self.msg_set_status("checking variables:", varrow, "Ok", self.COLOR_OK) + except AmountException: + self.msg_set_status( + "checking variables", + varrow, + _( + "In the inheritance process, " + + "the entire wallet will always be fully emptied. \n" + + "Your settings require an adjustment of the amounts" + ), + self.COLOR_WARNING, + ) + + self.msg_set_checking() + have_to_build = False + try: + self.bal_window.check_will() + self.msg_set_checking(self.msg_ok()) + except WillExpiredException: + _logger.debug("expired") + self.msg_set_checking("Expired") + 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 + ) + except NoHeirsException as e: + _logger.debug("no heirs") + self.msg_set_checking("No Heirs") + raise e + except NotCompleteWillException as e: + _logger.debug(f"not complete {e} true") + message = False + have_to_build = True + if isinstance(e, HeirChangeException): + message = _("Heirs changed:") + elif isinstance(e, WillExecutorNotPresent): + message = _("Will-Executor not present") + elif isinstance(e, WillexecutorChangeException): + message = _("Will-Executor changed") + elif isinstance(e, TxFeesChangedException): + message = _("Txfees are changed") + elif isinstance(e, HeirNotFoundException): + message = _("Heir not found") + if message: + _logger.debug(f"message: {message}") + self.msg_set_checking(message) + else: + self.msg_set_checking("New") + + if have_to_build: + self.msg_set_building() + try: + txs = self.bal_window.build_will() + if not txs: + self.msg_set_status( + _("Balance is too low. No transaction was built"), + None, + _("Skipped"), + self.COLOR_ERROR, + ) + return False,None + + self.bal_window.check_will() + for wid in Will.only_valid(self.bal_window.willitems): + self.bal_window.wallet.set_label(wid, "BAL Transaction") + self.msg_set_building(self.msg_ok()) + except WillExecutorNotPresent: + self.msg_set_status( + _("Will-Executor excluded"), None, _("Skipped"), self.COLOR_ERROR + ) + + except Exception as e: + self.msg_set_building(self.msg_error(e)) + return False, None + + # excluded_heirs = [] + for wid in Will.only_valid(self.bal_window.willitems): + heirs = self.bal_window.willitems[wid].heirs + for hid, heir in heirs.items(): + if "DUST" in str(heir[HEIR_REAL_AMOUNT]): + self.msg_set_status( + f"{hid},{heir[HEIR_DUST_AMOUNT]} is DUST", + None, + f"Excluded from will {wid}", + self.COLOR_WARNING, + ) + + have_to_sign = False + for wid in Will.only_valid(self.bal_window.willitems): + if not self.bal_window.willitems[wid].get_status("COMPLETE"): + have_to_sign = True + break + return have_to_sign, txs + + def on_accept(self): + self.bal_window.update_all() + pass + + def on_accept_phase2(self): + self.bal_window.update_all() + pass + + def on_error_push(self): + pass + + def wait(self, secs): + wait_row = None + for i in range(secs, 0, -1): + if self._stopping: + return + wait_row = self.msg_edit_row(_(f"Please wait {i}secs"), wait_row) + time.sleep(1) + self.msg_del_row(wait_row) + + def loop_broadcast_invalidating(self, tx): + self.msg_set_invalidating("Broadcasting") + try: + tx.add_info_from_wallet(self.bal_window.wallet) + self.network.run_from_another_thread(tx.add_info_from_network(self.network)) + txid = self.network.run_from_another_thread( + self.network.broadcast_transaction(tx, timeout=120), timeout=120 + ) + self.msg_set_invalidating(self.msg_ok()) + if not txid: + _logger.debug(f"should not be none txid: {txid}") + + except TxBroadcastError as e: + _logger.error(f"fail to broadcast transaction:{e}") + msg = e.get_message_for_gui() + self.msg_set_invalidating(self.msg_error(msg)) + except BestEffortRequestFailed as e: + self.msg_set_invalidating(self.msg_error(e)) + + def loop_push(self): + self.msg_set_pushing(_("Broadcasting")) + retry = False + try: + + willexecutors = Willexecutors.get_willexecutor_transactions( + self.bal_window.willitems + ) + for url, willexecutor in willexecutors.items(): + try: + if Willexecutors.is_selected( + self.bal_window.willexecutors.get(url) + ): + _logger.debug(f"{url}: {willexecutor}") + if not Willexecutors.push_transactions_to_willexecutor( + willexecutor + ): + for wid in willexecutor["txsids"]: + self.bal_window.willitems[wid].set_status( + "PUSH_FAIL", True + ) + retry = True + else: + for wid in willexecutor["txsids"]: + self.bal_window.willitems[wid].set_status( + "PUSHED", True + ) + except Willexecutors.AlreadyPresentException: + for wid in willexecutor["txsids"]: + row = self.msg_edit_row( + "checking {} - {} : {}".format( + self.bal_window.willitems[wid].we["url"], wid, "Waiting" + ) + ) + self.bal_plugin = self.bal_window.bal_plugin + w = self.bal_window.willitems[wid] + + w.set_check_willexecutor(Willexecutors.check_transaction(wid, w.we["url"])) + row = self.msg_edit_row( + "checked {} - {} : {}".format( + self.bal_window.willitems[wid].we["url"], + wid, + self.bal_window.willitems[wid].get_status("CHECKED"), + ), + row, + ) + + except Exception as e: + _logger.error(f"loop push error:{e}") + raise e + if retry: + raise Exception("retry") + + except Exception as e: + self.msg_set_pushing(self.msg_error(e)) + self.wait(10) + if not self._stopping: + self.loop_push() + + def invalidate_task(self, password, bal_window, tx): + _logger.debug(f"invalidate tx: {tx}") + # fee_per_byte = bal_window.will_settings.get("baltx_fees", 1) + tx = self.bal_window.wallet.sign_transaction(tx, password) + try: + if tx: + if tx.is_complete(): + self.loop_broadcast_invalidating(tx) + self.wait(5) + else: + raise Exception("tx not complete") + else: + raise Exception("not tx") + except Exception as e: + (f"exception:{e}") + self.msg_set_invalidating(f"Error: {e}") + raise Exception("Impossible to sign") + + def on_success_invalidate(self, success): + self.thread.add( + self.task_phase1, + on_success=self.on_success_phase1, + on_done=self.on_accept, + on_error=self.on_error_phase1, + ) + + def on_success_phase1(self, result): + self.have_to_sign, tx = list(result) + #if not tx: + # self.msg_edit_row(self.msg_error("Error, no tx was built")) + # return + _logger.debug("have to sign {}".format(self.have_to_sign)) + password = None + if self.have_to_sign is None: + _logger.debug("have to invalidate") + self.msg_set_invalidating() + # need to sign invalidate and restart phase 1 + + password = self.bal_window.get_wallet_password( + _("Invalidate your old will"), parent=self + ) + if password is False: + self.msg_set_invalidating(_("Aborted")) + self.wait(3) + self.close() + return + self.thread.add( + partial(self.invalidate_task, password, self.bal_window, tx), + on_success=self.on_success_invalidate, + on_done=self.on_accept, + on_error=self.on_error, + ) + + return + + elif self.have_to_sign: + password = self.bal_window.get_wallet_password( + _("Sign your will"), parent=self + ) + if password is False: + self.msg_set_signing(_("Aborted")) + else: + self.msg_set_signing(_("Nothing to do")) + self.thread.add( + partial(self.task_phase2, password), + on_success=self.on_success_phase2, + on_done=self.on_accept_phase2, + on_error=self.on_error_phase2, + ) + return + + def on_success_phase2(self, arg=False): + self.thread.stop() + self.bal_window.save_willitems() + self.msg_edit_row(_("Finished")) + self.close() + + def closeEvent(self, event): + self._stopping = True + self.thread.stop() + + def task_phase2(self, password): + if self.have_to_sign: + try: + if txs := self.bal_window.sign_transactions(password): + for txid, tx in txs.items(): + self.bal_window.willitems[txid].tx = copy.deepcopy(tx) + self.bal_window.save_willitems() + self.msg_set_signing(self.msg_ok()) + except Exception as e: + self.msg_set_signing(self.msg_error(e)) + + self.msg_set_pushing() + have_to_push = False + for wid in Will.only_valid(self.bal_window.willitems): + w = self.bal_window.willitems[wid] + if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"): + have_to_push = True + if not have_to_push: + self.msg_set_pushing(_("Nothing to do")) + else: + try: + self.loop_push() + self.msg_set_pushing(self.msg_ok()) + + except Exception as e: + td = traceback.format_exc() + self.msg_set_pushing(self.msg_error(e)) + self.msg_edit_row(self.msg_ok()) + self.wait(5) + + def on_error(self, error): + _logger.error(error) + pass + + def on_error_phase1(self, error): + self.bal_window.update_all() + a,b,c = error + self.msg_edit_row(self.msg_error(f"Error: {b}")) + _logger.error(f"error phase1: {b}") + + def on_error_phase2(self, error): + self.bal_window.upade_all() + a,b,c = error + self.msg_edit_row(self.msg_error(f"Error: {b}")) + _logger.error(f"error phase2: {b}") + + def msg_set_checking(self, status="Waiting", row=None): + row = self.check_row if row is None else row + self.check_row = self.msg_set_status(_("Checking your will"), row, status) + + def msg_set_invalidating(self, status=None, row=None): + row = self.inval_row if row is None else row + self.inval_row = self.msg_set_status( + _("Invalidating old will"), self.inval_row, status + ) + + def msg_set_building(self, status=None, row=None): + row = self.build_row if row is None else row + self.build_row = self.msg_set_status( + "Building your will", self.build_row, status + ) + + def msg_set_signing(self, status=None, row=None): + row = self.sign_row if row is None else row + self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status) + + def msg_set_pushing(self, status=None, row=None): + row = self.push_row if row is None else row + self.push_row = self.msg_set_status( + "Broadcasting your will to executors", self.push_row, status + ) + + def msg_set_waiting(self, status=None, row=None): + row = self.wait_row if row is None else row + self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row) + + def msg_error(self, e): + return "{}".format(self.COLOR_ERROR, e) + + def msg_ok(self, e="Ok"): + return "{}".format(self.COLOR_OK, e) + + def msg_warning(self, e): + return "{}") + + # Create label with proper settings + label = QLabel(formatted_text) + label.setWordWrap(True) + label.setTextFormat(Qt.TextFormat.RichText) + label.setOpenExternalLinks(False) # Security + + # Set size policy + label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + # Add to layout + self.labelsbox.addWidget(label) + + except Exception as e: + # Log errors without interrupting processing + import logging + logging.error(f"Error creating label in BalBuildWillDialog: {e}") + + # Reset queue and update dimensions + self._message_queue = [] + self.setMinimumHeight(min(30 * (len(self.labels) + 2), 400)) # Max height limit + + + def get_text(self): + return self.message_label.text() + + pass + + +class HeirListWidget(MyTreeView, MessageBoxMixin): + class Columns(MyTreeView.BaseColumnsEnum): + NAME = enum.auto() + ADDRESS = enum.auto() + AMOUNT = enum.auto() + + headers = { + Columns.NAME: _("Name"), + Columns.ADDRESS: _("Address"), + Columns.AMOUNT: _("Amount"), + } + filter_columns = [Columns.NAME, Columns.ADDRESS] + + ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000 + + ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 4000 + key_role = ROLE_HEIR_KEY + + def createEditor(self, parent, option, index): + return QLineEdit(parent) + def setEditorData(self, editor, index): + editor.setText(index.data()) + def setModelData(self, editor, model, index): + model.setData(index, editor.text()) + + def __init__(self, bal_window: "BalWindow", parent): + super().__init__( + parent=parent, + main_window=bal_window.window, + stretch_column=self.Columns.NAME, + editable_columns=[ + self.Columns.NAME, + self.Columns.ADDRESS, + self.Columns.AMOUNT, + ], + ) + self.decimal_point = bal_window.window.get_decimal_point() + self.bal_window = bal_window + + + try: + self.setModel(QStandardItemModel(self)) + self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder) + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + except Exception: + pass + + self.setSortingEnabled(True) + self.std_model = self.model() + + self.update() + + def on_activated(self, idx): + self.on_double_click(idx) + + def on_double_click(self, idx): + edit_key = self.get_edit_key_from_coordinate(idx.row(), idx.column()) + self.bal_window.heirs.get(edit_key) + self.bal_window.new_heir_dialog(edit_key) + + def on_edited(self, idx, edit_key, *, text): + original = prior_name = self.bal_window.heirs.get(edit_key) + if not prior_name: + return + col = idx.column() + try: + if col == 2: + text = Util.encode_amount(text, self.decimal_point) + elif col == 0: + self.bal_window.delete_heirs([edit_key]) + edit_key = text + prior_name[col - 1] = text + prior_name.insert(0, edit_key) + prior_name = tuple(prior_name) + except Exception: + prior_name = ( + (edit_key,) + prior_name[: col - 1] + (text,) + prior_name[col:] + ) + + try: + self.bal_window.set_heir(prior_name) + except Exception: + pass + + try: + self.bal_window.set_heir((edit_key,) + original) + except Exception: + self.update() + + def delete_heirs(self, selected_keys): + self.bal_window.delete_heirs(selected_keys) + self.update() + + def create_menu(self, position): + menu = QMenu() + idx = self.indexAt(position) + column = idx.column() or self.Columns.NAME + selected_keys = [] + for s_idx in self.selected_in_column(self.Columns.NAME): + sel_key = self.model().itemFromIndex(s_idx).data(0) + selected_keys.append(sel_key) + if selected_keys and idx.isValid(): + column_title = self.model().horizontalHeaderItem(column).text() + # ok + column_data = "\n".join( + self.model().itemFromIndex(s_idx).text() + for s_idx in self.selected_in_column(column) + ) + menu.addAction( + _("Copy {}").format(column_title), + lambda: self.place_text_on_clipboard(column_data, title=column_title), + ) + if column in self.editable_columns: + item = self.model().itemFromIndex(idx) + if item.isEditable(): + persistent = QPersistentModelIndex(idx) + menu.addAction( + _("Edit {}").format(column_title), + lambda p=persistent: self.edit(QModelIndex(p)), + ) + menu.addAction(_("Delete"), lambda: self.delete_heirs(selected_keys)) + menu.exec(self.viewport().mapToGlobal(position)) + + def update(self): + current_key = self.get_role_data_for_current_item( + col=self.Columns.NAME, role=self.ROLE_HEIR_KEY + ) + self.model().clear() + self.update_headers(self.__class__.headers) + set_current = None + for key in sorted(self.bal_window.heirs.keys()): + heir = self.bal_window.heirs[key] + labels = [""] * len(self.Columns) + labels[self.Columns.NAME] = key + labels[self.Columns.ADDRESS] = heir[0] + labels[self.Columns.AMOUNT] = Util.decode_amount( + heir[1], self.decimal_point + ) + + items = [QStandardItem(x) for x in labels] + items[self.Columns.NAME].setEditable(True) + items[self.Columns.ADDRESS].setEditable(True) + items[self.Columns.AMOUNT].setEditable(True) + items[self.Columns.NAME].setData( + key, self.ROLE_HEIR_KEY + self.Columns.NAME + ) + items[self.Columns.ADDRESS].setData( + key, self.ROLE_HEIR_KEY + self.Columns.ADDRESS + ) + items[self.Columns.AMOUNT].setData( + key, self.ROLE_HEIR_KEY + self.Columns.AMOUNT + ) + + row_count = self.model().rowCount() + self.model().insertRow(row_count, items) + + if key == current_key: + idx = self.model().index(row_count, self.Columns.NAME) + set_current = QPersistentModelIndex(idx) + self.set_current_idx(set_current) + # FIXME refresh loses sort order; so set "default" here: + self.filter() + run_hook("update_heirs_tab", self) + self.update_will_settings() + + def refresh_row(self, key, row): + # nothing to update here + pass + + def get_edit_key_from_coordinate(self, row, col): + a = self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY + col) + return a + + def create_toolbar(self, config): + toolbar, menu = self.create_toolbar_with_menu("") + menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog) + menu.addAction(_("Import"), self.bal_window.import_heirs) + menu.addAction(_("Export"), lambda: self.bal_window.export_heirs()) + + self.heir_locktime = HeirsLockTimeEdit(self, 0) + + def on_heir_locktime(): + if not self.heir_locktime.get_locktime(): + self.heir_locktime.set_locktime("1y") + self.bal_window.will_settings["locktime"] = ( + self.heir_locktime.get_locktime() + if self.heir_locktime.get_locktime() + else "1y" + ) + self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings) + + self.heir_locktime.valueEdited.connect(on_heir_locktime) + + self.heir_threshold = HeirsLockTimeEdit(self, 0) + + def on_heir_threshold(): + if not self.heir_threshold.get_locktime(): + self.heir_threshold.set_locktime("180d") + + self.bal_window.will_settings["threshold"] = ( + self.heir_threshold.get_locktime() + ) + self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings) + + self.heir_threshold.valueEdited.connect(on_heir_threshold) + + self.heir_tx_fees = QSpinBox() + self.heir_tx_fees.setMinimum(1) + self.heir_tx_fees.setMaximum(10000) + + def on_heir_tx_fees(): + if not self.heir_tx_fees.value(): + self.heir_tx_fees.set_value(1) + self.bal_window.will_settings["baltx_fees"] = self.heir_tx_fees.value() + self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings) + + self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees) + + self.heirs_widget = QWidget() + layout = QHBoxLayout() + self.heirs_widget.setLayout(layout) + + layout.addWidget(QLabel(_("Delivery Time:"))) + layout.addWidget(self.heir_locktime) + layout.addWidget( + HelpButton( + _( + "Locktime* to be used in the transaction\n" + + "if you choose Raw, you can insert various options based on suffix:\n" + + " - d: number of days after current day(ex: 1d means tomorrow)\n" + + " - y: number of years after currrent day(ex: 1y means one year from today)\n" + + "* locktime can be anticipated to update will\n" + ) + ) + ) + + layout.addWidget(QLabel(" ")) + layout.addWidget(QLabel(_("Check Alive:"))) + layout.addWidget(self.heir_threshold) + layout.addWidget( + HelpButton( + _( + "Check to ask for invalidation.\n" + + "When less then this time is missing, ask to invalidate.\n" + + "If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n" + + "if you choose Raw, you can insert various options based on suffix:\n" + + " - d: number of days after current day(ex: 1d means tomorrow).\n" + + " - y: number of years after currrent day(ex: 1y means one year from today).\n\n" + ) + ) + ) + layout.addWidget(QLabel(" ")) + layout.addWidget(QLabel(_("Fees:"))) + layout.addWidget(self.heir_tx_fees) + layout.addWidget(HelpButton(_("Fee to be used in the transaction"))) + layout.addWidget(QLabel("sats/vbyte")) + layout.addWidget(QLabel(" ")) + newHeirButton = QPushButton(_("New Heir")) + newHeirButton.clicked.connect(self.bal_window.new_heir_dialog) + layout.addWidget(newHeirButton) + + toolbar.insertWidget(2, self.heirs_widget) + + return toolbar + + def update_will_settings(self): + try: + self.heir_locktime.set_locktime(self.bal_window.will_settings["locktime"]) + self.heir_threshold.set_locktime(self.bal_window.will_settings["threshold"]) + self.heir_tx_fees.setValue(int(self.bal_window.will_settings["baltx_fees"])) + + except Exception as e: + pass + _logger.debug(f"Exception update_will_settings {e}") + + def build_transactions(self): + # will = self.bal_window.prepare_will() + self.bal_window.prepare_will() + + +class PreviewList(MyTreeView): + class Columns(MyTreeView.BaseColumnsEnum): + LOCKTIME = enum.auto() + TXID = enum.auto() + WILLEXECUTOR = enum.auto() + STATUS = enum.auto() + + headers = { + Columns.LOCKTIME: _("Locktime"), + Columns.TXID: _("Txid"), + Columns.WILLEXECUTOR: _("Will-Executor"), + Columns.STATUS: _("Status"), + } + + ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000 + key_role = ROLE_HEIR_KEY + + def __init__(self, bal_window: "BalWindow", parent, will): + super().__init__( + parent=parent, + stretch_column=self.Columns.TXID, + ) + self.parent = parent + self.bal_window = bal_window + self.decimal_point = bal_window.window.get_decimal_point + self.setModel(QStandardItemModel(self)) + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + + if will is not None: + self.will = will + else: + self.will = bal_window.willitems + + self.wallet = bal_window.window.wallet + self.setModel(QStandardItemModel(self)) + self.sortByColumn(self.Columns.LOCKTIME, Qt.SortOrder.AscendingOrder) + self.setSortingEnabled(True) + self.std_model = self.model() + self.config = bal_window.bal_plugin.config + self.bal_plugin = self.bal_window.bal_plugin + + self.update() + + def on_activated(self, idx): + self.on_double_click(idx) + + def on_double_click(self, idx): + idx = self.model().index(idx.row(), self.Columns.TXID) + sel_key = self.model().itemFromIndex(idx).data(0) + self.show_transaction([sel_key]) + + def create_menu(self, position): + menu = QMenu() + idx = self.indexAt(position) + column = idx.column() or self.Columns.TXID + selected_keys = [] + for s_idx in self.selected_in_column(self.Columns.TXID): + sel_key = self.model().itemFromIndex(s_idx).data(0) + selected_keys.append(sel_key) + if selected_keys and idx.isValid(): + column_title = self.model().horizontalHeaderItem(column).text() + # column_data = "\n".join( + # self.model().itemFromIndex(s_idx).text() + # for s_idx in self.selected_in_column(column) + # ) + + menu.addAction( + _("details").format(column_title), + lambda: self.show_transaction(selected_keys), + ).setEnabled(len(selected_keys) < 2) + menu.addAction( + _("check ").format(column_title), + lambda: self.check_transactions(selected_keys), + ) + if self.bal_plugin.ENABLE_MULTIVERSE.get(): + try: + self.importaction = self.menu.addAction( + _("Import"), self.import_will + ) + except Exception: + pass + + menu.addSeparator() + menu.addAction( + _("delete").format(column_title), lambda: self.delete(selected_keys) + ) + + menu.exec(self.viewport().mapToGlobal(position)) + + def delete(self, selected_keys): + for key in selected_keys: + del self.will[key] + try: + del self.bal_window.willitems[key] + except Exception: + pass + try: + del self.bal_window.will[key] + except Exception: + pass + self.update() + + def check_transactions(self, selected_keys): + wout = {} + for k in selected_keys: + wout[k] = self.will[k] + if wout: + self.bal_window.check_transactions(wout) + self.update() + + def show_transaction(self, selected_keys): + for key in selected_keys: + self.bal_window.show_transaction(self.will[key].tx) + + self.update() + + def select(self, selected_keys): + self.selected += selected_keys + self.update() + + def deselect(self, selected_keys): + for key in selected_keys: + self.selected.remove(key) + self.update() + + def update_will(self, will): + self.will.update(will) + self.update() + + def replace(self, set_current, current_key, txid, bal_tx): + if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status("REPLACED"): + return False + if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status( + "INVALIDATED" + ): + return False + + if not isinstance(bal_tx, WillItem): + bal_tx = WillItem(bal_tx) + + tx = bal_tx.tx + + labels = [""] * len(self.Columns) + labels[self.Columns.LOCKTIME] = Util.locktime_to_str(tx.locktime) + labels[self.Columns.TXID] = txid + we = "None" + 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 = "...{}".format(status[-50:]) + labels[self.Columns.STATUS] = status + + items = [] + for e in labels: + if isinstance(e, list): + try: + items.append(QStandardItem(*e)) + except Exception as e: + pass + else: + items.append(QStandardItem(str(e))) + + items[-1].setBackground(QColor(bal_tx.get_color())) + + row_count = self.model().rowCount() + self.model().insertRow(row_count, items) + if txid == current_key: + idx = self.model().index(row_count, self.Columns.TXID) + set_current = QPersistentModelIndex(idx) + self.set_current_idx(set_current) + return set_current + + def update(self): + try: + self.menu.removeAction(self.importaction) + except Exception: + pass + + if self.will is None: + return + + current_key = self.get_role_data_for_current_item( + col=self.Columns.TXID, role=self.ROLE_HEIR_KEY + ) + self.model().clear() + self.update_headers(self.__class__.headers) + + set_current = None + for txid, bal_tx in self.will.items(): + tmp = self.replace(set_current, current_key, txid, bal_tx) + if tmp: + set_current = tmp + self.sortByColumn(self.Columns.LOCKTIME, Qt.SortOrder.AscendingOrder) + self.setSortingEnabled(True) + + def create_toolbar(self, config): + toolbar, menu = self.create_toolbar_with_menu("") + menu.addAction(_("Prepare"), self.build_transactions) + 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_plugin.ENABLE_MULTIVERSE.get(): + self.importaction = menu.addAction(_("Import"), self.import_will) + menu.addAction(_("Broadcast"), self.broadcast) + menu.addAction(_("Check"), self.check) + menu.addAction(_("Invalidate"), self.invalidate_will) + + wizard = QPushButton(_("Setup Wizard")) + wizard.clicked.connect(self.bal_window.init_wizard) + #display = QPushButton(_("Display")) + #display.clicked.connect(self.bal_window.preview_modal_dialog) + + refresh = QPushButton(_("Refresh")) + refresh.clicked.connect(self.check) + + widget = QWidget() + hlayout = QHBoxLayout(widget) + hlayout.addWidget(wizard) + hlayout.addWidget(refresh) + toolbar.insertWidget(2, widget) + + self.menu = menu + self.toolbar = toolbar + return toolbar + + def hide_replaced(self): + self.bal_window.bal_plugin.hide_replaced() + self.update() + + def hide_invalidated(self): + self.bal_window.bal_plugin.hide_invalidated() + self.update() + + def build_transactions(self): + will = self.bal_window.prepare_will() + if will: + self.update_will(will) + + def export_json_file(self, path): + write_json_file(path, self.will) + + def export_will(self): + self.bal_window.export_will() + self.update() + + def import_will(self): + self.bal_window.import_will() + + def ask_password_and_sign_transactions(self): + self.bal_window.ask_password_and_sign_transactions(callback=self.update) + + def broadcast(self): + self.bal_window.broadcast_transactions() + self.update() + + def check(self): + close_window = BalBuildWillDialog(self.bal_window) + close_window.build_will_task() + + will = {} + for wid, w in self.bal_window.willitems.items(): + if ( + w.get_status("VALID") + and w.get_status("PUSHED") + and not w.get_status("CHECKED") + ): + will[wid] = w + if will: + self.bal_window.check_transactions(will) + self.update() + + def invalidate_will(self): + self.bal_window.invalidate_will() + self.update() + + +class PreviewDialog(BalDialog, MessageBoxMixin): + def __init__(self, bal_window, will): + self.parent = bal_window.window + BalDialog.__init__( + self, bal_window=bal_window, bal_plugin=bal_window.bal_plugin + ) + self.bal_plugin = bal_window.bal_plugin + self.gui_object = self.bal_plugin.gui_object + self.config = self.bal_plugin.config + self.bal_window = bal_window + self.wallet = bal_window.window.wallet + self.format_amount = bal_window.window.format_amount + self.base_unit = bal_window.window.base_unit + self.format_fiat_and_units = bal_window.window.format_fiat_and_units + self.fx = bal_window.window.fx + self.format_fee_rate = bal_window.window.format_fee_rate + self.show_address = bal_window.window.show_address + if not will: + self.will = bal_window.willitems + else: + self.will = will + self.setWindowTitle(_("Transactions Preview")) + self.setMinimumSize(1000, 200) + self.size_label = QLabel() + self.transactions_list = PreviewList(self.bal_window, self.will) + + try: + self.bal_window.init_class_variables() + except Exception as e: + _logger.error(f"PreviewDialog Exception: {e}") + self.check_will() + + vbox = QVBoxLayout(self) + vbox.addWidget(self.size_label) + vbox.addWidget(self.transactions_list) + buttonbox = QHBoxLayout() + + b = QPushButton(_("Sign")) + b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions) + buttonbox.addWidget(b) + + b = QPushButton(_("Export Will")) + b.clicked.connect(self.transactions_list.export_will) + buttonbox.addWidget(b) + + b = QPushButton(_("Broadcast")) + b.clicked.connect(self.transactions_list.broadcast) + buttonbox.addWidget(b) + + b = QPushButton(_("Invalidate will")) + b.clicked.connect(self.transactions_list.invalidate_will) + buttonbox.addWidget(b) + + vbox.addLayout(buttonbox) + + self.update() + + def update_will(self, will): + self.will.update(will) + self.transactions_list.update_will(will) + self.update() + + def update(self): + self.transactions_list.update() + + def is_hidden(self): + return self.isMinimized() or self.isHidden() + + def show_or_hide(self): + if self.is_hidden(): + self.bring_to_top() + else: + self.hide() + + def bring_to_top(self): + self.show() + self.raise_() + + def closeEvent(self, event): + event.accept() + + +class WillDetailDialog(BalDialog): + def __init__(self, bal_window): + + self.will = bal_window.willitems + self.threshold = Util.parse_locktime_string( + bal_window.will_settings["threshold"] + ) + self.bal_window = bal_window + Will.add_willtree(self.will) + super().__init__(bal_window.window, bal_window.bal_plugin) + self.config = bal_window.window.config + self.wallet = bal_window.wallet + self.format_amount = bal_window.window.format_amount + self.base_unit = bal_window.window.base_unit + self.format_fiat_and_units = bal_window.window.format_fiat_and_units + self.fx = bal_window.window.fx + self.format_fee_rate = bal_window.window.format_fee_rate + self.decimal_point = bal_window.window.get_decimal_point() + self.base_unit_name = decimal_point_to_base_unit_name(self.decimal_point) + self.setWindowTitle(_("Will Details")) + self.setMinimumSize(670, 700) + self.vlayout = QVBoxLayout() + w = QWidget() + hlayout = QHBoxLayout(w) + + b = QPushButton(_("Sign")) + b.clicked.connect(self.ask_password_and_sign_transactions) + hlayout.addWidget(b) + + b = QPushButton(_("Broadcast")) + b.clicked.connect(self.broadcast_transactions) + hlayout.addWidget(b) + + b = QPushButton(_("Export")) + b.clicked.connect(self.export_will) + hlayout.addWidget(b) + b = QPushButton(_("Invalidate")) + b.clicked.connect(bal_window.invalidate_will) + hlayout.addWidget(b) + self.vlayout.addWidget(w) + + self.paint_scroll_area() + self.vlayout.addWidget( + QLabel(_("Expiration date: ") + Util.locktime_to_str(self.threshold)) + ) + self.vlayout.addWidget(self.scrollbox) + w = QWidget() + hlayout = QHBoxLayout(w) + hlayout.addWidget( + QLabel(_("Valid Txs:") + str(len(Will.only_valid_list(self.will)))) + ) + hlayout.addWidget(QLabel(_("Total Txs:") + str(len(self.will)))) + self.vlayout.addWidget(w) + self.setLayout(self.vlayout) + + def paint_scroll_area(self): + self.scrollbox = QScrollArea() + viewport = QWidget(self.scrollbox) + self.willlayout = QVBoxLayout(viewport) + self.detailsWidget = WillWidget(parent=self) + 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.update() + + def broadcast_transactions(self): + self.bal_window.broadcast_transactions() + self.update() + + def export_will(self): + self.bal_window.export_will() + + def toggle_replaced(self): + self.bal_window.bal_plugin.hide_replaced() + toggle = _("Hide") + if self.bal_window.bal_plugin._hide_replaced: + toggle = _("Unhide") + self.toggle_replace_button.setText(f"{toggle} {_('replaced')}") + self.update() + + def toggle_invalidated(self): + self.bal_window.bal_plugin.hide_invalidated() + toggle = _("Hide") + if self.bal_window.bal_plugin._hide_invalidated: + toggle = _("Unhide") + self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}")) + self.update() + + def update(self): + self.will = self.bal_window.willitems + pos = self.vlayout.indexOf(self.scrollbox) + self.vlayout.removeWidget(self.scrollbox) + self.paint_scroll_area() + self.vlayout.insertWidget(pos, self.scrollbox) + super().update() + + +class WillWidget(QWidget): + def __init__(self, father=None, parent=None): + super().__init__() + vlayout = QVBoxLayout() + self.setLayout(vlayout) + self.will = parent.bal_window.willitems + self.parent = parent + for w in self.will: + if ( + self.will[w].get_status("REPLACED") + and self.parent.bal_window.bal_plugin._hide_replaced + ): + continue + if ( + self.will[w].get_status("INVALIDATED") + and self.parent.bal_window.bal_plugin._hide_invalidated + ): + continue + f = self.will[w].father + if father == f: + qwidget = QWidget() + # childWidget = QWidget() + hlayout = QHBoxLayout(qwidget) + qwidget.setLayout(hlayout) + vlayout.addWidget(qwidget) + detailw = QWidget() + detaillayout = QVBoxLayout() + detailw.setLayout(detaillayout) + + willpushbutton = QPushButton(w) + + willpushbutton.clicked.connect( + partial(self.parent.bal_window.show_transaction, txid=w) + ) + detaillayout.addWidget(willpushbutton) + locktime = Util.locktime_to_str(self.will[w].tx.locktime) + creation = Util.locktime_to_str(self.will[w].time) + + def qlabel(title, value): + label = "" + _(str(title)) + f":\t{str(value)}" + return QLabel(label) + + detaillayout.addWidget(qlabel("Locktime", locktime)) + detaillayout.addWidget(qlabel("Creation Time", creation)) + try: + total_fees = ( + self.will[w].tx.input_value() - self.will[w].tx.output_value() + ) + except Exception: + total_fees = -1 + decoded_fees = total_fees + 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("")) + detaillayout.addWidget(QLabel("Heirs:")) + for heir in self.will[w].heirs: + if 'w!ll3x3c"' not in heir: + decoded_amount = Util.decode_amount( + self.will[w].heirs[heir][3], self.parent.decimal_point + ) + detaillayout.addWidget( + qlabel( + heir, f"{decoded_amount} {self.parent.base_unit_name}" + ) + ) + if self.will[w].we: + detaillayout.addWidget(QLabel("")) + detaillayout.addWidget(QLabel(_("Willexecutor: Date: Thu, 9 Apr 2026 02:10:49 +0000 Subject: [PATCH 2/4] docs(qt.py): Add detailed interval documentation for HeirsLockTimeEdit class behavior --- qt.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/qt.py b/qt.py index 0104730..b59787d 100644 --- a/qt.py +++ b/qt.py @@ -1275,26 +1275,37 @@ class _LockTimeEditor: ======= """ HeirsLockTimeEdit - A custom QWidget for editing locktime values in the context of heirs distribution. - + This widget combines raw locktime editing with date-based selection and provides additional functionality for managing locktime values in a heir inheritance scenario. - + Features: - Supports both raw locktime values and human-readable date formats - - Emits valueEdited signal when the locktime value changes + - Emits valueEdited signal when the locktime value is edited - Provides threshold-based validation for locktime values - Integrates with heir distribution workflows - + + Behavior: + The class handles three types of locktime values: + 1. **Timestamps**: Raw integer values representing Unix timestamps + 2. **Day intervals**: Values ending with 'd' (e.g., '3d' = 3 days from now) + 3. **Year intervals**: Values ending with 'y' (e.g., '2y' = 2 years from now) + + Only these formats are valid: + - Examples: '1609459200', '3d', '2y' + - Invalid: 'invalid', '5m', '10x' + + The widget automatically converts day/year intervals to appropriate timestamps. + Attributes: valueEdited (pyqtSignal): Signal emitted when the locktime value is edited locktime_threshold (int): Minimum threshold value for locktime (default: 50000000) - + Args: parent: Optional parent QWidget default_index (int): Default index for the combo box (default: 1) """ ->>>>>>> origin/doc class HeirsLockTimeEdit(QWidget, _LockTimeEditor): valueEdited = pyqtSignal() locktime_threshold = 50000000 From d613438800c434710443878330268187f73ff2bb Mon Sep 17 00:00:00 2001 From: kaibot Date: Thu, 9 Apr 2026 02:39:04 +0000 Subject: [PATCH 3/4] docs(qt.py): Add comprehensive documentation for BalWizardDialog class --- qt.py | 327 ++++------------------------------------------------------ 1 file changed, 20 insertions(+), 307 deletions(-) diff --git a/qt.py b/qt.py index b59787d..f7271eb 100644 --- a/qt.py +++ b/qt.py @@ -1274,324 +1274,37 @@ class _LockTimeEditor: <<<<<<< HEAD ======= """ - HeirsLockTimeEdit - A custom QWidget for editing locktime values in the context of heirs distribution. + BalWizardDialog - A custom QDialog that implements a multi-step wizard interface. - This widget combines raw locktime editing with date-based selection and provides - additional functionality for managing locktime values in a heir inheritance scenario. + This dialog provides a structured, step-by-step workflow for complex operations + in the Bal Electrum plugin, guiding users through a sequence of pages with + forward/backward navigation and validation. Features: - - Supports both raw locktime values and human-readable date formats - - Emits valueEdited signal when the locktime value is edited - - Provides threshold-based validation for locktime values - - Integrates with heir distribution workflows + - Multi-page navigation with Previous/Next buttons + - Automatic validation before proceeding to next page + - Progress tracking with visual indicators + - Customizable page flow and validation rules + - Integration with BalDialog base class for consistent styling - Behavior: - The class handles three types of locktime values: - 1. **Timestamps**: Raw integer values representing Unix timestamps - 2. **Day intervals**: Values ending with 'd' (e.g., '3d' = 3 days from now) - 3. **Year intervals**: Values ending with 'y' (e.g., '2y' = 2 years from now) - - Only these formats are valid: - - Examples: '1609459200', '3d', '2y' - - Invalid: 'invalid', '5m', '10x' - - The widget automatically converts day/year intervals to appropriate timestamps. + Usage: + The wizard follows a standard pattern: + 1. Initialize with a list of page constructors + 2. Each page is responsible for its own setup and validation + 3. The dialog manages navigation and state between pages + 4. Finalize action is triggered when all pages are completed Attributes: - valueEdited (pyqtSignal): Signal emitted when the locktime value is edited - locktime_threshold (int): Minimum threshold value for locktime (default: 50000000) + pages (list): List of page constructors for the wizard + current_page (int): Index of the currently displayed page + page_widgets (list): List of instantiated page widgets Args: parent: Optional parent QWidget - default_index (int): Default index for the combo box (default: 1) + title (str): Title to display in the dialog header + pages (list): List of page constructors (callables) for each step """ -class HeirsLockTimeEdit(QWidget, _LockTimeEditor): - valueEdited = pyqtSignal() - locktime_threshold = 50000000 - - def __init__(self, parent=None, default_index=1): - QWidget.__init__(self, parent) - - hbox = QHBoxLayout() - self.setLayout(hbox) - hbox.setContentsMargins(0, 0, 0, 0) - hbox.setSpacing(0) - - self.locktime_raw_e = LockTimeRawEdit(self, time_edit=self) - self.locktime_date_e = LockTimeDateEdit(self, time_edit=self) - self.editors = [self.locktime_raw_e, self.locktime_date_e] - - self.combo = QComboBox() - options = [_("Raw"), _("Date")] - self.option_index_to_editor_map = { - 0: self.locktime_raw_e, - 1: self.locktime_date_e, - } - self.combo.addItems(options) - - self.editor = self.option_index_to_editor_map[default_index] - self.combo.currentIndexChanged.connect(self.on_current_index_changed) - self.combo.setCurrentIndex(default_index) - self.on_current_index_changed(default_index) - - hbox.addWidget(self.combo) - for w in self.editors: - hbox.addWidget(w) - hbox.addStretch(1) - # spacer_widget = QWidget() - # spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) - # hbox.addWidget(spacer_widget) - - self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit) - self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit) - self.combo.currentIndexChanged.connect(self.valueEdited.emit) - - def on_current_index_changed(self, i): - for w in self.editors: - w.setVisible(False) - w.setEnabled(False) - prev_locktime = self.editor.get_locktime() - self.editor = self.option_index_to_editor_map[i] - if self.editor.is_acceptable_locktime(prev_locktime): - self.editor.set_locktime(prev_locktime, force=True) - self.editor.setVisible(True) - self.editor.setEnabled(True) - - def get_locktime(self) -> Optional[str]: - return self.editor.get_locktime() - - def set_index(self, index): - self.combo.setCurrentIndex(index) - self.on_current_index_changed(index) - - def set_locktime(self, x: Any, force=True) -> None: - self.editor.set_locktime(x, force) - - -class LockTimeRawEdit(QLineEdit, _LockTimeEditor): - def __init__(self, parent=None, time_edit=None): - QLineEdit.__init__(self, parent) - self.setFixedWidth(14 * char_width_in_lineedit()) - self.textChanged.connect(self.numbify) - self.isdays = False - self.isyears = False - self.isblocks = False - self.time_edit = time_edit - - def replace_str(self, text): - return str(text).replace("d", "").replace("y", "").replace("b", "") - - def checkbdy(self, s, pos, appendix): - try: - charpos = pos - 1 - charpos = max(0, charpos) - charpos = min(len(s) - 1, charpos) - if appendix == s[charpos]: - s = self.replace_str(s) + appendix - pos = charpos - except Exception: - pass - return pos, s - - def numbify(self): - text = self.text().strip() - # chars = '0123456789bdy' removed the option to choose locktime by block - chars = "0123456789dy" - pos = self.cursorPosition() - pos = len("".join([i for i in text[:pos] if i in chars])) - s = "".join([i for i in text if i in chars]) - self.isdays = False - self.isyears = False - self.isblocks = False - - pos, s = self.checkbdy(s, pos, "d") - pos, s = self.checkbdy(s, pos, "y") - pos, s = self.checkbdy(s, pos, "b") - - if "d" in s: - self.isdays = True - if "y" in s: - self.isyears = True - if "b" in s: - self.isblocks = True - - if self.isdays: - s = self.replace_str(s) + "d" - if self.isyears: - s = self.replace_str(s) + "y" - if self.isblocks: - s = self.replace_str(s) + "b" - - self.set_locktime(s, force=False) - # setText sets Modified to False. Instead we want to remember - # if updates were because of user modification. - self.setModified(self.hasFocus()) - self.setCursorPosition(pos) - - def get_locktime(self) -> Optional[str]: - try: - return str(self.text()) - except Exception: - return None - - def set_locktime(self, x: Any, force=True) -> None: - out = str(x) - if "d" in out: - out = self.replace_str(x) + "d" - elif "y" in out: - out = self.replace_str(x) + "y" - elif "b" in out: - out = self.replace_str(x) + "b" - else: - try: - out = int(x) - except Exception: - self.setText("") - return - out = max(out, self.min_allowed_value) - out = min(out, self.max_allowed_value) - self.setText(str(out)) - - -class LockTimeHeightEdit(LockTimeRawEdit): - max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX - - def __init__(self, parent=None, time_edit=None): - LockTimeRawEdit.__init__(self, parent) - self.setFixedWidth(20 * char_width_in_lineedit()) - self.time_edit = time_edit - - def paintEvent(self, event): - super().paintEvent(event) - panel = QStyleOptionFrame() - self.initStyleOption(panel) - textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self) - textRect.adjust(2, 0, -10, 0) - painter = QPainter(self) - painter.setPen(ColorScheme.GRAY.as_color()) - painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height") - - -def get_max_allowed_timestamp() -> int: - ts = NLOCKTIME_MAX - # Test if this value is within the valid timestamp limits (which is platform-dependent). - # see #6170 - try: - datetime.fromtimestamp(ts) - except (OSError, OverflowError): - ts = 2**31 - 1 # INT32_MAX - datetime.fromtimestamp(ts) # test if raises - return ts - - -class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor): - min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1 - max_allowed_value = get_max_allowed_timestamp() - - def __init__(self, parent=None, time_edit=None): - QDateTimeEdit.__init__(self, parent) - self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value)) - self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value)) - self.setDateTime(QDateTime.currentDateTime()) - self.time_edit = time_edit - - def get_locktime(self) -> Optional[int]: - dt = self.dateTime().toPyDateTime() - locktime = int(time.mktime(dt.timetuple())) - return locktime - - def set_locktime(self, x: Any, force=False) -> None: - if not self.is_acceptable_locktime(x): - self.setDateTime(QDateTime.currentDateTime()) - return - try: - x = int(x) - except Exception: - self.setDateTime(QDateTime.currentDateTime()) - return - dt = datetime.fromtimestamp(x) - self.setDateTime(dt) - - -_NOT_GIVEN = object() # sentinel value - - -class PercAmountEdit(BTCAmountEdit): - def __init__( - self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN - ): - super().__init__(decimal_point, is_int, parent, max_amount=max_amount) - - def numbify(self): - text = self.text().strip() - if text == "!": - self.shortcut.emit() - return - pos = self.cursorPosition() - chars = "0123456789%" - chars += DECIMAL_POINT - - s = "".join([i for i in text if i in chars]) - - if "%" in s: - self.is_perc = True - s = s.replace("%", "") - else: - self.is_perc = False - - if DECIMAL_POINT in s: - p = s.find(DECIMAL_POINT) - s = s.replace(DECIMAL_POINT, "") - s = s[:p] + DECIMAL_POINT + s[p : p + 8] - if self.is_perc: - s += "%" - - self.setText(s) - self.setModified(self.hasFocus()) - self.setCursorPosition(pos) - - def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]: - try: - text = text.replace(DECIMAL_POINT, ".") - text = text.replace("%", "") - return (Decimal)(text) - except Exception: - return None - - def _get_text_from_amount(self, amount): - out = super()._get_text_from_amount(amount) - if self.is_perc: - out += "%" - return out - - def paintEvent(self, event): - QLineEdit.paintEvent(self, event) - if self.base_unit: - panel = QStyleOptionFrame() - self.initStyleOption(panel) - textRect = self.style().subElementRect( - QStyle.SubElement.SE_LineEditContents, panel, self - ) - textRect.adjust(2, 0, -10, 0) - painter = QPainter(self) - painter.setPen(ColorScheme.GRAY.as_color()) - if len(self.text()) == 0: - painter.drawText( - textRect, - int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), - self.base_unit() + " or perc value", - ) - - -class BalDialog(WindowModalDialog): - def __init__(self, parent, bal_plugin, title=None, icon="icons/bal32x32.png"): - self.parent = parent - WindowModalDialog.__init__(self, parent, title) - # WindowModalDialog.__init__(self,parent) - self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon))) - - class BalWizardDialog(BalDialog): def __init__(self, bal_window: "BalWindow"): assert bal_window From b739bdab40bb862df6fd2579d7a32865b1b246e9 Mon Sep 17 00:00:00 2001 From: kaibot Date: Thu, 9 Apr 2026 02:43:06 +0000 Subject: [PATCH 4/4] docs(qt.py): Add comprehensive documentation for BalWizardDialog class --- qt.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/qt.py b/qt.py index b59787d..050f2b7 100644 --- a/qt.py +++ b/qt.py @@ -1592,6 +1592,38 @@ class BalDialog(WindowModalDialog): self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon))) + """ + BalWizardDialog - A custom QDialog that implements a multi-step wizard interface. + + This dialog provides a structured, step-by-step workflow for complex operations + in the Bal Electrum plugin, guiding users through a sequence of pages with + forward/backward navigation and validation. + + Features: + - Multi-page navigation with Previous/Next buttons + - Automatic validation before proceeding to next page + - Progress tracking with visual indicators + - Customizable page flow and validation rules + - Integration with BalDialog base class for consistent styling + + Usage: + The wizard follows a standard pattern: + 1. Initialize with a list of page constructors + 2. Each page is responsible for its own setup and validation + 3. The dialog manages navigation and state between pages + 4. Finalize action is triggered when all pages are completed + + Attributes: + pages (list): List of page constructors for the wizard + current_page (int): Index of the currently displayed page + page_widgets (list): List of instantiated page widgets + + Args: + parent: Optional parent QWidget + title (str): Title to display in the dialog header + pages (list): List of page constructors (callables) for each step + """ + class BalWizardDialog(BalDialog): def __init__(self, bal_window: "BalWindow"): assert bal_window