""" 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 try: QT_VERSION = sys._GUI_QT_VERSION except Exception: QT_VERSION = 6 if QT_VERSION == 5: from PyQt5.QtCore import ( QDateTime, QModelIndex, QPersistentModelIndex, Qt, pyqtSignal, ) from PyQt5.QtGui import ( QColor, QIcon, QPainter, QPalette, QPixmap, 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 ( QDateTime, QModelIndex, QPersistentModelIndex, Qt, pyqtSignal, ) from PyQt6.QtGui import ( QColor, QIcon, QPainter, QPalette, QPixmap, 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, 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.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 .bal_resources import DEFAULT_ICON, icon_path from .heirs import Heirs 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 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: raise e self.logger.error(("except:", menu_child.text())) 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.heirs_tab = self.create_heirs_tab() self.will_tab = self.create_will_tab() self.ok = False self.disable_plugin = True self.bal_plugin.get_decimal_point = self.window.get_decimal_point if self.window.wallet: 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: 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.db)) 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: 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.update_will_settings() 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 = l = HeirList(self, self.window) tab = self.window.create_list_tab(l) tab.is_shown_cv = shown_cv(True) return tab def create_will_tab(self): self.will_list = l = PreviewList(self, self.window, None) tab = self.window.create_list_tab(l) 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 export_inheritance_handler(self,path): # txs = self.build_inheritance_transaction(ignore_duplicate=True, keep_original=False) # with open(path,"w") as f: # for tx in txs: # tx['status']+="."+BalPlugin.STATUS_EXPORTED # f.write(str(tx['tx'])) # f.write('\n') 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.update() return True def delete_heirs(self, heirs): for heir in heirs: del self.heirs[heir] self.heirs.save() self.heir_list.update() return True def import_heirs( self, ): import_meta_gui( self.window, _("heirs"), self.heirs.import_file, self.heir_list.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): 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: 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(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) except Exception as 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() return 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 = self.current_block + self.locktime_blocks self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get() self.willexecutors = Willexecutors.get_willexecutors( self.bal_plugin, update=True, bal_window=self, task=False ) self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get()) except Exception as e: self.logger.error(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 self.init_class_variables() try: 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.\n{e}" ) ) 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 != 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") a = 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: 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: 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): self.logger.error(err) 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" ) ) self.willitems[wid].check_willexecutor() 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.check_willexecutor() 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.willexecutor_list.update() except Exception as e: _logger.error(f"error updating willexecutors {e}") pass def on_failure(e): self.logger.error(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): self.will_list.update_will(self.willitems) self.heirs_tab.update() self.will_tab.update() self.will_list.update() 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 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, self) 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): pass def on_reject(self): pass def on_close(self): pass def closeEvent(self, event): self.bal_window.update_all() self.bal_window.heir_list.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.heirs_list = HeirList(self.bal_window, self.parent) 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_import.clicked.connect(self.export_to_file) widget = QWidget() vbox = QVBoxLayout(widget) vbox.addWidget(self.heirs_list) vbox.addLayout(Buttons(button_add, button_import, button_export)) return widget def import_from_file(self): self.bal_window.import_heirs() self.heirs_list.update() def export_to_file(self): self.bal_window.export_heirs() def add_heir(self): self.bal_window.new_heir_dialog() self.heirs_list.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.json"), 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) 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.bal_plugin) 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() 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() def __init__(self, bal_window, parent=None): if not parent: parent = bal_window.window BalDialog.__init__(self, parent, bal_window.bal_plugin, "Building Will") self.updatemessage.connect(self.update) self.bal_window = bal_window self.message_label = QLabel("Building Will:") self.vbox = QVBoxLayout(self) self.vbox.addWidget(self.message_label) self.qwidget = QWidget() self.vbox.addWidget(self.qwidget) self.labels = [] 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): _logger.debug("close plugin phase 1 started") try: self.bal_window.init_class_variables() except NoHeirsException: _logger.error("no heirs exception") return False, None varrow = self.msg_set_status("checking variables") 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") 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.msg_set_checking() have_to_build = False try: self.bal_window.check_will() self.msg_set_checking("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: _logger.debug("no heirs") self.msg_set_checking("No Heirs") except NotCompleteWillException as e: _logger.debug("not complete", e) 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("message") self.msg_set_checking(message) else: self.msg_set_checking("New") if have_to_build: self.msg_set_building() try: self.bal_window.build_will() 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("Ok") except Exception as e: self.msg_set_building(self.msg_error(e)) return False, None 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, None def on_accept(self): pass def on_accept_phase2(self): 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("Ok") if not txid: _logger.debug(f"should not be none txid: {txid}") except TxBroadcastError as e: _logger.error(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_window.willitems[wid].check_willexecutor() 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(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_error(self, error): _logger.error(error) pass def on_success_phase1(self, result): self.have_to_sign, tx = list(result) _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.bal_window.update_all() 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("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("Ok") except Exception as e: self.msg_set_pushing(self.msg_error(e)) self.msg_edit_row("Ok") self.wait(5) def on_error_phase1(self, error): _logger.error(f"error phase1: {error}") def on_error_phase2(self, error): _logger.error("error phase2: { error}") 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 "Error: {}".format(e) def msg_set_status(self, msg, row=None, status=None): status = "Wait" if status is None else status line = "{}:\t{}".format(_(msg), status) return self.msg_edit_row(line, row) def ask_password(self, msg=None): self.password = self.bal_window.get_wallet_password(msg, parent=self) def msg_edit_row(self, line, row=None): _logger.debug(f"{row},{line}") try: self.labels[row] = line except Exception: self.labels.append(line) row = len(self.labels) - 1 self.updatemessage.emit() return row def msg_del_row(self, row): try: del self.labels[row] except Exception: pass self.updatemessage.emit() def update(self): self.vbox.removeWidget(self.qwidget) self.qwidget = QWidget(self) labelsbox = QVBoxLayout(self.qwidget) for label in self.labels: labelsbox.addWidget(QLabel(label)) self.vbox.addWidget(self.qwidget) def get_text(self): return self.message_label.text() pass class HeirList(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 + 1001 key_role = ROLE_HEIR_KEY 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 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.bal_window.delete_heirs(selected_keys) ) menu.exec(self.viewport().mapToGlobal(position)) # def get_selected_keys(self): # 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) # return selected_keys def update(self): if self.maybe_defer_update(): return 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 + 1) items[self.Columns.ADDRESS].setData(key, self.ROLE_HEIR_KEY + 2) items[self.Columns.AMOUNT].setData(key, self.ROLE_HEIR_KEY + 3) 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): return self.get_role_data_from_coordinate( row, col, role=self.ROLE_HEIR_KEY + col + 1 ) 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_tx_fees.setValue(int(self.bal_window.will_settings["baltx_fees"])) self.heir_threshold.set_locktime(self.bal_window.will_settings["threshold"]) except Exception as e: _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.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 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) widget = QWidget() hlayout = QHBoxLayout(widget) hlayout.addWidget(wizard) hlayout.addWidget(display) 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): will = {} for wid, w in self.bal_window.willitems.items(): if w.get_status("VALID"): will[wid] = w 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) 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() def read_bal_QIcon(icon_basename: str = DEFAULT_ICON) -> QIcon: return QIcon(icon_path(icon_basename)) def read_bal_QPixmap(icon_basename: str = DEFAULT_ICON) -> QPixmap: return QPixmap(icon_path(icon_basename)) 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: