'''
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 (
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: