''' Bal Bitcoin after life ''' import copy from datetime import datetime from decimal import Decimal import enum from functools import partial import json import os import random import sys import traceback import time from typing import ( TYPE_CHECKING, Callable, Optional, List, Union, Tuple, Mapping,Any) import urllib.parse import urllib.request try: QT_VERSION=sys._GUI_QT_VERSION except: QT_VERSION=6 if QT_VERSION == 5: from PyQt5.QtGui import QStandardItemModel, QStandardItem from PyQt5.QtCore import Qt,QPersistentModelIndex, QModelIndex from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,QMenu) from PyQt5.QtCore import ( QPersistentModelIndex, QModelIndex, Qt, QRectF, QRect, QSizeF, QUrl, QPoint, QSize, QDateTime, pyqtProperty, pyqtSignal, pyqtSlot, QObject, QEventLoop, pyqtSignal) from PyQt5.QtGui import ( QStandardItemModel, QStandardItem, QPalette, QColor, QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont, QColor, QDesktopServices, qRgba, QPainterPath, QPalette, QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont, QIcon, QColor, QDesktopServices, qRgba, QPainterPath, QPalette) from PyQt5.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QMenu, QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QScrollArea, QAbstractItemView, QWidget, QDateTimeEdit, QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy, QCheckBox, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, QMenuBar, QPushButton, QScrollArea, QSpacerItem, QSizePolicy, QSpinBox, QVBoxLayout, QWidget, QStyle, QStyleOptionFrame, QComboBox, QHBoxLayout, ) else: #QT6 from PyQt6.QtCore import ( Qt, QDateTime, QPersistentModelIndex, QModelIndex, pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtSignal, QSize, Qt, QRectF, QRect, QSizeF, QUrl, QPoint, QSize) from PyQt6.QtGui import ( QStandardItemModel, QStandardItem, QPalette, QColor, QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont, QColor, QDesktopServices, qRgba, QPainterPath, QPalette, QPainter, QPixmap, QImage, QBitmap, QPainter, QFontDatabase, QPen, QFont, QIcon, QColor, QDesktopServices, qRgba, QPainterPath, QPalette) from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QMenu, QAbstractItemView, QWidget, QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QWidget, QScrollArea, QDateTimeEdit, QLabel, QVBoxLayout, QCheckBox, QWidget, QLabel, QVBoxLayout, QCheckBox, QLineEdit, QStyle, QStyleOptionFrame, QSizePolicy, QGridLayout, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QCheckBox, QSpinBox, QMenuBar, QMenu, QLineEdit, QScrollArea, QWidget, QSpacerItem, QComboBox, QSizePolicy) from electrum import json_db from electrum import constants from electrum.bitcoin import ( is_address, NLOCKTIME_MIN, NLOCKTIME_MAX, NLOCKTIME_BLOCKHEIGHT_MAX) from electrum.gui.qt.amountedit import ( BTCAmountEdit, char_width_in_lineedit, ColorScheme) if TYPE_CHECKING: from electrum.gui.qt.main_window import ElectrumWindow from electrum.gui.qt.util import ( Buttons, read_QIcon, import_meta_gui, export_meta_gui, MessageBoxMixin, Buttons, read_QIcon, export_meta_gui, MessageBoxMixin, char_width_in_lineedit, ColorScheme, Buttons, CancelButton, char_width_in_lineedit, CloseButton, EnterButton, HelpButton, MessageBoxMixin, OkButton, TaskThread, WindowModalDialog, WWLabel, ) from electrum.gui.qt.main_window import StatusBarButton from electrum.gui.qt.my_treeview import ( MyTreeView, MySortModel) from electrum.gui.qt.transaction_dialog import TxDialog from electrum.gui.qt.password_dialog import PasswordDialog from electrum.gui.qt.qrtextedit import ScanQRTextEdit from electrum.i18n import _ from electrum.logging import get_logger,Logger from electrum.json_db import StoredDict from electrum.network import ( Network, TxBroadcastError, BestEffortRequestFailed ) from electrum.plugin import ( hook, run_hook) from electrum.transaction import ( SerializationError, Transaction, tx_from_any) from electrum.util import ( write_json_file, read_json_file, make_dir, InvalidPassword, UserCancelled, resource_path, write_json_file, read_json_file, FileImportFailed, bfh, read_json_file, write_json_file, decimal_point_to_base_unit_name, FileImportFailed, DECIMAL_POINT, FEERATE_PRECISION, quantize_feerate, UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE, FileExportFailed) from .bal import BalPlugin from .bal_resources import ( DEFAULT_ICON, icon_path) from .heirs import Heirs from .util import Util from .will import ( Will, WillItem, NoHeirsException, NoWillExecutorNotPresent, NotCompleteWillException, AmountException, HeirNotFoundException, HeirChangeException, WillexecutorChangeException, WillExecutorNotPresent, TxFeesChangedException, WillExpiredException) 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('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.info("HOOK load wallet") w = self.get_window(main_window) 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): for winid,win in self.bal_windows.items(): if win.wallet == wallet: win.on_close() 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) return EnterButton(_('Settings'), partial(w.settings_dialog,window)) 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.get_window_title("Settings")) d.setMinimumSize(100, 200) qicon=read_QPixmap("bal32x32.png") lbl_logo = QLabel() lbl_logo.setPixmap(qicon) heir_ping_willexecutors = bal_checkbox(self, BalPlugin.PING_WILLEXECUTORS) heir_ask_ping_willexecutors = bal_checkbox(self, BalPlugin.ASK_PING_WILLEXECUTORS) heir_no_willexecutor = bal_checkbox(self, BalPlugin.NO_WILLEXECUTOR) heir_hide_replaced = bal_checkbox(self,BalPlugin.HIDE_REPLACED,self) heir_hide_invalidated = bal_checkbox(self,BalPlugin.HIDE_INVALIDATED,self) 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") grid.addWidget(heir_repush,6,0) grid.addWidget(HelpButton("Broadcast all transactions to willexecutors including those already pushed"),6,2) if ret := bool(d.exec()): try: self.update_all() return ret except: 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 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("heir.png"), _("&Heirs")) add_optional_tab(self.window.tabs, self.will_tab, read_QIcon("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") 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") 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 show_willexecutor_dialog(self): self.willexecutor_dialog = WillExecutorDialog(self) self.willexecutor_dialog.show() def create_heirs_tab(self): self.heir_list = l = HeirList(self) 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,None) tab = self.window.create_list_tab(l) tab.is_shown_cv = shown_cv(True) return tab def new_heir_dialog(self): d = BalDialog(self.window, self.bal_plugin.get_window_title("New heir")) vbox = QVBoxLayout(d) grid = QGridLayout() heir_name = QLineEdit() heir_name.setFixedWidth(32 * char_width_in_lineedit()) heir_address = QLineEdit() heir_address.setFixedWidth(32 * char_width_in_lineedit()) heir_amount = PercAmountEdit(self.window.get_decimal_point) heir_locktime = HeirsLockTimeEdit(self.window,0) heir_is_xpub = QCheckBox() 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) vbox.addLayout(grid) vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) while d.exec(): #TODO SAVE HEIR heir = [ heir_name.text(), heir_address.text(), Util.encode_amount(heir_amount.text(),self.bal_plugin.config.get_decimal_point()), str(heir_locktime.get_locktime()), ] try: self.set_heir(heir) 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) 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['tx_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['tx_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['tx_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): for heir in self.heirs: h=self.heirs[heir] 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.config_get(BalPlugin.LOCKTIME_BLOCKS) 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.config_get(BalPlugin.NO_WILLEXECUTOR) self.willexecutors = Willexecutors.get_willexecutors(self.bal_plugin,update=True,bal_window=self,task=False) self.init_heirs_to_locktime() 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 as e: 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("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('tx_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 as e: 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=BalCloseDialog(self) close_window.close_plugin_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 as e: 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: 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']: row = self.waiting_dialog.update("checking {} - {} : {}".format(self.willitems[wid].we['url'],wid, "Waiting")) self.willitems[wid].check_willexecutor() row = 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]=Will.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(): if w.we: 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: 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): def on_success(result): del self.waiting_dialog try: self.willexecutor_dialog.willexecutor_list.update() except Exception as e: _logger.error("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 HeirsLockTimeEdit(QWidget): 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) 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 _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 as e: return False return cls.min_allowed_value <= x <= cls.max_allowed_value 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 as e: pass return pos, s def numbify(self): text = self.text().strip() #chars = '0123456789bdy' removed the option to choose locktime by block chars = '0123456789dy' pos = posx = 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 as e: 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 as e: 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,title=None, icon = 'bal32x32.png'): self.parent=parent WindowModalDialog.__init__(self,self.parent,title) self.setWindowIcon(read_QIcon(icon)) 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, _("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, _("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, plugin,variable,window=None): QCheckBox.__init__(self) self.setChecked(plugin.config_get(variable)) def on_check(v): plugin.config.set_key(variable, v == 2) plugin.config_get(variable) self.stateChanged.connect(on_check) class BalCloseDialog(BalDialog): updatemessage=pyqtSignal() def __init__(self,bal_window): BalDialog.__init__(self,bal_window.window,"Closing BAL") self.updatemessage.connect(self.update) self.bal_window=bal_window self.message_label = QLabel("Closing BAL:") 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 close_plugin_task(self): _logger.debug("close 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: return False, None self.msg_set_status("checking variables","Waiting") try: 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()) except AmountException: self.msg_edit_row(''+_("In the inheritance process, the entire wallet will always be fully emptied. 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 as e: self.msg_set_checking("Expired") fee_per_byte=self.bal_window.will_settings.get('tx_fees',1) return None, Will.invalidate_will(self.bal_window.willitems,self.bal_window.wallet,fee_per_byte) except NoHeirsException: self.msg_set_checking("No Heirs") except NotCompleteWillException as 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: 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,tx,password): _logger.debug(f"invalidate tx: {tx}") 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 else: raise except Exception as e: self.msg_set_invalidating("Error") 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: 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.bal_window.window) if password is False: self.msg_set_invalidating("Aborted") self.wait(3) self.close() return self.thread.add(partial(self.invalidate_task,tx,password),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.bal_window.window) 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("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 = None, 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,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 as e: 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 as e: 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() def ThreadStopped(Exception): 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'): super().__init__( parent=bal_window.window, 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.bal_plugin.config.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: pass self.setSortingEnabled(True) self.std_model = self.model() self.update() 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 as e: prior_name = (edit_key,)+prior_name[:col-1]+(text,)+prior_name[col:] try: self.bal_window.set_heir(prior_name) except Exception as e: pass try: self.bal_window.set_heir((edit_key,)+original) except Exception as e: 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() 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 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) self.model().insertRow(self.model().rowCount(), 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) 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.window(),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.config.set_key('will_settings',self.bal_window.will_settings,save = True) 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.config.set_key('will_settings',self.bal_window.will_settings,save = True) 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['tx_fees'] = self.heir_tx_fees.value() self.bal_window.bal_plugin.config.set_key('will_settings',self.bal_window.will_settings,save = True) 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): self.heir_threshold.set_locktime(self.bal_window.will_settings['threshold']) self.heir_locktime.set_locktime(self.bal_window.will_settings['locktime']) self.heir_tx_fees.setValue(int(self.bal_window.will_settings['tx_fees'])) def build_transactions(self): 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, parent: 'BalWindow',will): super().__init__( parent=parent.window, stretch_column=self.Columns.TXID, ) self.decimal_point=parent.bal_plugin.config.get_decimal_point self.setModel(QStandardItemModel(self)) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) if not will is None: self.will = will else: self.will = parent.willitems self.bal_window = parent self.wallet=parent.window.wallet self.setModel(QStandardItemModel(self)) self.setSortingEnabled(True) self.std_model = self.model() self.config = parent.bal_plugin.config self.bal_plugin=self.bal_window.bal_plugin self.update() 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)) 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: pass try: del self.bal_window.will[key] except: 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 update(self): 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(): if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status('REPLACED'): continue if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status('INVALIDATED'): continue 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 type(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())) self.model().insertRow(self.model().rowCount(), items) if txid == current_key: idx = self.model().index(row_count, self.Columns.TXID) set_current = QPersistentModelIndex(idx) self.set_current_idx(set_current) 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) menu.addAction(_("Broadcast"), self.broadcast) menu.addAction(_("Check"), self.check) menu.addAction(_("Invalidate"), self.invalidate_will) prepareButton = QPushButton(_("Prepare")) prepareButton.clicked.connect(self.build_transactions) signButton = QPushButton(_("Sign")) signButton.clicked.connect(self.ask_password_and_sign_transactions) pushButton = QPushButton(_("Broadcast")) pushButton.clicked.connect(self.broadcast) displayButton = QPushButton(_("Display")) displayButton.clicked.connect(self.bal_window.preview_modal_dialog) hlayout = QHBoxLayout() widget = QWidget() hlayout.addWidget(prepareButton) hlayout.addWidget(signButton) hlayout.addWidget(pushButton) hlayout.addWidget(displayButton) widget.setLayout(hlayout) toolbar.insertWidget(2,widget) return toolbar def hide_replaced(self): self.bal_window.bal_plugin.hide_replaced() self.update() def hide_invalidated(self): f.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): self.bal_window.check_transactions(self.bal_window.willitems) 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) 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_QIcon(icon_basename: str=DEFAULT_ICON) -> QIcon: return QIcon(icon_path(icon_basename)) def read_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) 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.bal_plugin.config.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)) total_fees = self.will[w].tx.input_value() - self.will[w].tx.output_value() 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: