forked from bitcoinafterlife/bal-electrum-plugin
3594 lines
121 KiB
Python
3594 lines
121 KiB
Python
"""
|
|
|
|
Bal
|
|
|
|
Bitcoin after life
|
|
|
|
|
|
"""
|
|
|
|
import copy
|
|
import enum
|
|
import sys
|
|
import time
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from functools import partial
|
|
from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Union
|
|
|
|
try:
|
|
QT_VERSION = sys._GUI_QT_VERSION
|
|
except Exception:
|
|
QT_VERSION = 6
|
|
|
|
if QT_VERSION == 5:
|
|
from PyQt5.QtCore import (
|
|
QDateTime,
|
|
QModelIndex,
|
|
QPersistentModelIndex,
|
|
Qt,
|
|
pyqtSignal,
|
|
)
|
|
from PyQt5.QtGui import (
|
|
QColor,
|
|
QIcon,
|
|
QPainter,
|
|
QPalette,
|
|
QPixmap,
|
|
QStandardItem,
|
|
QStandardItemModel,
|
|
)
|
|
from PyQt5.QtWidgets import (
|
|
QAbstractItemView,
|
|
QCheckBox,
|
|
QComboBox,
|
|
QDateTimeEdit,
|
|
QGridLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMenu,
|
|
QMenuBar,
|
|
QPushButton,
|
|
QScrollArea,
|
|
QSizePolicy,
|
|
QSpinBox,
|
|
QStyle,
|
|
QStyleOptionFrame,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
else: # QT6
|
|
from PyQt6.QtCore import (
|
|
QDateTime,
|
|
QModelIndex,
|
|
QPersistentModelIndex,
|
|
Qt,
|
|
pyqtSignal,
|
|
)
|
|
from PyQt6.QtGui import (
|
|
QColor,
|
|
QIcon,
|
|
QPainter,
|
|
QPalette,
|
|
QPixmap,
|
|
QStandardItem,
|
|
QStandardItemModel,
|
|
)
|
|
from PyQt6.QtWidgets import (
|
|
QAbstractItemView,
|
|
QCheckBox,
|
|
QComboBox,
|
|
QDateTimeEdit,
|
|
QGridLayout,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QLineEdit,
|
|
QMenu,
|
|
QMenuBar,
|
|
QPushButton,
|
|
QScrollArea,
|
|
QSizePolicy,
|
|
QSpinBox,
|
|
QStyle,
|
|
QStyleOptionFrame,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from electrum.bitcoin import (
|
|
NLOCKTIME_BLOCKHEIGHT_MAX,
|
|
NLOCKTIME_MAX,
|
|
NLOCKTIME_MIN,
|
|
)
|
|
from electrum.gui.qt.amountedit import (
|
|
BTCAmountEdit,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from electrum.gui.qt.main_window import ElectrumWindow
|
|
|
|
from electrum.gui.qt.main_window import StatusBarButton
|
|
from electrum.gui.qt.my_treeview import MyTreeView
|
|
from electrum.gui.qt.password_dialog import PasswordDialog
|
|
from electrum.gui.qt.transaction_dialog import TxDialog
|
|
from electrum.gui.qt.util import (
|
|
Buttons,
|
|
CancelButton,
|
|
ColorScheme,
|
|
EnterButton,
|
|
HelpButton,
|
|
MessageBoxMixin,
|
|
OkButton,
|
|
TaskThread,
|
|
WindowModalDialog,
|
|
char_width_in_lineedit,
|
|
import_meta_gui,
|
|
read_QIcon,
|
|
read_QIcon_from_bytes,
|
|
read_QPixmap_from_bytes,
|
|
)
|
|
from electrum.i18n import _
|
|
from electrum.logging import Logger, get_logger
|
|
from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError
|
|
from electrum.payment_identifier import PaymentIdentifier
|
|
from electrum.plugin import hook, run_hook
|
|
from electrum.transaction import SerializationError, Transaction, tx_from_any
|
|
from electrum.util import (
|
|
DECIMAL_POINT,
|
|
FileImportFailed,
|
|
UserCancelled,
|
|
decimal_point_to_base_unit_name,
|
|
read_json_file,
|
|
write_json_file,
|
|
)
|
|
|
|
from .bal import BalPlugin
|
|
from .bal_resources import DEFAULT_ICON, icon_path
|
|
from .heirs import Heirs
|
|
from .util import Util
|
|
from .will import (
|
|
AmountException,
|
|
HeirChangeException,
|
|
HeirNotFoundException,
|
|
NoHeirsException,
|
|
NotCompleteWillException,
|
|
NoWillExecutorNotPresent,
|
|
TxFeesChangedException,
|
|
Will,
|
|
WillexecutorChangeException,
|
|
WillExecutorNotPresent,
|
|
WillExpiredException,
|
|
WillItem,
|
|
)
|
|
from .willexecutors import Willexecutors
|
|
|
|
_logger = get_logger(__name__)
|
|
|
|
|
|
class Plugin(BalPlugin, Logger):
|
|
def __init__(self, parent, config, name):
|
|
Logger.__init__(self)
|
|
self.logger.info("INIT BALPLUGIN")
|
|
BalPlugin.__init__(self, parent, config, name)
|
|
self.bal_windows = {}
|
|
|
|
@hook
|
|
def init_qt(self, gui_object):
|
|
self.logger.info("HOOK bal init qt")
|
|
try:
|
|
self.gui_object = gui_object
|
|
for window in gui_object.windows:
|
|
wallet = window.wallet
|
|
if wallet:
|
|
window.show_warning(
|
|
_("Please restart Electrum to activate the BAL plugin"),
|
|
title=_("Success"),
|
|
)
|
|
return
|
|
w = BalWindow(self, window)
|
|
self.bal_windows[window.winId] = w
|
|
for child in window.children():
|
|
if isinstance(child, QMenuBar):
|
|
for menu_child in child.children():
|
|
if isinstance(menu_child, QMenu):
|
|
try:
|
|
if menu_child.title() == _("&Tools"):
|
|
w.init_menubar_tools(menu_child)
|
|
|
|
except Exception as e:
|
|
raise e
|
|
self.logger.error(("except:", menu_child.text()))
|
|
|
|
except Exception as e:
|
|
self.logger.error("Error loading plugini {}".format(e))
|
|
raise e
|
|
|
|
@hook
|
|
def create_status_bar(self, sb):
|
|
self.logger.info("HOOK create status bar")
|
|
return
|
|
b = StatusBarButton(
|
|
read_QIcon_from_bytes(self.bal_plugin.read_file("icons/bal32x32.png")),
|
|
"Bal " + _("Bitcoin After Life"),
|
|
partial(self.setup_dialog, sb),
|
|
sb.height(),
|
|
)
|
|
sb.addPermanentWidget(b)
|
|
|
|
@hook
|
|
def init_menubar(self, window):
|
|
self.logger.info("HOOK init_menubar")
|
|
w = self.get_window(window)
|
|
w.init_menubar_tools(window.tools_menu)
|
|
|
|
@hook
|
|
def load_wallet(self, wallet, main_window):
|
|
self.logger.debug("HOOK load wallet")
|
|
w = self.get_window(main_window)
|
|
# havetoupdate = Util.fix_will_settings_tx_fees(wallet.db)
|
|
w.wallet = wallet
|
|
w.init_will()
|
|
w.willexecutors = Willexecutors.get_willexecutors(
|
|
self, update=False, bal_window=w
|
|
)
|
|
w.disable_plugin = False
|
|
w.ok = True
|
|
|
|
@hook
|
|
def close_wallet(self, wallet):
|
|
self.logger.debug("HOOK close wallet")
|
|
for winid, win in self.bal_windows.items():
|
|
if win.wallet == wallet:
|
|
win.on_close()
|
|
|
|
@hook
|
|
def init_keystore(self):
|
|
self.logger.debug("init keystore")
|
|
|
|
@hook
|
|
def daemon_wallet_loaded(self, boh, wallet):
|
|
self.logger.debug("daemon wallet loaded")
|
|
|
|
def get_window(self, window):
|
|
w = self.bal_windows.get(window.winId, None)
|
|
if w is None:
|
|
w = BalWindow(self, window)
|
|
self.bal_windows[window.winId] = w
|
|
return w
|
|
|
|
def requires_settings(self):
|
|
return True
|
|
|
|
def settings_widget(self, window):
|
|
|
|
w = self.get_window(window.window)
|
|
widget = QWidget()
|
|
enterbutton = EnterButton(_("Settings"), partial(w.settings_dialog, window))
|
|
|
|
widget.setLayout(Buttons(enterbutton, widget))
|
|
return widget
|
|
|
|
def password_dialog(self, msg=None, parent=None):
|
|
parent = parent or self
|
|
d = PasswordDialog(parent, msg)
|
|
return d.run()
|
|
|
|
def get_seed(self):
|
|
password = None
|
|
if self.wallet.has_keystore_encryption():
|
|
password = self.password_dialog(parent=self.d.parent())
|
|
if not password:
|
|
raise UserCancelled()
|
|
|
|
keystore = self.wallet.get_keystore()
|
|
if not keystore or not keystore.has_seed():
|
|
return
|
|
self.extension = bool(keystore.get_passphrase(password))
|
|
return keystore.get_seed(password)
|
|
|
|
def settings_dialog(self, window, wallet):
|
|
|
|
d = BalDialog(window, self, self.get_window_title("Settings"))
|
|
d.setMinimumSize(100, 200)
|
|
qicon = read_QPixmap_from_bytes(self.read_file("icons/bal32x32.png"))
|
|
lbl_logo = QLabel()
|
|
lbl_logo.setPixmap(qicon)
|
|
|
|
#heir_ping_willexecutors = bal_checkbox(self.PING_WILLEXECUTORS)
|
|
#heir_ask_ping_willexecutors = bal_checkbox(self.ASK_PING_WILLEXECUTORS)
|
|
heir_no_willexecutor = bal_checkbox(self.NO_WILLEXECUTOR)
|
|
|
|
def on_multiverse_change():
|
|
self.update_all()
|
|
|
|
# heir_enable_multiverse = bal_checkbox(self.ENABLE_MULTIVERSE,on_multiverse_change)
|
|
|
|
heir_hide_replaced = bal_checkbox(self.HIDE_REPLACED, on_multiverse_change)
|
|
|
|
heir_hide_invalidated = bal_checkbox(
|
|
self.HIDE_INVALIDATED, on_multiverse_change
|
|
)
|
|
|
|
heir_repush = QPushButton("Rebroadcast transactions")
|
|
heir_repush.clicked.connect(partial(self.broadcast_transactions, True))
|
|
grid = QGridLayout(d)
|
|
add_widget(
|
|
grid,
|
|
"Hide Replaced",
|
|
heir_hide_replaced,
|
|
1,
|
|
"Hide replaced transactions from will detail and list",
|
|
)
|
|
add_widget(
|
|
grid,
|
|
"Hide Invalidated",
|
|
heir_hide_invalidated,
|
|
2,
|
|
"Hide invalidated transactions from will detail and list",
|
|
)
|
|
#add_widget(
|
|
# grid,
|
|
# "Ping Willexecutors",
|
|
# heir_ping_willexecutors,
|
|
# 3,
|
|
# "Ping willexecutors to get payment info before compiling will",
|
|
#)
|
|
#add_widget(
|
|
# grid,
|
|
# " - Ask before",
|
|
# heir_ask_ping_willexecutors,
|
|
# 4,
|
|
# "Ask before to ping willexecutor",
|
|
#)
|
|
#add_widget(
|
|
# grid,
|
|
# "Backup Transaction",
|
|
# heir_no_willexecutor,
|
|
# 5,
|
|
# "Add transactions without willexecutor",
|
|
#)
|
|
# add_widget(grid,"Enable Multiverse(EXPERIMENTAL/BROKEN)",heir_enable_multiverse,6,"enable multiple locktimes, will import.... ")
|
|
grid.addWidget(heir_repush, 7, 0)
|
|
grid.addWidget(
|
|
HelpButton(
|
|
"Broadcast all transactions to willexecutors including those already pushed"
|
|
),
|
|
7,
|
|
2,
|
|
)
|
|
|
|
if ret := bool(d.exec()):
|
|
try:
|
|
self.update_all()
|
|
return ret
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
def broadcast_transactions(self, force):
|
|
for k, w in self.bal_windows.items():
|
|
w.broadcast_transactions(force)
|
|
|
|
def update_all(self):
|
|
for k, w in self.bal_windows.items():
|
|
w.update_all()
|
|
|
|
def get_window_title(self, title):
|
|
return _("BAL - ") + _(title)
|
|
|
|
|
|
class shown_cv:
|
|
_type = bool
|
|
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
def get(self):
|
|
return self.value
|
|
|
|
def set(self, value):
|
|
self.value = value
|
|
|
|
|
|
class BalWindow(Logger):
|
|
def __init__(self, bal_plugin: "BalPlugin", window: "ElectrumWindow"):
|
|
Logger.__init__(self)
|
|
self.bal_plugin = bal_plugin
|
|
self.window = window
|
|
self.heirs = {}
|
|
self.will = {}
|
|
self.willitems = {}
|
|
self.willexecutors = {}
|
|
self.will_settings = None
|
|
self.heirs_tab = self.create_heirs_tab()
|
|
self.will_tab = self.create_will_tab()
|
|
self.ok = False
|
|
self.disable_plugin = True
|
|
self.bal_plugin.get_decimal_point = self.window.get_decimal_point
|
|
|
|
if self.window.wallet:
|
|
self.wallet = self.window.wallet
|
|
self.heirs_tab.wallet = self.wallet
|
|
self.will_tab.wallet = self.wallet
|
|
|
|
def init_menubar_tools(self, tools_menu):
|
|
self.tools_menu = tools_menu
|
|
|
|
def add_optional_tab(tabs, tab, icon, description):
|
|
tab.tab_icon = icon
|
|
tab.tab_description = description
|
|
tab.tab_pos = len(tabs)
|
|
if tab.is_shown_cv.get():
|
|
tabs.addTab(tab, icon, description.replace("&", ""))
|
|
|
|
def add_toggle_action(tab):
|
|
is_shown = tab.is_shown_cv.get()
|
|
tab.menu_action = self.window.view_menu.addAction(
|
|
tab.tab_description, lambda: self.window.toggle_tab(tab)
|
|
)
|
|
tab.menu_action.setCheckable(True)
|
|
tab.menu_action.setChecked(is_shown)
|
|
|
|
add_optional_tab(
|
|
self.window.tabs,
|
|
self.heirs_tab,
|
|
read_QIcon_from_bytes(self.bal_plugin.read_file("icons/heir.png")),
|
|
_("&Heirs"),
|
|
)
|
|
add_optional_tab(
|
|
self.window.tabs,
|
|
self.will_tab,
|
|
read_QIcon_from_bytes(self.bal_plugin.read_file("icons/will.png")),
|
|
_("&Will"),
|
|
)
|
|
tools_menu.addSeparator()
|
|
self.tools_menu.willexecutors_action = tools_menu.addAction(
|
|
_("&Will-Executors"), self.show_willexecutor_dialog
|
|
)
|
|
self.window.view_menu.addSeparator()
|
|
add_toggle_action(self.heirs_tab)
|
|
add_toggle_action(self.will_tab)
|
|
|
|
def load_willitems(self):
|
|
self.willitems = {}
|
|
for wid, w in self.will.items():
|
|
self.willitems[wid] = WillItem(w, wallet=self.wallet)
|
|
if self.willitems:
|
|
self.will_list.will = self.willitems
|
|
self.will_list.update_will(self.willitems)
|
|
self.will_tab.update()
|
|
|
|
def save_willitems(self):
|
|
keys = list(self.will.keys())
|
|
for k in keys:
|
|
del self.will[k]
|
|
for wid, w in self.willitems.items():
|
|
self.will[wid] = w.to_dict()
|
|
|
|
def init_will(self):
|
|
self.logger.info("********************init_____will____________**********")
|
|
if not self.willexecutors:
|
|
self.willexecutors = Willexecutors.get_willexecutors(
|
|
self.bal_plugin, update=False, bal_window=self
|
|
)
|
|
if not self.heirs:
|
|
self.heirs = Heirs._validate(Heirs(self.wallet.db))
|
|
if not self.will:
|
|
self.will = self.wallet.db.get_dict("will")
|
|
Util.fix_will_tx_fees(self.will)
|
|
if self.will:
|
|
self.willitems = {}
|
|
try:
|
|
self.load_willitems()
|
|
except:
|
|
self.disable_plugin = True
|
|
self.show_warning(
|
|
_("Please restart Electrum to activate the BAL plugin"),
|
|
title=_("Success"),
|
|
)
|
|
self.close_wallet()
|
|
return
|
|
|
|
if not self.will_settings:
|
|
self.will_settings = self.wallet.db.get_dict("will_settings")
|
|
Util.fix_will_settings_tx_fees(self.will_settings)
|
|
|
|
self.logger.info("will_settings: {}".format(self.will_settings))
|
|
if not self.will_settings:
|
|
Util.copy(self.will_settings, self.bal_plugin.default_will_settings())
|
|
self.logger.debug("not_will_settings {}".format(self.will_settings))
|
|
|
|
self.bal_plugin.validate_will_settings(self.will_settings)
|
|
self.heir_list.update_will_settings()
|
|
|
|
def init_wizard(self):
|
|
wizard_dialog = BalWizardDialog(self)
|
|
wizard_dialog.exec()
|
|
|
|
def show_willexecutor_dialog(self):
|
|
self.willexecutor_dialog = WillExecutorDialog(self)
|
|
self.willexecutor_dialog.show()
|
|
|
|
def create_heirs_tab(self):
|
|
self.heir_list = l = HeirList(self, self.window)
|
|
|
|
tab = self.window.create_list_tab(l)
|
|
tab.is_shown_cv = shown_cv(False)
|
|
return tab
|
|
|
|
def create_will_tab(self):
|
|
self.will_list = l = PreviewList(self, self.window, None)
|
|
tab = self.window.create_list_tab(l)
|
|
tab.is_shown_cv = shown_cv(True)
|
|
return tab
|
|
|
|
|
|
def new_heir_dialog(self, heir_key=None):
|
|
heir = self.heirs.get(heir_key)
|
|
title = "New heir"
|
|
if heir:
|
|
title = f"Edit: {heir_key}"
|
|
|
|
d = BalDialog(
|
|
self.window, self.bal_plugin, self.bal_plugin.get_window_title(_(title))
|
|
)
|
|
|
|
vbox = QVBoxLayout(d)
|
|
grid = QGridLayout()
|
|
|
|
heir_name = QLineEdit()
|
|
heir_name.setFixedWidth(32 * char_width_in_lineedit())
|
|
if heir:
|
|
heir_name.setText(str(heir_key))
|
|
heir_address = QLineEdit()
|
|
heir_address.setFixedWidth(32 * char_width_in_lineedit())
|
|
if heir:
|
|
heir_address.setText(str(heir[0]))
|
|
heir_amount = PercAmountEdit(self.window.get_decimal_point)
|
|
if heir:
|
|
heir_amount.setText(
|
|
str(Util.decode_amount(heir[1], self.window.get_decimal_point()))
|
|
)
|
|
self.heir_locktime = HeirsLockTimeEdit(self.window, 0)
|
|
if heir:
|
|
self.heir_locktime.set_locktime(heir[2])
|
|
|
|
heir_is_xpub = QCheckBox()
|
|
|
|
new_heir_button = QPushButton(_("Add another heir"))
|
|
self.add_another_heir = False
|
|
|
|
def new_heir():
|
|
self.add_another_heir = True
|
|
d.accept()
|
|
|
|
new_heir_button.clicked.connect(new_heir)
|
|
new_heir_button.setDefault(True)
|
|
|
|
grid.addWidget(QLabel(_("Name")), 1, 0)
|
|
grid.addWidget(heir_name, 1, 1)
|
|
grid.addWidget(HelpButton(_("Unique name or description about heir")), 1, 2)
|
|
|
|
grid.addWidget(QLabel(_("Address")), 2, 0)
|
|
grid.addWidget(heir_address, 2, 1)
|
|
grid.addWidget(HelpButton(_("heir bitcoin address")), 2, 2)
|
|
|
|
grid.addWidget(QLabel(_("Amount")), 3, 0)
|
|
grid.addWidget(heir_amount, 3, 1)
|
|
grid.addWidget(HelpButton(_("Fixed or Percentage amount if end with %")), 3, 2)
|
|
|
|
locktime_label = QLabel(_("Locktime"))
|
|
enable_multiverse = self.bal_plugin.ENABLE_MULTIVERSE.get()
|
|
if enable_multiverse:
|
|
grid.addWidget(locktime_label, 4, 0)
|
|
grid.addWidget(self.heir_locktime, 4, 1)
|
|
grid.addWidget(HelpButton(_("locktime")), 4, 2)
|
|
|
|
vbox.addLayout(grid)
|
|
buttons = [CancelButton(d), OkButton(d)]
|
|
if not heir:
|
|
buttons.append(new_heir_button)
|
|
vbox.addLayout(Buttons(*buttons))
|
|
while d.exec():
|
|
# TODO SAVE HEIR
|
|
heir = [
|
|
heir_name.text(),
|
|
heir_address.text(),
|
|
Util.encode_amount(heir_amount.text(), self.window.get_decimal_point()),
|
|
str(self.heir_locktime.get_locktime()),
|
|
]
|
|
try:
|
|
self.set_heir(heir)
|
|
if self.add_another_heir:
|
|
self.new_heir_dialog()
|
|
break
|
|
except Exception as e:
|
|
self.show_error(str(e))
|
|
|
|
# def export_inheritance_handler(self,path):
|
|
# txs = self.build_inheritance_transaction(ignore_duplicate=True, keep_original=False)
|
|
# with open(path,"w") as f:
|
|
# for tx in txs:
|
|
# tx['status']+="."+BalPlugin.STATUS_EXPORTED
|
|
# f.write(str(tx['tx']))
|
|
# f.write('\n')
|
|
|
|
def set_heir(self, heir):
|
|
heir = list(heir)
|
|
if not self.bal_plugin.ENABLE_MULTIVERSE.get():
|
|
heir[3] = self.will_settings["locktime"]
|
|
|
|
h = Heirs.validate_heir(heir[0], heir[1:])
|
|
self.heirs[heir[0]] = h
|
|
self.heir_list.update()
|
|
return True
|
|
|
|
def delete_heirs(self, heirs):
|
|
for heir in heirs:
|
|
del self.heirs[heir]
|
|
self.heirs.save()
|
|
self.heir_list.update()
|
|
return True
|
|
|
|
def import_heirs(
|
|
self,
|
|
):
|
|
import_meta_gui(
|
|
self.window, _("heirs"), self.heirs.import_file, self.heir_list.update
|
|
)
|
|
|
|
def export_heirs(self):
|
|
Util.export_meta_gui(self.window, _("heirs"), self.heirs.export_file)
|
|
|
|
def prepare_will(self, ignore_duplicate=False, keep_original=False):
|
|
will = self.build_inheritance_transaction(
|
|
ignore_duplicate=ignore_duplicate, keep_original=keep_original
|
|
)
|
|
return will
|
|
|
|
def delete_not_valid(self, txid, s_utxo):
|
|
raise NotImplementedError()
|
|
|
|
def update_will(self, will):
|
|
Will.update_will(self.willitems, will)
|
|
self.willitems.update(will)
|
|
Will.normalize_will(self.willitems, self.wallet)
|
|
|
|
def build_will(self, ignore_duplicate=True, keep_original=True):
|
|
will = {}
|
|
willtodelete = []
|
|
willtoappend = {}
|
|
try:
|
|
self.init_class_variables()
|
|
self.willexecutors = Willexecutors.get_willexecutors(
|
|
self.bal_plugin, update=False, bal_window=self
|
|
)
|
|
if not self.no_willexecutor:
|
|
|
|
f = False
|
|
for u, w in self.willexecutors.items():
|
|
if Willexecutors.is_selected(w):
|
|
f = True
|
|
if not f:
|
|
raise NoWillExecutorNotPresent(
|
|
"No Will-Executor or backup transaction selected"
|
|
)
|
|
txs = self.heirs.get_transactions(
|
|
self.bal_plugin,
|
|
self.window.wallet,
|
|
self.will_settings["baltx_fees"],
|
|
None,
|
|
self.date_to_check,
|
|
)
|
|
self.logger.info(txs)
|
|
creation_time = time.time()
|
|
if txs:
|
|
for txid in txs:
|
|
txtodelete = []
|
|
_break = False
|
|
tx = {}
|
|
tx["tx"] = txs[txid]
|
|
tx["my_locktime"] = txs[txid].my_locktime
|
|
tx["heirsvalue"] = txs[txid].heirsvalue
|
|
tx["description"] = txs[txid].description
|
|
tx["willexecutor"] = copy.deepcopy(txs[txid].willexecutor)
|
|
tx["status"] = _("New")
|
|
tx["baltx_fees"] = txs[txid].tx_fees
|
|
tx["time"] = creation_time
|
|
tx["heirs"] = copy.deepcopy(txs[txid].heirs)
|
|
tx["txchildren"] = []
|
|
will[txid] = WillItem(tx, _id=txid, wallet=self.wallet)
|
|
self.update_will(will)
|
|
except Exception as e:
|
|
raise e
|
|
pass
|
|
return self.willitems
|
|
|
|
def check_will(self):
|
|
return Will.is_will_valid(
|
|
self.willitems,
|
|
self.block_to_check,
|
|
self.date_to_check,
|
|
self.will_settings["baltx_fees"],
|
|
self.window.wallet.get_utxos(),
|
|
heirs=self.heirs,
|
|
willexecutors=self.willexecutors,
|
|
self_willexecutor=self.no_willexecutor,
|
|
wallet=self.wallet,
|
|
callback_not_valid_tx=self.delete_not_valid,
|
|
)
|
|
|
|
def show_message(self, text):
|
|
self.window.show_message(text)
|
|
|
|
def show_warning(self, text, parent=None):
|
|
self.window.show_warning(text, parent=None)
|
|
|
|
def show_error(self, text):
|
|
self.window.show_error(text)
|
|
|
|
def show_critical(self, text):
|
|
self.window.show_critical(text)
|
|
|
|
def init_heirs_to_locktime(self, multiverse=False):
|
|
for heir in self.heirs:
|
|
h = self.heirs[heir]
|
|
if not multiverse:
|
|
self.heirs[heir] = [h[0], h[1], self.will_settings["locktime"]]
|
|
|
|
def init_class_variables(self):
|
|
if not self.heirs:
|
|
raise NoHeirsException()
|
|
return
|
|
try:
|
|
self.date_to_check = Util.parse_locktime_string(
|
|
self.will_settings["threshold"]
|
|
)
|
|
found = False
|
|
self.locktime_blocks = self.bal_plugin.LOCKTIME_BLOCKS.get()
|
|
self.current_block = Util.get_current_height(self.wallet.network)
|
|
self.block_to_check = self.current_block + self.locktime_blocks
|
|
self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get()
|
|
self.willexecutors = Willexecutors.get_willexecutors(
|
|
self.bal_plugin, update=True, bal_window=self, task=False
|
|
)
|
|
self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get())
|
|
|
|
except Exception as e:
|
|
self.logger.error(e)
|
|
raise e
|
|
|
|
def build_inheritance_transaction(self, ignore_duplicate=True, keep_original=True):
|
|
try:
|
|
if self.disable_plugin:
|
|
self.logger.info("plugin is disabled")
|
|
return
|
|
if not self.heirs:
|
|
self.logger.warning("not heirs {}".format(self.heirs))
|
|
return
|
|
self.init_class_variables()
|
|
try:
|
|
Will.check_amounts(
|
|
self.heirs,
|
|
self.willexecutors,
|
|
self.window.wallet.get_utxos(),
|
|
self.date_to_check,
|
|
self.window.wallet.dust_threshold(),
|
|
)
|
|
except AmountException as e:
|
|
self.show_warning(
|
|
_(
|
|
f"In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.\n{e}"
|
|
)
|
|
)
|
|
locktime = Util.parse_locktime_string(self.will_settings["locktime"])
|
|
if locktime < self.date_to_check:
|
|
self.show_error(_("locktime is lower than threshold"))
|
|
return
|
|
if not self.no_willexecutor:
|
|
f = False
|
|
for k, we in self.willexecutors.items():
|
|
if Willexecutors.is_selected(we):
|
|
f = True
|
|
if not f:
|
|
self.show_error(
|
|
_(" no backup transaction or willexecutor selected")
|
|
)
|
|
return
|
|
|
|
try:
|
|
self.check_will()
|
|
except WillExpiredException:
|
|
self.invalidate_will()
|
|
return
|
|
except NoHeirsException:
|
|
return
|
|
except NotCompleteWillException as e:
|
|
self.logger.info("{}:{}".format(type(e), e))
|
|
message = False
|
|
if isinstance(e, HeirChangeException):
|
|
message = "Heirs changed:"
|
|
elif isinstance(e, WillExecutorNotPresent):
|
|
message = "Will-Executor not present:"
|
|
elif isinstance(e, WillexecutorChangeException):
|
|
message = "Will-Executor changed"
|
|
elif isinstance(e, TxFeesChangedException):
|
|
message = "Txfees are changed"
|
|
elif isinstance(e, HeirNotFoundException):
|
|
message = "Heir not found"
|
|
|
|
if message:
|
|
self.show_message(
|
|
f"{_(message)}:\n {e}\n{_('will have to be built')}"
|
|
)
|
|
|
|
self.logger.info("build will")
|
|
self.build_will(ignore_duplicate, keep_original)
|
|
|
|
try:
|
|
self.check_will()
|
|
for wid, w in self.willitems.items():
|
|
self.wallet.set_label(wid, "BAL Transaction")
|
|
except WillExpiredException as e:
|
|
self.invalidate_will()
|
|
except NotCompleteWillException as e:
|
|
self.show_error(
|
|
"Error:{}\n {}".format(
|
|
str(e),
|
|
_("Please, check your heirs, locktime and threshold!"),
|
|
)
|
|
)
|
|
|
|
self.window.history_list.update()
|
|
self.window.utxo_list.update()
|
|
self.update_all()
|
|
return self.willitems
|
|
except Exception as e:
|
|
raise e
|
|
|
|
def show_transaction_real(
|
|
self,
|
|
tx: Transaction,
|
|
*,
|
|
parent: "ElectrumWindow",
|
|
prompt_if_unsaved: bool = False,
|
|
external_keypairs: Mapping[bytes, bytes] = None,
|
|
payment_identifier: "PaymentIdentifier" = None,
|
|
):
|
|
try:
|
|
d = TxDialog(
|
|
tx,
|
|
parent=parent,
|
|
prompt_if_unsaved=prompt_if_unsaved,
|
|
external_keypairs=external_keypairs,
|
|
# payment_identifier=payment_identifier,
|
|
)
|
|
d.setWindowIcon(
|
|
read_QIcon_from_bytes(self.bal_plugin.read_file("icons/bal32x32.png"))
|
|
)
|
|
except SerializationError as e:
|
|
self.logger.error("unable to deserialize the transaction")
|
|
parent.show_critical(
|
|
_("Electrum was unable to deserialize the transaction:") + "\n" + str(e)
|
|
)
|
|
else:
|
|
d.show()
|
|
return d
|
|
|
|
def show_transaction(self, tx=None, txid=None, parent=None):
|
|
if not parent:
|
|
parent = self.window
|
|
if txid != None and txid in self.willitems:
|
|
tx = self.willitems[txid].tx
|
|
if not tx:
|
|
raise Exception(_("no tx"))
|
|
return self.show_transaction_real(tx, parent=parent)
|
|
|
|
def invalidate_will(self):
|
|
def on_success(result):
|
|
if result:
|
|
self.show_message(
|
|
_(
|
|
"Please sign and broadcast this transaction to invalidate current will"
|
|
)
|
|
)
|
|
self.wallet.set_label(result.txid(), "BAL Invalidate")
|
|
a = self.show_transaction(result)
|
|
else:
|
|
self.show_message(_("No transactions to invalidate"))
|
|
|
|
def on_failure(exec_info):
|
|
self.show_error(f"ERROR:{exec_info}")
|
|
|
|
fee_per_byte = self.will_settings.get("baltx_fees", 1)
|
|
task = partial(Will.invalidate_will, self.willitems, self.wallet, fee_per_byte)
|
|
msg = _("Calculating Transactions")
|
|
self.waiting_dialog = BalWaitingDialog(
|
|
self, msg, task, on_success, on_failure, exe=False
|
|
)
|
|
self.waiting_dialog.exe()
|
|
|
|
def sign_transactions(self, password):
|
|
try:
|
|
txs = {}
|
|
signed = None
|
|
tosign = None
|
|
|
|
def get_message():
|
|
msg = ""
|
|
if signed:
|
|
msg = _(f"signed: {signed}\n")
|
|
return msg + _(f"signing: {tosign}")
|
|
|
|
for txid in Will.only_valid(self.willitems):
|
|
wi = self.willitems[txid]
|
|
tx = copy.deepcopy(wi.tx)
|
|
if wi.get_status("COMPLETE"):
|
|
txs[txid] = tx
|
|
continue
|
|
tosign = txid
|
|
try:
|
|
self.waiting_dialog.update(get_message())
|
|
except:
|
|
pass
|
|
for txin in tx.inputs():
|
|
prevout = txin.prevout.to_json()
|
|
if prevout[0] in self.willitems:
|
|
change = self.willitems[prevout[0]].tx.outputs()[prevout[1]]
|
|
txin._trusted_value_sats = change.value
|
|
try:
|
|
txin.script_descriptor = change.script_descriptor
|
|
except:
|
|
pass
|
|
txin.is_mine = True
|
|
txin._TxInput__address = change.address
|
|
txin._TxInput__scriptpubkey = change.scriptpubkey
|
|
txin._TxInput__value_sats = change.value
|
|
|
|
self.wallet.sign_transaction(tx, password, ignore_warnings=True)
|
|
signed = tosign
|
|
is_complete = False
|
|
if tx.is_complete():
|
|
is_complete = True
|
|
wi.set_status("COMPLETE", True)
|
|
txs[txid] = tx
|
|
except Exception:
|
|
return None
|
|
return txs
|
|
|
|
def get_wallet_password(self, message=None, parent=None):
|
|
parent = self.window if not parent else parent
|
|
password = None
|
|
if self.wallet.has_keystore_encryption():
|
|
password = self.bal_plugin.password_dialog(parent=parent, msg=message)
|
|
if password is None:
|
|
return False
|
|
try:
|
|
self.wallet.check_password(password)
|
|
except Exception as e:
|
|
self.show_error(str(e))
|
|
password = self.get_wallet_password(message)
|
|
return password
|
|
|
|
def on_close(self):
|
|
try:
|
|
if not self.disable_plugin:
|
|
close_window = BalBuildWillDialog(self)
|
|
close_window.build_will_task()
|
|
self.save_willitems()
|
|
self.heirs_tab.close()
|
|
self.will_tab.close()
|
|
self.tools_menu.removeAction(self.tools_menu.willexecutors_action)
|
|
self.window.toggle_tab(self.heirs_tab)
|
|
self.window.toggle_tab(self.will_tab)
|
|
self.window.tabs.update()
|
|
except Exception:
|
|
pass
|
|
|
|
def ask_password_and_sign_transactions(self, callback=None):
|
|
def on_success(txs):
|
|
if txs:
|
|
for txid, tx in txs.items():
|
|
self.willitems[txid].tx = copy.deepcopy(tx)
|
|
self.will[txid] = self.willitems[txid].to_dict()
|
|
try:
|
|
self.will_list.update()
|
|
except Exception:
|
|
pass
|
|
if callback:
|
|
try:
|
|
callback()
|
|
except Exception as e:
|
|
raise e
|
|
|
|
def on_failure(exc_info):
|
|
self.logger.info("sign fail: {}".format(exc_info))
|
|
self.show_error(exc_info)
|
|
|
|
password = self.get_wallet_password()
|
|
task = partial(self.sign_transactions, password)
|
|
msg = _("Signing transactions...")
|
|
self.waiting_dialog = BalWaitingDialog(
|
|
self, msg, task, on_success, on_failure, exe=False
|
|
)
|
|
self.waiting_dialog.exe()
|
|
|
|
def broadcast_transactions(self, force=False):
|
|
def on_success(sulcess):
|
|
self.will_list.update()
|
|
if sulcess:
|
|
self.logger.info("error, some transaction was not sent")
|
|
self.show_warning(_("Some transaction was not broadcasted"))
|
|
return
|
|
self.logger.debug("OK, sulcess transaction was sent")
|
|
self.show_message(
|
|
_("All transactions are broadcasted to respective Will-Executors")
|
|
)
|
|
|
|
def on_failure(err):
|
|
self.logger.error(err)
|
|
|
|
task = partial(self.push_transactions_to_willexecutors, force)
|
|
msg = _("Selecting Will-Executors")
|
|
self.waiting_dialog = BalWaitingDialog(
|
|
self, msg, task, on_success, on_failure, exe=False
|
|
)
|
|
self.waiting_dialog.exe()
|
|
|
|
def push_transactions_to_willexecutors(self, force=False):
|
|
willexecutors = Willexecutors.get_willexecutor_transactions(self.willitems)
|
|
|
|
def getMsg(willexecutors):
|
|
msg = "Broadcasting Transactions to Will-Executors:\n"
|
|
for url in willexecutors:
|
|
msg += f"{url}:\t{willexecutors[url]['broadcast_status']}\n"
|
|
return msg
|
|
|
|
error = False
|
|
for url in willexecutors:
|
|
willexecutor = willexecutors[url]
|
|
self.waiting_dialog.update(getMsg(willexecutors))
|
|
if "txs" in willexecutor:
|
|
try:
|
|
if Willexecutors.push_transactions_to_willexecutor(
|
|
willexecutors[url]
|
|
):
|
|
for wid in willexecutors[url]["txsids"]:
|
|
self.willitems[wid].set_status("PUSHED", True)
|
|
willexecutors[url]["broadcast_status"] = _("Success")
|
|
else:
|
|
for wid in willexecutors[url]["txsids"]:
|
|
self.willitems[wid].set_status("PUSH_FAIL", True)
|
|
error = True
|
|
willexecutors[url]["broadcast_status"] = _("Failed")
|
|
del willexecutor["txs"]
|
|
except Willexecutors.AlreadyPresentException:
|
|
for wid in willexecutor["txsids"]:
|
|
self.waiting_dialog.update(
|
|
"checking {} - {} : {}".format(
|
|
self.willitems[wid].we["url"], wid, "Waiting"
|
|
)
|
|
)
|
|
self.willitems[wid].check_willexecutor()
|
|
self.waiting_dialog.update(
|
|
"checked {} - {} : {}".format(
|
|
self.willitems[wid].we["url"],
|
|
wid,
|
|
self.willitems[wid].get_status("CHECKED"),
|
|
)
|
|
)
|
|
|
|
if error:
|
|
return True
|
|
|
|
def export_json_file(self, path):
|
|
for wid in self.willitems:
|
|
self.willitems[wid].set_status("EXPORTED", True)
|
|
self.will[wid] = self.willitems[wid].to_dict()
|
|
write_json_file(path, self.will)
|
|
|
|
def export_will(self):
|
|
try:
|
|
Util.export_meta_gui(self.window, _("will.json"), self.export_json_file)
|
|
except Exception as e:
|
|
self.show_error(str(e))
|
|
raise e
|
|
|
|
def import_will(self):
|
|
def sulcess():
|
|
self.will_list.update_will(self.willitems)
|
|
|
|
import_meta_gui(self.window, _("will"), self.import_json_file, sulcess)
|
|
|
|
def import_json_file(self, path):
|
|
try:
|
|
data = read_json_file(path)
|
|
willitems = {}
|
|
for k, v in data.items():
|
|
data[k]["tx"] = tx_from_any(v["tx"])
|
|
willitems[k] = WillItem(data[k], _id=k)
|
|
self.update_will(willitems)
|
|
except Exception as e:
|
|
raise e
|
|
raise FileImportFailed(_("Invalid will file"))
|
|
|
|
def check_transactions_task(self, will):
|
|
start = time.time()
|
|
for wid, w in will.items():
|
|
self.waiting_dialog.update(
|
|
"checking transaction: {}\n willexecutor: {}".format(wid, w.we["url"])
|
|
)
|
|
w.check_willexecutor()
|
|
|
|
if time.time() - start < 3:
|
|
time.sleep(3 - (time.time() - start))
|
|
|
|
def check_transactions(self, will):
|
|
def on_success(result):
|
|
del self.waiting_dialog
|
|
self.update_all()
|
|
pass
|
|
|
|
def on_failure(e):
|
|
self.logger.error(f"error checking transactions {e}")
|
|
pass
|
|
|
|
task = partial(self.check_transactions_task, will)
|
|
msg = _("Check Transaction")
|
|
self.waiting_dialog = BalWaitingDialog(
|
|
self, msg, task, on_success, on_failure, exe=False
|
|
)
|
|
self.waiting_dialog.exe()
|
|
|
|
def ping_willexecutors_task(self, wes):
|
|
self.logger.info("ping willexecutots task")
|
|
pinged = []
|
|
failed = []
|
|
|
|
def get_title():
|
|
msg = _("Ping Will-Executors:")
|
|
msg += "\n\n"
|
|
for url in wes:
|
|
urlstr = "{:<50}: ".format(url[:50])
|
|
if url in pinged:
|
|
urlstr += "Ok"
|
|
elif url in failed:
|
|
urlstr += "Ko"
|
|
else:
|
|
urlstr += "--"
|
|
urlstr += "\n"
|
|
msg += urlstr
|
|
|
|
return msg
|
|
|
|
for url, we in wes.items():
|
|
try:
|
|
self.waiting_dialog.update(get_title())
|
|
except Exception:
|
|
pass
|
|
wes[url] = Willexecutors.get_info_task(url, we)
|
|
if wes[url]["status"] == "KO":
|
|
failed.append(url)
|
|
else:
|
|
pinged.append(url)
|
|
|
|
def ping_willexecutors(self, wes, parent=None):
|
|
if not parent:
|
|
parent = self
|
|
|
|
def on_success(result):
|
|
#del self.waiting_dialog
|
|
try:
|
|
parent.willexecutor_list.update()
|
|
except Exception as e:
|
|
_logger.error(f"error updating willexecutors {e}")
|
|
pass
|
|
|
|
def on_failure(e):
|
|
self.logger.error(e)
|
|
pass
|
|
|
|
self.logger.info("ping willexecutors")
|
|
task = partial(self.ping_willexecutors_task, wes)
|
|
msg = _("Ping Will-Executors")
|
|
self.waiting_dialog = BalWaitingDialog(
|
|
self, msg, task, on_success, on_failure, exe=False
|
|
)
|
|
self.waiting_dialog.exe()
|
|
|
|
def preview_modal_dialog(self):
|
|
self.dw = WillDetailDialog(self)
|
|
self.dw.show()
|
|
|
|
def update_all(self):
|
|
self.will_list.update_will(self.willitems)
|
|
self.heirs_tab.update()
|
|
self.will_tab.update()
|
|
self.will_list.update()
|
|
|
|
|
|
def add_widget(grid, label, widget, row, help_):
|
|
grid.addWidget(QLabel(_(label)), row, 0)
|
|
grid.addWidget(widget, row, 1)
|
|
grid.addWidget(HelpButton(help_), row, 2)
|
|
|
|
|
|
class _LockTimeEditor:
|
|
min_allowed_value = NLOCKTIME_MIN
|
|
max_allowed_value = NLOCKTIME_MAX
|
|
|
|
def get_locktime(self) -> Optional[int]:
|
|
raise NotImplementedError()
|
|
|
|
def set_locktime(self, x: Any, force=True) -> None:
|
|
raise NotImplementedError()
|
|
|
|
def is_acceptable_locktime(cls, x: Any) -> bool:
|
|
if not x: # e.g. empty string
|
|
return True
|
|
try:
|
|
x = int(x)
|
|
except Exception:
|
|
return False
|
|
return cls.min_allowed_value <= x <= cls.max_allowed_value
|
|
|
|
|
|
class HeirsLockTimeEdit(QWidget, _LockTimeEditor):
|
|
valueEdited = pyqtSignal()
|
|
locktime_threshold = 50000000
|
|
|
|
def __init__(self, parent=None, default_index=1):
|
|
QWidget.__init__(self, parent)
|
|
|
|
hbox = QHBoxLayout()
|
|
self.setLayout(hbox)
|
|
hbox.setContentsMargins(0, 0, 0, 0)
|
|
hbox.setSpacing(0)
|
|
|
|
self.locktime_raw_e = LockTimeRawEdit(self, time_edit=self)
|
|
self.locktime_date_e = LockTimeDateEdit(self, time_edit=self)
|
|
self.editors = [self.locktime_raw_e, self.locktime_date_e]
|
|
|
|
self.combo = QComboBox()
|
|
options = [_("Raw"), _("Date")]
|
|
self.option_index_to_editor_map = {
|
|
0: self.locktime_raw_e,
|
|
1: self.locktime_date_e,
|
|
}
|
|
self.combo.addItems(options)
|
|
|
|
self.editor = self.option_index_to_editor_map[default_index]
|
|
self.combo.currentIndexChanged.connect(self.on_current_index_changed)
|
|
self.combo.setCurrentIndex(default_index)
|
|
self.on_current_index_changed(default_index)
|
|
|
|
hbox.addWidget(self.combo)
|
|
for w in self.editors:
|
|
hbox.addWidget(w)
|
|
hbox.addStretch(1)
|
|
# spacer_widget = QWidget()
|
|
# spacer_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
# hbox.addWidget(spacer_widget)
|
|
|
|
self.locktime_raw_e.editingFinished.connect(self.valueEdited.emit)
|
|
self.locktime_date_e.dateTimeChanged.connect(self.valueEdited.emit)
|
|
self.combo.currentIndexChanged.connect(self.valueEdited.emit)
|
|
|
|
def on_current_index_changed(self, i):
|
|
for w in self.editors:
|
|
w.setVisible(False)
|
|
w.setEnabled(False)
|
|
prev_locktime = self.editor.get_locktime()
|
|
self.editor = self.option_index_to_editor_map[i]
|
|
if self.editor.is_acceptable_locktime(prev_locktime):
|
|
self.editor.set_locktime(prev_locktime, force=True)
|
|
self.editor.setVisible(True)
|
|
self.editor.setEnabled(True)
|
|
|
|
def get_locktime(self) -> Optional[str]:
|
|
return self.editor.get_locktime()
|
|
|
|
def set_index(self, index):
|
|
self.combo.setCurrentIndex(index)
|
|
self.on_current_index_changed(index)
|
|
|
|
def set_locktime(self, x: Any, force=True) -> None:
|
|
self.editor.set_locktime(x, force)
|
|
|
|
|
|
class LockTimeRawEdit(QLineEdit, _LockTimeEditor):
|
|
def __init__(self, parent=None, time_edit=None):
|
|
QLineEdit.__init__(self, parent)
|
|
self.setFixedWidth(14 * char_width_in_lineedit())
|
|
self.textChanged.connect(self.numbify)
|
|
self.isdays = False
|
|
self.isyears = False
|
|
self.isblocks = False
|
|
self.time_edit = time_edit
|
|
|
|
def replace_str(self, text):
|
|
return str(text).replace("d", "").replace("y", "").replace("b", "")
|
|
|
|
def checkbdy(self, s, pos, appendix):
|
|
try:
|
|
charpos = pos - 1
|
|
charpos = max(0, charpos)
|
|
charpos = min(len(s) - 1, charpos)
|
|
if appendix == s[charpos]:
|
|
s = self.replace_str(s) + appendix
|
|
pos = charpos
|
|
except Exception:
|
|
pass
|
|
return pos, s
|
|
|
|
def numbify(self):
|
|
text = self.text().strip()
|
|
# chars = '0123456789bdy' removed the option to choose locktime by block
|
|
chars = "0123456789dy"
|
|
pos = self.cursorPosition()
|
|
pos = len("".join([i for i in text[:pos] if i in chars]))
|
|
s = "".join([i for i in text if i in chars])
|
|
self.isdays = False
|
|
self.isyears = False
|
|
self.isblocks = False
|
|
|
|
pos, s = self.checkbdy(s, pos, "d")
|
|
pos, s = self.checkbdy(s, pos, "y")
|
|
pos, s = self.checkbdy(s, pos, "b")
|
|
|
|
if "d" in s:
|
|
self.isdays = True
|
|
if "y" in s:
|
|
self.isyears = True
|
|
if "b" in s:
|
|
self.isblocks = True
|
|
|
|
if self.isdays:
|
|
s = self.replace_str(s) + "d"
|
|
if self.isyears:
|
|
s = self.replace_str(s) + "y"
|
|
if self.isblocks:
|
|
s = self.replace_str(s) + "b"
|
|
|
|
self.set_locktime(s, force=False)
|
|
# setText sets Modified to False. Instead we want to remember
|
|
# if updates were because of user modification.
|
|
self.setModified(self.hasFocus())
|
|
self.setCursorPosition(pos)
|
|
|
|
def get_locktime(self) -> Optional[str]:
|
|
try:
|
|
return str(self.text())
|
|
except Exception:
|
|
return None
|
|
|
|
def set_locktime(self, x: Any, force=True) -> None:
|
|
out = str(x)
|
|
if "d" in out:
|
|
out = self.replace_str(x) + "d"
|
|
elif "y" in out:
|
|
out = self.replace_str(x) + "y"
|
|
elif "b" in out:
|
|
out = self.replace_str(x) + "b"
|
|
else:
|
|
try:
|
|
out = int(x)
|
|
except Exception:
|
|
self.setText("")
|
|
return
|
|
out = max(out, self.min_allowed_value)
|
|
out = min(out, self.max_allowed_value)
|
|
self.setText(str(out))
|
|
|
|
|
|
class LockTimeHeightEdit(LockTimeRawEdit):
|
|
max_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX
|
|
|
|
def __init__(self, parent=None, time_edit=None):
|
|
LockTimeRawEdit.__init__(self, parent)
|
|
self.setFixedWidth(20 * char_width_in_lineedit())
|
|
self.time_edit = time_edit
|
|
|
|
def paintEvent(self, event):
|
|
super().paintEvent(event)
|
|
panel = QStyleOptionFrame()
|
|
self.initStyleOption(panel)
|
|
textRect = self.style().subElementRect(QStyle.SE_LineEditContents, panel, self)
|
|
textRect.adjust(2, 0, -10, 0)
|
|
painter = QPainter(self)
|
|
painter.setPen(ColorScheme.GRAY.as_color())
|
|
painter.drawText(textRect, int(Qt.AlignRight | Qt.AlignVCenter), "height")
|
|
|
|
|
|
def get_max_allowed_timestamp() -> int:
|
|
ts = NLOCKTIME_MAX
|
|
# Test if this value is within the valid timestamp limits (which is platform-dependent).
|
|
# see #6170
|
|
try:
|
|
datetime.fromtimestamp(ts)
|
|
except (OSError, OverflowError):
|
|
ts = 2**31 - 1 # INT32_MAX
|
|
datetime.fromtimestamp(ts) # test if raises
|
|
return ts
|
|
|
|
|
|
class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
|
|
min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1
|
|
max_allowed_value = get_max_allowed_timestamp()
|
|
|
|
def __init__(self, parent=None, time_edit=None):
|
|
QDateTimeEdit.__init__(self, parent)
|
|
self.setMinimumDateTime(datetime.fromtimestamp(self.min_allowed_value))
|
|
self.setMaximumDateTime(datetime.fromtimestamp(self.max_allowed_value))
|
|
self.setDateTime(QDateTime.currentDateTime())
|
|
self.time_edit = time_edit
|
|
|
|
def get_locktime(self) -> Optional[int]:
|
|
dt = self.dateTime().toPyDateTime()
|
|
locktime = int(time.mktime(dt.timetuple()))
|
|
return locktime
|
|
|
|
def set_locktime(self, x: Any, force=False) -> None:
|
|
if not self.is_acceptable_locktime(x):
|
|
self.setDateTime(QDateTime.currentDateTime())
|
|
return
|
|
try:
|
|
x = int(x)
|
|
except Exception:
|
|
self.setDateTime(QDateTime.currentDateTime())
|
|
return
|
|
dt = datetime.fromtimestamp(x)
|
|
self.setDateTime(dt)
|
|
|
|
|
|
_NOT_GIVEN = object() # sentinel value
|
|
|
|
|
|
class PercAmountEdit(BTCAmountEdit):
|
|
def __init__(
|
|
self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN
|
|
):
|
|
super().__init__(decimal_point, is_int, parent, max_amount=max_amount)
|
|
|
|
def numbify(self):
|
|
text = self.text().strip()
|
|
if text == "!":
|
|
self.shortcut.emit()
|
|
return
|
|
pos = self.cursorPosition()
|
|
chars = "0123456789%"
|
|
chars += DECIMAL_POINT
|
|
|
|
s = "".join([i for i in text if i in chars])
|
|
|
|
if "%" in s:
|
|
self.is_perc = True
|
|
s = s.replace("%", "")
|
|
else:
|
|
self.is_perc = False
|
|
|
|
if DECIMAL_POINT in s:
|
|
p = s.find(DECIMAL_POINT)
|
|
s = s.replace(DECIMAL_POINT, "")
|
|
s = s[:p] + DECIMAL_POINT + s[p : p + 8]
|
|
if self.is_perc:
|
|
s += "%"
|
|
|
|
self.setText(s)
|
|
self.setModified(self.hasFocus())
|
|
self.setCursorPosition(pos)
|
|
|
|
def _get_amount_from_text(self, text: str) -> Union[None, Decimal, int]:
|
|
try:
|
|
text = text.replace(DECIMAL_POINT, ".")
|
|
text = text.replace("%", "")
|
|
return (Decimal)(text)
|
|
except Exception:
|
|
return None
|
|
|
|
def _get_text_from_amount(self, amount):
|
|
out = super()._get_text_from_amount(amount)
|
|
if self.is_perc:
|
|
out += "%"
|
|
return out
|
|
|
|
def paintEvent(self, event):
|
|
QLineEdit.paintEvent(self, event)
|
|
if self.base_unit:
|
|
panel = QStyleOptionFrame()
|
|
self.initStyleOption(panel)
|
|
textRect = self.style().subElementRect(
|
|
QStyle.SubElement.SE_LineEditContents, panel, self
|
|
)
|
|
textRect.adjust(2, 0, -10, 0)
|
|
painter = QPainter(self)
|
|
painter.setPen(ColorScheme.GRAY.as_color())
|
|
if len(self.text()) == 0:
|
|
painter.drawText(
|
|
textRect,
|
|
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
|
|
self.base_unit() + " or perc value",
|
|
)
|
|
|
|
|
|
class BalDialog(WindowModalDialog):
|
|
def __init__(self, parent, bal_plugin, title=None, icon="icons/bal32x32.png"):
|
|
self.parent = parent
|
|
WindowModalDialog.__init__(self, parent, title)
|
|
# WindowModalDialog.__init__(self,parent)
|
|
self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))
|
|
|
|
|
|
class BalWizardDialog(BalDialog):
|
|
def __init__(self, bal_window: "BalWindow"):
|
|
assert bal_window
|
|
BalDialog.__init__(
|
|
self, bal_window.window, bal_window.bal_plugin, _("Bal Wizard Setup")
|
|
)
|
|
self.setMinimumSize(800, 400)
|
|
self.bal_window = bal_window
|
|
self.parent = bal_window.window
|
|
self.layout = QVBoxLayout(self)
|
|
self.widget = BalWizardHeirsWidget(
|
|
bal_window, self, self.on_next_heir, None, self.on_cancel_heir
|
|
)
|
|
self.layout.addWidget(self.widget)
|
|
|
|
def next_widget(self, widget):
|
|
self.layout.removeWidget(self.widget)
|
|
self.widget.close()
|
|
self.widget = widget
|
|
self.layout.addWidget(self.widget)
|
|
# self.update()
|
|
# self.repaint()
|
|
|
|
def on_next_heir(self):
|
|
self.next_widget(
|
|
BalWizardLocktimeAndFeeWidget(
|
|
self.bal_window,
|
|
self,
|
|
self.on_next_locktimeandfee,
|
|
self.on_previous_heir,
|
|
self.on_cancel_heir,
|
|
)
|
|
)
|
|
|
|
def on_previous_heir(self):
|
|
self.next_widget(
|
|
BalWizardHeirsWidget(
|
|
self.bal_window, self, self.on_next_heir, None, self.on_cancel_heir
|
|
)
|
|
)
|
|
|
|
def on_cancel_heir(self):
|
|
pass
|
|
|
|
def on_next_wedonwload(self):
|
|
self.next_widget(
|
|
BalWizardWEWidget(
|
|
self.bal_window,
|
|
self,
|
|
self.on_next_we,
|
|
self.on_next_locktimeandfee,
|
|
self.on_cancel_heir,
|
|
)
|
|
)
|
|
|
|
def on_next_we(self):
|
|
close_window = BalBuildWillDialog(self.bal_window)
|
|
close_window.build_will_task()
|
|
self.close()
|
|
# self.next_widget(BalWizardLocktimeAndFeeWidget(self.bal_window,self,self.on_next_locktimeandfee,self.on_next_wedonwload,self.on_next_wedonwload.on_cancel_heir))
|
|
|
|
def on_next_locktimeandfee(self):
|
|
self.next_widget(
|
|
BalWizardWEDownloadWidget(
|
|
self.bal_window,
|
|
self,
|
|
self.on_next_wedonwload,
|
|
self.on_next_heir,
|
|
self.on_cancel_heir,
|
|
)
|
|
)
|
|
|
|
def on_accept(self):
|
|
pass
|
|
|
|
def on_reject(self):
|
|
pass
|
|
|
|
def on_close(self):
|
|
pass
|
|
|
|
def closeEvent(self, event):
|
|
self.bal_window.update_all()
|
|
self.bal_window.heir_list.update_will_settings()
|
|
pass
|
|
|
|
|
|
class BalWizardWidget(QWidget):
|
|
title = None
|
|
message = None
|
|
|
|
def __init__(
|
|
self, bal_window: "BalWindow", parent, on_next, on_previous, on_cancel
|
|
):
|
|
QWidget.__init__(self, parent)
|
|
self.vbox = QVBoxLayout(self)
|
|
self.bal_window = bal_window
|
|
self.parent = parent
|
|
self.on_next = on_next
|
|
self.on_cancel = on_cancel
|
|
self.titleLabel = QLabel(self.title)
|
|
self.vbox.addWidget(self.titleLabel)
|
|
self.messageLabel = QLabel(_(self.message))
|
|
self.vbox.addWidget(self.messageLabel)
|
|
|
|
self.content = self.get_content()
|
|
self.content_container = QWidget()
|
|
self.containrelayout = QVBoxLayout(self.content_container)
|
|
self.containrelayout.addWidget(self.content)
|
|
|
|
self.vbox.addWidget(self.content_container)
|
|
|
|
spacer_widget = QWidget()
|
|
spacer_widget.setSizePolicy(
|
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
)
|
|
self.vbox.addWidget(spacer_widget)
|
|
|
|
self.buttons = []
|
|
if on_previous:
|
|
self.on_previous = on_previous
|
|
self.previous_button = QPushButton(_("Previous"))
|
|
self.previous_button.clicked.connect(self._on_previous)
|
|
self.buttons.append(self.previous_button)
|
|
|
|
self.next_button = QPushButton(_("Next"))
|
|
self.next_button.clicked.connect(self._on_next)
|
|
self.buttons.append(self.next_button)
|
|
|
|
self.abort_button = QPushButton(_("Cancel"))
|
|
self.abort_button.clicked.connect(self._on_cancel)
|
|
self.buttons.append(self.abort_button)
|
|
|
|
self.vbox.addLayout(Buttons(*self.buttons))
|
|
|
|
def _on_cancel(self):
|
|
self.on_cancel()
|
|
self.parent.close()
|
|
|
|
def _on_next(self):
|
|
if self.validate():
|
|
self.on_next()
|
|
|
|
def _on_previous(self):
|
|
self.on_previous()
|
|
|
|
def get_content(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
return True
|
|
|
|
|
|
class BalWizardHeirsWidget(BalWizardWidget):
|
|
title = "Bitcoin After Life Heirs"
|
|
message = (
|
|
"Please add your heirs\n remember that 100% of wallet balance will be spent"
|
|
)
|
|
|
|
def get_content(self):
|
|
self.heirs_list = HeirList(self.bal_window, self.parent)
|
|
button_add = QPushButton(_("Add"))
|
|
button_add.clicked.connect(self.add_heir)
|
|
button_import = QPushButton(_("Import"))
|
|
button_import.clicked.connect(self.import_from_file)
|
|
button_export = QPushButton(_("Export"))
|
|
button_export.clicked.connect(self.export_to_file)
|
|
widget = QWidget()
|
|
vbox = QVBoxLayout(widget)
|
|
vbox.addWidget(self.heirs_list)
|
|
vbox.addLayout(Buttons(button_add, button_import, button_export))
|
|
return widget
|
|
|
|
def import_from_file(self):
|
|
self.bal_window.import_heirs()
|
|
self.heirs_list.update()
|
|
|
|
def export_to_file(self):
|
|
self.bal_window.export_heirs()
|
|
|
|
def add_heir(self):
|
|
self.bal_window.new_heir_dialog()
|
|
self.heirs_list.update()
|
|
|
|
def validate(self):
|
|
return True
|
|
|
|
|
|
class BalWizardWEDownloadWidget(BalWizardWidget):
|
|
title = _("Bitcoin After Life Will-Executors")
|
|
message = _("Choose willexecutors download method")
|
|
|
|
def get_content(self):
|
|
# question = QLabel()
|
|
self.combo = QComboBox()
|
|
self.combo.addItems(
|
|
[
|
|
"Automatically download and select willexecutors",
|
|
"Only download willexecutors list",
|
|
"Import willexecutor list from file",
|
|
"Manual",
|
|
]
|
|
)
|
|
# heir_name.setFixedWidth(32 * char_width_in_lineedit())
|
|
return self.combo
|
|
|
|
def validate(self):
|
|
return True
|
|
|
|
def _on_next(self):
|
|
|
|
index = self.combo.currentIndex()
|
|
_logger.debug(f"selected index:{index}")
|
|
if index < 3:
|
|
self.bal_window.willexecutors = Willexecutors.get_willexecutors(
|
|
self.bal_window.bal_plugin
|
|
)
|
|
|
|
if index == 2:
|
|
|
|
def doNothing():
|
|
self.bal_window.willexecutors.update(self.willexecutors)
|
|
Willexecutors.save(
|
|
self.bal_window.bal_plugin, self.bal_window.willexecutors
|
|
)
|
|
pass
|
|
|
|
import_meta_gui(
|
|
self.bal_window.window,
|
|
_("willexecutors.json"),
|
|
self.import_json_file,
|
|
doNothing,
|
|
)
|
|
|
|
if index < 2:
|
|
|
|
def on_success(willexecutors):
|
|
self.bal_window.willexecutors.update(willexecutors)
|
|
self.bal_window.ping_willexecutors(self.bal_window.willexecutors,False)
|
|
if index < 1:
|
|
for we in self.bal_window.willexecutors:
|
|
if self.bal_window.willexecutors[we]["status"] == 200:
|
|
self.bal_window.willexecutors[we]["selected"] = True
|
|
Willexecutors.save(
|
|
self.bal_window.bal_plugin, self.bal_window.willexecutors
|
|
)
|
|
|
|
def on_failure(fail):
|
|
_logger.debug(f"Failed to download willexecutors list {fail}")
|
|
pass
|
|
|
|
task = partial(Willexecutors.download_list, self.bal_window.bal_plugin)
|
|
msg = _("Downloading Will-Executors list")
|
|
self.waiting_dialog = BalWaitingDialog(
|
|
self.bal_window, msg, task, on_success, on_failure, exe=False
|
|
)
|
|
self.waiting_dialog.exe()
|
|
|
|
elif index == 3:
|
|
# TODO DO NOTHING
|
|
pass
|
|
|
|
if self.validate():
|
|
return self.on_next()
|
|
|
|
def import_json_file(self, path):
|
|
data = read_json_file(path)
|
|
data = self._validate(data)
|
|
self.willexecutors = data
|
|
|
|
def _validate(self, data):
|
|
return data
|
|
|
|
|
|
class BalWizardWEWidget(BalWizardWidget):
|
|
title = "Bitcoin After Life Will-Executors"
|
|
message = _("Configure and select your willexecutors")
|
|
|
|
def get_content(self):
|
|
widget = QWidget()
|
|
vbox = QVBoxLayout(widget)
|
|
vbox.addWidget(
|
|
WillExecutorWidget(
|
|
self,
|
|
self.bal_window,
|
|
Willexecutors.get_willexecutors(self.bal_window.bal_plugin),
|
|
)
|
|
)
|
|
return widget
|
|
|
|
|
|
class BalWizardLocktimeAndFeeWidget(BalWizardWidget):
|
|
title = "Bitcoin After Life Will Settings"
|
|
message = _("")
|
|
|
|
def get_content(self):
|
|
widget = QWidget()
|
|
self.heir_locktime = HeirsLockTimeEdit(widget, 0)
|
|
will_settings = self.bal_window.bal_plugin.WILL_SETTINGS.get()
|
|
self.heir_locktime.set_locktime(will_settings["locktime"])
|
|
|
|
def on_heir_locktime():
|
|
if not self.heir_locktime.get_locktime():
|
|
self.heir_locktime.set_locktime("1y")
|
|
self.bal_window.will_settings["locktime"] = (
|
|
self.heir_locktime.get_locktime()
|
|
if self.heir_locktime.get_locktime()
|
|
else "1y"
|
|
)
|
|
self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings)
|
|
|
|
self.heir_locktime.valueEdited.connect(on_heir_locktime)
|
|
|
|
self.heir_threshold = HeirsLockTimeEdit(widget, 0)
|
|
self.heir_threshold.set_locktime(will_settings["threshold"])
|
|
|
|
def on_heir_threshold():
|
|
if not self.heir_threshold.get_locktime():
|
|
self.heir_threshold.set_locktime("180d")
|
|
|
|
self.bal_window.will_settings["threshold"] = (
|
|
self.heir_threshold.get_locktime()
|
|
)
|
|
self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings)
|
|
|
|
self.heir_threshold.valueEdited.connect(on_heir_threshold)
|
|
|
|
self.heir_tx_fees = QSpinBox(widget)
|
|
|
|
self.heir_tx_fees.setMinimum(1)
|
|
self.heir_tx_fees.setMaximum(10000)
|
|
self.heir_tx_fees.setValue(will_settings["baltx_fees"])
|
|
|
|
def on_heir_tx_fees():
|
|
if not self.heir_tx_fees.value():
|
|
self.heir_tx_fees.set_value(1)
|
|
self.bal_window.will_settings["baltx_fees"] = self.heir_tx_fees.value()
|
|
self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings)
|
|
|
|
self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees)
|
|
|
|
def make_hlayout(label, twidget, help_text):
|
|
tw = QWidget()
|
|
hlayout = QHBoxLayout(tw)
|
|
hlayout.addWidget(QLabel(label))
|
|
hlayout.addWidget(twidget)
|
|
hlayout.addWidget(HelpButton(help_text))
|
|
hlayout.addStretch(1)
|
|
spacer_widget = QWidget()
|
|
spacer_widget.setSizePolicy(
|
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
)
|
|
hlayout.addWidget(spacer_widget)
|
|
return tw
|
|
|
|
layout = QVBoxLayout(widget)
|
|
|
|
layout.addWidget(
|
|
make_hlayout(
|
|
_("Delivery Time:"),
|
|
self.heir_locktime,
|
|
_(
|
|
"Locktime* to be used in the transaction\n"
|
|
+ "if you choose Raw, you can insert various options based on suffix:\n"
|
|
+ " - d: number of days after current day(ex: 1d means tomorrow)\n"
|
|
+ " - y: number of years after currrent day(ex: 1y means one year from today)\n"
|
|
+ "* locktime can be anticipated to update will\n"
|
|
),
|
|
)
|
|
)
|
|
layout.addWidget(
|
|
make_hlayout(
|
|
_("Check Alive:"),
|
|
self.heir_threshold,
|
|
_(
|
|
"Check to ask for invalidation.\n"
|
|
+ "When less then this time is missing, ask to invalidate.\n"
|
|
+ "If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n"
|
|
+ "if you choose Raw, you can insert various options based on suffix:\n"
|
|
+ " - d: number of days after current day(ex: 1d means tomorrow).\n"
|
|
+ " - y: number of years after currrent day(ex: 1y means one year from today).\n\n"
|
|
),
|
|
)
|
|
)
|
|
layout.addWidget(
|
|
make_hlayout(
|
|
_("Fees(sats/vbyte):"),
|
|
self.heir_tx_fees,
|
|
("Fee to be used in the transaction"),
|
|
)
|
|
)
|
|
|
|
spacer_widget = QWidget()
|
|
spacer_widget.setSizePolicy(
|
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
)
|
|
layout.addWidget(spacer_widget)
|
|
return widget
|
|
|
|
|
|
class BalWaitingDialog(BalDialog):
|
|
updatemessage = pyqtSignal([str], arguments=["message"])
|
|
|
|
def __init__(
|
|
self,
|
|
bal_window: "BalWindow",
|
|
message: str,
|
|
task,
|
|
on_success=None,
|
|
on_error=None,
|
|
on_cancel=None,
|
|
exe=True,
|
|
):
|
|
assert bal_window
|
|
BalDialog.__init__(
|
|
self, bal_window.window, bal_window.bal_plugin, _("Please wait")
|
|
)
|
|
self.message_label = QLabel(message)
|
|
vbox = QVBoxLayout(self)
|
|
vbox.addWidget(self.message_label)
|
|
self.updatemessage.connect(self.update_message)
|
|
if on_cancel:
|
|
self.cancel_button = CancelButton(self)
|
|
self.cancel_button.clicked.connect(on_cancel)
|
|
vbox.addLayout(Buttons(self.cancel_button))
|
|
self.accepted.connect(self.on_accepted)
|
|
self.task = task
|
|
self.on_success = on_success
|
|
self.on_error = on_error
|
|
self.on_cancel = on_cancel
|
|
if exe:
|
|
self.exe()
|
|
|
|
def exe(self):
|
|
self.thread = TaskThread(self)
|
|
self.thread.finished.connect(self.deleteLater) # see #3956
|
|
self.thread.finished.connect(self.finished)
|
|
self.thread.add(self.task, self.on_success, self.accept, self.on_error)
|
|
self.exec()
|
|
|
|
def hello(self):
|
|
pass
|
|
|
|
def finished(self):
|
|
_logger.info("finished")
|
|
|
|
def wait(self):
|
|
self.thread.wait()
|
|
|
|
def on_accepted(self):
|
|
self.thread.stop()
|
|
|
|
def update_message(self, msg):
|
|
self.message_label.setText(msg)
|
|
|
|
def update(self, msg):
|
|
self.updatemessage.emit(msg)
|
|
|
|
def getText(self):
|
|
return self.message_label.text()
|
|
|
|
def closeEvent(self, event):
|
|
self.thread.stop()
|
|
|
|
|
|
class BalBlockingWaitingDialog(BalDialog):
|
|
def __init__(self, bal_window: "BalWindow", message: str, task: Callable[[], Any]):
|
|
BalDialog.__init__(self, bal_window, bal_window.bal_plugin, _("Please wait"))
|
|
self.message_label = QLabel(message)
|
|
vbox = QVBoxLayout(self)
|
|
vbox.addWidget(self.message_label)
|
|
self.finished.connect(self.deleteLater) # see #3956
|
|
# show popup
|
|
self.show()
|
|
# refresh GUI; needed for popup to appear and for message_label to get drawn
|
|
# QCoreApplication.processEvents()
|
|
# QCoreApplication.processEvents()
|
|
try:
|
|
# block and run given task
|
|
task()
|
|
finally:
|
|
# close popup
|
|
self.accept()
|
|
|
|
|
|
class bal_checkbox(QCheckBox):
|
|
def __init__(self, variable, on_click=None):
|
|
QCheckBox.__init__(self)
|
|
self.setChecked(variable.get())
|
|
self.on_click = on_click
|
|
|
|
def on_check(v):
|
|
variable.set(v == 2)
|
|
variable.get()
|
|
if self.on_click:
|
|
self.on_click()
|
|
|
|
self.stateChanged.connect(on_check)
|
|
|
|
|
|
class BalBuildWillDialog(BalDialog):
|
|
updatemessage = pyqtSignal()
|
|
|
|
def __init__(self, bal_window, parent=None):
|
|
if not parent:
|
|
parent = bal_window.window
|
|
BalDialog.__init__(self, parent, bal_window.bal_plugin, "Building Will")
|
|
self.parent=parent
|
|
self.updatemessage.connect(self.update)
|
|
self.bal_window = bal_window
|
|
self.message_label = QLabel("Building Will:")
|
|
self.vbox = QVBoxLayout(self)
|
|
self.vbox.addWidget(self.message_label)
|
|
self.qwidget = QWidget()
|
|
self.vbox.addWidget(self.qwidget)
|
|
self.labels = []
|
|
self.check_row = None
|
|
self.inval_row = None
|
|
self.build_row = None
|
|
self.sign_row = None
|
|
self.push_row = None
|
|
self.network = Network.get_instance()
|
|
self._stopping = False
|
|
self.thread = TaskThread(self)
|
|
self.thread.finished.connect(self.task_finished) # see #3956
|
|
|
|
def task_finished(self):
|
|
pass
|
|
|
|
def build_will_task(self):
|
|
_logger.debug("build will task to be started")
|
|
self.thread.add(
|
|
self.task_phase1,
|
|
on_success=self.on_success_phase1,
|
|
on_done=self.on_accept,
|
|
on_error=self.on_error_phase1,
|
|
)
|
|
self.show()
|
|
self.exec()
|
|
|
|
def task_phase1(self):
|
|
_logger.debug("close plugin phase 1 started")
|
|
try:
|
|
self.bal_window.init_class_variables()
|
|
except NoHeirsException:
|
|
_logger.error("no heirs exception")
|
|
return False, None
|
|
varrow = self.msg_set_status("checking variables")
|
|
try:
|
|
_logger.debug("checking variables")
|
|
Will.check_amounts(
|
|
self.bal_window.heirs,
|
|
self.bal_window.willexecutors,
|
|
self.bal_window.window.wallet.get_utxos(),
|
|
self.bal_window.date_to_check,
|
|
self.bal_window.window.wallet.dust_threshold(),
|
|
)
|
|
_logger.debug("variables ok")
|
|
self.msg_set_status("checking variables:", varrow,"Ok")
|
|
except AmountException:
|
|
self.msg_set_status(
|
|
"checking variables",
|
|
varrow,
|
|
'<font color="#ff0000">'
|
|
+ _(
|
|
"In the inheritance process, "
|
|
+ "the entire wallet will always be fully emptied. \n"
|
|
+ "Your settings require an adjustment of the amounts"
|
|
)
|
|
+ "</font>",
|
|
)
|
|
|
|
self.msg_set_checking()
|
|
have_to_build = False
|
|
try:
|
|
self.bal_window.check_will()
|
|
self.msg_set_checking("Ok")
|
|
except WillExpiredException:
|
|
_logger.debug("expired")
|
|
self.msg_set_checking("Expired")
|
|
fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
|
|
return None, Will.invalidate_will(
|
|
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
|
|
)
|
|
except NoHeirsException:
|
|
_logger.debug("no heirs")
|
|
self.msg_set_checking("No Heirs")
|
|
except NotCompleteWillException as e:
|
|
_logger.debug("not complete", e)
|
|
message = False
|
|
have_to_build = True
|
|
if isinstance(e, HeirChangeException):
|
|
message = "Heirs changed:"
|
|
elif isinstance(e, WillExecutorNotPresent):
|
|
message = "Will-Executor not present"
|
|
elif isinstance(e, WillexecutorChangeException):
|
|
message = "Will-Executor changed"
|
|
elif isinstance(e, TxFeesChangedException):
|
|
message = "Txfees are changed"
|
|
elif isinstance(e, HeirNotFoundException):
|
|
message = "Heir not found"
|
|
if message:
|
|
_logger.debug("message")
|
|
self.msg_set_checking(message)
|
|
else:
|
|
self.msg_set_checking("New")
|
|
|
|
if have_to_build:
|
|
self.msg_set_building()
|
|
try:
|
|
self.bal_window.build_will()
|
|
self.bal_window.check_will()
|
|
for wid in Will.only_valid(self.bal_window.willitems):
|
|
self.bal_window.wallet.set_label(wid, "BAL Transaction")
|
|
self.msg_set_building("Ok")
|
|
except Exception as e:
|
|
self.msg_set_building(self.msg_error(e))
|
|
return False, None
|
|
have_to_sign = False
|
|
for wid in Will.only_valid(self.bal_window.willitems):
|
|
if not self.bal_window.willitems[wid].get_status("COMPLETE"):
|
|
have_to_sign = True
|
|
break
|
|
return have_to_sign, None
|
|
|
|
def on_accept(self):
|
|
pass
|
|
|
|
def on_accept_phase2(self):
|
|
pass
|
|
|
|
def on_error_push(self):
|
|
pass
|
|
|
|
def wait(self, secs):
|
|
wait_row = None
|
|
for i in range(secs, 0, -1):
|
|
if self._stopping:
|
|
return
|
|
wait_row = self.msg_edit_row(f"Please wait {i}secs", wait_row)
|
|
time.sleep(1)
|
|
self.msg_del_row(wait_row)
|
|
|
|
def loop_broadcast_invalidating(self, tx):
|
|
self.msg_set_invalidating("Broadcasting")
|
|
try:
|
|
tx.add_info_from_wallet(self.bal_window.wallet)
|
|
self.network.run_from_another_thread(tx.add_info_from_network(self.network))
|
|
txid = self.network.run_from_another_thread(
|
|
self.network.broadcast_transaction(tx, timeout=120), timeout=120
|
|
)
|
|
self.msg_set_invalidating("Ok")
|
|
if not txid:
|
|
_logger.debug(f"should not be none txid: {txid}")
|
|
|
|
except TxBroadcastError as e:
|
|
_logger.error(e)
|
|
msg = e.get_message_for_gui()
|
|
self.msg_set_invalidating(self.msg_error(msg))
|
|
except BestEffortRequestFailed as e:
|
|
self.msg_set_invalidating(self.msg_error(e))
|
|
|
|
def loop_push(self):
|
|
self.msg_set_pushing("Broadcasting")
|
|
retry = False
|
|
try:
|
|
willexecutors = Willexecutors.get_willexecutor_transactions(
|
|
self.bal_window.willitems
|
|
)
|
|
for url, willexecutor in willexecutors.items():
|
|
try:
|
|
if Willexecutors.is_selected(
|
|
self.bal_window.willexecutors.get(url)
|
|
):
|
|
_logger.debug(f"{url}: {willexecutor}")
|
|
if not Willexecutors.push_transactions_to_willexecutor(
|
|
willexecutor
|
|
):
|
|
for wid in willexecutor["txsids"]:
|
|
self.bal_window.willitems[wid].set_status(
|
|
"PUSH_FAIL", True
|
|
)
|
|
retry = True
|
|
else:
|
|
for wid in willexecutor["txsids"]:
|
|
self.bal_window.willitems[wid].set_status(
|
|
"PUSHED", True
|
|
)
|
|
except Willexecutors.AlreadyPresentException:
|
|
for wid in willexecutor["txsids"]:
|
|
row = self.msg_edit_row(
|
|
"checking {} - {} : {}".format(
|
|
self.bal_window.willitems[wid].we["url"], wid, "Waiting"
|
|
)
|
|
)
|
|
self.bal_window.willitems[wid].check_willexecutor()
|
|
row = self.msg_edit_row(
|
|
"checked {} - {} : {}".format(
|
|
self.bal_window.willitems[wid].we["url"],
|
|
wid,
|
|
self.bal_window.willitems[wid].get_status("CHECKED"),
|
|
),
|
|
row,
|
|
)
|
|
|
|
except Exception as e:
|
|
|
|
_logger.error(e)
|
|
raise e
|
|
if retry:
|
|
raise Exception("retry")
|
|
|
|
except Exception as e:
|
|
self.msg_set_pushing(self.msg_error(e))
|
|
self.wait(10)
|
|
if not self._stopping:
|
|
self.loop_push()
|
|
|
|
def invalidate_task(self,password,bal_window,tx):
|
|
_logger.debug(f"invalidate tx: {tx}")
|
|
fee_per_byte = bal_window.will_settings.get("baltx_fees", 1)
|
|
tx = self.bal_window.wallet.sign_transaction(tx, password)
|
|
try:
|
|
if tx:
|
|
if tx.is_complete():
|
|
self.loop_broadcast_invalidating(tx)
|
|
self.wait(5)
|
|
else:
|
|
raise Exception("tx not complete")
|
|
else:
|
|
raise Exception("not tx")
|
|
except Exception as e:
|
|
(f"exception:{e}")
|
|
self.msg_set_invalidating(f"Error: {e}")
|
|
raise Exception("Impossible to sign")
|
|
|
|
def on_success_invalidate(self, success):
|
|
self.thread.add(
|
|
self.task_phase1,
|
|
on_success=self.on_success_phase1,
|
|
on_done=self.on_accept,
|
|
on_error=self.on_error_phase1,
|
|
)
|
|
|
|
def on_error(self, error):
|
|
_logger.error(error)
|
|
pass
|
|
def on_success_phase1(self, result):
|
|
self.have_to_sign, tx = list(result)
|
|
_logger.debug("have to sign {}".format(self.have_to_sign))
|
|
password = None
|
|
if self.have_to_sign is None:
|
|
_logger.debug("have to invalidate")
|
|
self.msg_set_invalidating()
|
|
# need to sign invalidate and restart phase 1
|
|
|
|
password = self.bal_window.get_wallet_password(
|
|
"Invalidate your old will", parent=self
|
|
)
|
|
if password is False:
|
|
self.msg_set_invalidating("Aborted")
|
|
self.wait(3)
|
|
self.close()
|
|
return
|
|
self.thread.add(
|
|
partial(self.invalidate_task,password,self.bal_window,tx),
|
|
on_success=self.on_success_invalidate,
|
|
on_done=self.on_accept,
|
|
on_error=self.on_error,
|
|
)
|
|
|
|
return
|
|
|
|
elif self.have_to_sign:
|
|
password = self.bal_window.get_wallet_password(
|
|
"Sign your will", parent=self
|
|
)
|
|
if password is False:
|
|
self.msg_set_signing("Aborted")
|
|
else:
|
|
self.msg_set_signing("Nothing to do")
|
|
self.thread.add(
|
|
partial(self.task_phase2, password),
|
|
on_success=self.on_success_phase2,
|
|
on_done=self.on_accept_phase2,
|
|
on_error=self.on_error_phase2,
|
|
)
|
|
return
|
|
|
|
def on_success_phase2(self, arg=False):
|
|
self.thread.stop()
|
|
self.bal_window.save_willitems()
|
|
self.msg_edit_row("Finished")
|
|
self.close()
|
|
|
|
def closeEvent(self, event):
|
|
self.bal_window.update_all()
|
|
self._stopping = True
|
|
self.thread.stop()
|
|
|
|
def task_phase2(self, password):
|
|
if self.have_to_sign:
|
|
try:
|
|
if txs := self.bal_window.sign_transactions(password):
|
|
for txid, tx in txs.items():
|
|
self.bal_window.willitems[txid].tx = copy.deepcopy(tx)
|
|
self.bal_window.save_willitems()
|
|
self.msg_set_signing("Ok")
|
|
except Exception as e:
|
|
self.msg_set_signing(self.msg_error(e))
|
|
|
|
self.msg_set_pushing()
|
|
have_to_push = False
|
|
for wid in Will.only_valid(self.bal_window.willitems):
|
|
w = self.bal_window.willitems[wid]
|
|
if w.we and w.get_status("COMPLETE") and not w.get_status("PUSHED"):
|
|
have_to_push = True
|
|
if not have_to_push:
|
|
self.msg_set_pushing("Nothing to do")
|
|
else:
|
|
try:
|
|
self.loop_push()
|
|
self.msg_set_pushing("Ok")
|
|
|
|
except Exception as e:
|
|
self.msg_set_pushing(self.msg_error(e))
|
|
self.msg_edit_row("Ok")
|
|
self.wait(5)
|
|
|
|
def on_error_phase1(self, error):
|
|
_logger.error(f"error phase1: {error}")
|
|
|
|
def on_error_phase2(self, error):
|
|
_logger.error("error phase2: { error}")
|
|
|
|
def msg_set_checking(self, status="Waiting", row=None):
|
|
row = self.check_row if row is None else row
|
|
self.check_row = self.msg_set_status("Checking your will", row, status)
|
|
|
|
def msg_set_invalidating(self, status=None, row=None):
|
|
row = self.inval_row if row is None else row
|
|
self.inval_row = self.msg_set_status(
|
|
"Invalidating old will", self.inval_row, status
|
|
)
|
|
|
|
def msg_set_building(self, status=None, row=None):
|
|
row = self.build_row if row is None else row
|
|
self.build_row = self.msg_set_status(
|
|
"Building your will", self.build_row, status
|
|
)
|
|
|
|
def msg_set_signing(self, status=None, row=None):
|
|
row = self.sign_row if row is None else row
|
|
self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status)
|
|
|
|
def msg_set_pushing(self, status=None, row=None):
|
|
row = self.push_row if row is None else row
|
|
self.push_row = self.msg_set_status(
|
|
"Broadcasting your will to executors", self.push_row, status
|
|
)
|
|
|
|
def msg_set_waiting(self, status=None, row=None):
|
|
row = self.wait_row if row is None else row
|
|
self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row)
|
|
|
|
def msg_error(self, e):
|
|
return "Error: {}".format(e)
|
|
|
|
def msg_set_status(self, msg, row=None, status=None):
|
|
status = "Wait" if status is None else status
|
|
line = "{}:\t{}".format(_(msg), status)
|
|
return self.msg_edit_row(line, row)
|
|
|
|
def ask_password(self, msg=None):
|
|
self.password = self.bal_window.get_wallet_password(msg, parent=self)
|
|
|
|
def msg_edit_row(self, line, row=None):
|
|
_logger.debug(f"{row},{line}")
|
|
|
|
try:
|
|
self.labels[row] = line
|
|
except Exception:
|
|
self.labels.append(line)
|
|
row = len(self.labels) - 1
|
|
|
|
self.updatemessage.emit()
|
|
|
|
return row
|
|
|
|
def msg_del_row(self, row):
|
|
try:
|
|
del self.labels[row]
|
|
except Exception:
|
|
pass
|
|
self.updatemessage.emit()
|
|
|
|
def update(self):
|
|
self.vbox.removeWidget(self.qwidget)
|
|
self.qwidget = QWidget(self)
|
|
labelsbox = QVBoxLayout(self.qwidget)
|
|
for label in self.labels:
|
|
labelsbox.addWidget(QLabel(label))
|
|
self.vbox.addWidget(self.qwidget)
|
|
|
|
def get_text(self):
|
|
return self.message_label.text()
|
|
|
|
pass
|
|
|
|
|
|
class HeirList(MyTreeView, MessageBoxMixin):
|
|
class Columns(MyTreeView.BaseColumnsEnum):
|
|
NAME = enum.auto()
|
|
ADDRESS = enum.auto()
|
|
AMOUNT = enum.auto()
|
|
|
|
headers = {
|
|
Columns.NAME: _("Name"),
|
|
Columns.ADDRESS: _("Address"),
|
|
Columns.AMOUNT: _("Amount"),
|
|
}
|
|
filter_columns = [Columns.NAME, Columns.ADDRESS]
|
|
|
|
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000
|
|
|
|
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 1001
|
|
key_role = ROLE_HEIR_KEY
|
|
|
|
def __init__(self, bal_window: "BalWindow", parent):
|
|
super().__init__(
|
|
parent=parent,
|
|
main_window=bal_window.window,
|
|
stretch_column=self.Columns.NAME,
|
|
editable_columns=[
|
|
self.Columns.NAME,
|
|
self.Columns.ADDRESS,
|
|
self.Columns.AMOUNT,
|
|
],
|
|
)
|
|
self.decimal_point = bal_window.window.get_decimal_point()
|
|
self.bal_window = bal_window
|
|
|
|
try:
|
|
self.setModel(QStandardItemModel(self))
|
|
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
|
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
except Exception:
|
|
pass
|
|
|
|
self.setSortingEnabled(True)
|
|
self.std_model = self.model()
|
|
|
|
self.update()
|
|
|
|
def on_activated(self, idx):
|
|
self.on_double_click(idx)
|
|
|
|
def on_double_click(self, idx):
|
|
edit_key = self.get_edit_key_from_coordinate(idx.row(), idx.column())
|
|
self.bal_window.heirs.get(edit_key)
|
|
self.bal_window.new_heir_dialog(edit_key)
|
|
|
|
def on_edited(self, idx, edit_key, *, text):
|
|
original = prior_name = self.bal_window.heirs.get(edit_key)
|
|
if not prior_name:
|
|
return
|
|
col = idx.column()
|
|
try:
|
|
if col == 2:
|
|
text = Util.encode_amount(text, self.decimal_point)
|
|
elif col == 0:
|
|
self.bal_window.delete_heirs([edit_key])
|
|
edit_key = text
|
|
prior_name[col - 1] = text
|
|
prior_name.insert(0, edit_key)
|
|
prior_name = tuple(prior_name)
|
|
except Exception:
|
|
prior_name = (
|
|
(edit_key,) + prior_name[: col - 1] + (text,) + prior_name[col:]
|
|
)
|
|
|
|
try:
|
|
self.bal_window.set_heir(prior_name)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
self.bal_window.set_heir((edit_key,) + original)
|
|
except Exception:
|
|
self.update()
|
|
|
|
def create_menu(self, position):
|
|
menu = QMenu()
|
|
idx = self.indexAt(position)
|
|
column = idx.column() or self.Columns.NAME
|
|
selected_keys = []
|
|
for s_idx in self.selected_in_column(self.Columns.NAME):
|
|
sel_key = self.model().itemFromIndex(s_idx).data(0)
|
|
selected_keys.append(sel_key)
|
|
if selected_keys and idx.isValid():
|
|
column_title = self.model().horizontalHeaderItem(column).text()
|
|
# ok
|
|
column_data = "\n".join(
|
|
self.model().itemFromIndex(s_idx).text()
|
|
for s_idx in self.selected_in_column(column)
|
|
)
|
|
menu.addAction(
|
|
_("Copy {}").format(column_title),
|
|
lambda: self.place_text_on_clipboard(column_data, title=column_title),
|
|
)
|
|
if column in self.editable_columns:
|
|
item = self.model().itemFromIndex(idx)
|
|
if item.isEditable():
|
|
persistent = QPersistentModelIndex(idx)
|
|
menu.addAction(
|
|
_("Edit {}").format(column_title),
|
|
lambda p=persistent: self.edit(QModelIndex(p)),
|
|
)
|
|
menu.addAction(
|
|
_("Delete"), lambda: self.bal_window.delete_heirs(selected_keys)
|
|
)
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
# def get_selected_keys(self):
|
|
# selected_keys = []
|
|
# for s_idx in self.selected_in_column(self.Columns.NAME):
|
|
# sel_key = self.model().itemFromIndex(s_idx).data(0)
|
|
# selected_keys.append(sel_key)
|
|
# return selected_keys
|
|
def update(self):
|
|
if self.maybe_defer_update():
|
|
return
|
|
current_key = self.get_role_data_for_current_item(
|
|
col=self.Columns.NAME, role=self.ROLE_HEIR_KEY
|
|
)
|
|
self.model().clear()
|
|
self.update_headers(self.__class__.headers)
|
|
set_current = None
|
|
for key in sorted(self.bal_window.heirs.keys()):
|
|
heir = self.bal_window.heirs[key]
|
|
labels = [""] * len(self.Columns)
|
|
labels[self.Columns.NAME] = key
|
|
labels[self.Columns.ADDRESS] = heir[0]
|
|
labels[self.Columns.AMOUNT] = Util.decode_amount(
|
|
heir[1], self.decimal_point
|
|
)
|
|
|
|
items = [QStandardItem(x) for x in labels]
|
|
items[self.Columns.NAME].setEditable(True)
|
|
items[self.Columns.ADDRESS].setEditable(True)
|
|
items[self.Columns.AMOUNT].setEditable(True)
|
|
items[self.Columns.NAME].setData(key, self.ROLE_HEIR_KEY + 1)
|
|
items[self.Columns.ADDRESS].setData(key, self.ROLE_HEIR_KEY + 2)
|
|
items[self.Columns.AMOUNT].setData(key, self.ROLE_HEIR_KEY + 3)
|
|
|
|
row_count = self.model().rowCount()
|
|
self.model().insertRow(row_count, items)
|
|
|
|
if key == current_key:
|
|
idx = self.model().index(row_count, self.Columns.NAME)
|
|
set_current = QPersistentModelIndex(idx)
|
|
self.set_current_idx(set_current)
|
|
# FIXME refresh loses sort order; so set "default" here:
|
|
self.filter()
|
|
run_hook("update_heirs_tab", self)
|
|
self.update_will_settings()
|
|
|
|
def refresh_row(self, key, row):
|
|
# nothing to update here
|
|
pass
|
|
|
|
def get_edit_key_from_coordinate(self, row, col):
|
|
return self.get_role_data_from_coordinate(
|
|
row, col, role=self.ROLE_HEIR_KEY + col + 1
|
|
)
|
|
|
|
def create_toolbar(self, config):
|
|
toolbar, menu = self.create_toolbar_with_menu("")
|
|
menu.addAction(_("&New Heir"), self.bal_window.new_heir_dialog)
|
|
menu.addAction(_("Import"), self.bal_window.import_heirs)
|
|
menu.addAction(_("Export"), lambda: self.bal_window.export_heirs())
|
|
|
|
self.heir_locktime = HeirsLockTimeEdit(self, 0)
|
|
|
|
def on_heir_locktime():
|
|
if not self.heir_locktime.get_locktime():
|
|
self.heir_locktime.set_locktime("1y")
|
|
self.bal_window.will_settings["locktime"] = (
|
|
self.heir_locktime.get_locktime()
|
|
if self.heir_locktime.get_locktime()
|
|
else "1y"
|
|
)
|
|
self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings)
|
|
|
|
self.heir_locktime.valueEdited.connect(on_heir_locktime)
|
|
|
|
self.heir_threshold = HeirsLockTimeEdit(self, 0)
|
|
|
|
def on_heir_threshold():
|
|
if not self.heir_threshold.get_locktime():
|
|
self.heir_threshold.set_locktime("180d")
|
|
|
|
self.bal_window.will_settings["threshold"] = (
|
|
self.heir_threshold.get_locktime()
|
|
)
|
|
self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings)
|
|
|
|
self.heir_threshold.valueEdited.connect(on_heir_threshold)
|
|
|
|
self.heir_tx_fees = QSpinBox()
|
|
self.heir_tx_fees.setMinimum(1)
|
|
self.heir_tx_fees.setMaximum(10000)
|
|
|
|
def on_heir_tx_fees():
|
|
if not self.heir_tx_fees.value():
|
|
self.heir_tx_fees.set_value(1)
|
|
self.bal_window.will_settings["baltx_fees"] = self.heir_tx_fees.value()
|
|
self.bal_window.bal_plugin.WILL_SETTINGS.set(self.bal_window.will_settings)
|
|
|
|
self.heir_tx_fees.valueChanged.connect(on_heir_tx_fees)
|
|
|
|
self.heirs_widget = QWidget()
|
|
layout = QHBoxLayout()
|
|
self.heirs_widget.setLayout(layout)
|
|
|
|
layout.addWidget(QLabel(_("Delivery Time:")))
|
|
layout.addWidget(self.heir_locktime)
|
|
layout.addWidget(
|
|
HelpButton(
|
|
_(
|
|
"Locktime* to be used in the transaction\n"
|
|
+ "if you choose Raw, you can insert various options based on suffix:\n"
|
|
+ " - d: number of days after current day(ex: 1d means tomorrow)\n"
|
|
+ " - y: number of years after currrent day(ex: 1y means one year from today)\n"
|
|
+ "* locktime can be anticipated to update will\n"
|
|
)
|
|
)
|
|
)
|
|
|
|
layout.addWidget(QLabel(" "))
|
|
layout.addWidget(QLabel(_("Check Alive:")))
|
|
layout.addWidget(self.heir_threshold)
|
|
layout.addWidget(
|
|
HelpButton(
|
|
_(
|
|
"Check to ask for invalidation.\n"
|
|
+ "When less then this time is missing, ask to invalidate.\n"
|
|
+ "If you fail to invalidate during this time, your transactions will be delivered to your heirs.\n"
|
|
+ "if you choose Raw, you can insert various options based on suffix:\n"
|
|
+ " - d: number of days after current day(ex: 1d means tomorrow).\n"
|
|
+ " - y: number of years after currrent day(ex: 1y means one year from today).\n\n"
|
|
)
|
|
)
|
|
)
|
|
layout.addWidget(QLabel(" "))
|
|
layout.addWidget(QLabel(_("Fees:")))
|
|
layout.addWidget(self.heir_tx_fees)
|
|
layout.addWidget(HelpButton(_("Fee to be used in the transaction")))
|
|
layout.addWidget(QLabel("sats/vbyte"))
|
|
layout.addWidget(QLabel(" "))
|
|
newHeirButton = QPushButton(_("New Heir"))
|
|
newHeirButton.clicked.connect(self.bal_window.new_heir_dialog)
|
|
layout.addWidget(newHeirButton)
|
|
|
|
toolbar.insertWidget(2, self.heirs_widget)
|
|
|
|
return toolbar
|
|
|
|
def update_will_settings(self):
|
|
try:
|
|
self.heir_locktime.set_locktime(self.bal_window.will_settings["locktime"])
|
|
self.heir_tx_fees.setValue(int(self.bal_window.will_settings["baltx_fees"]))
|
|
self.heir_threshold.set_locktime(self.bal_window.will_settings["threshold"])
|
|
|
|
except Exception as e:
|
|
_logger.debug(f"Exception update_will_settings {e}")
|
|
|
|
def build_transactions(self):
|
|
# will = self.bal_window.prepare_will()
|
|
self.bal_window.prepare_will()
|
|
|
|
|
|
class PreviewList(MyTreeView):
|
|
class Columns(MyTreeView.BaseColumnsEnum):
|
|
LOCKTIME = enum.auto()
|
|
TXID = enum.auto()
|
|
WILLEXECUTOR = enum.auto()
|
|
STATUS = enum.auto()
|
|
|
|
headers = {
|
|
Columns.LOCKTIME: _("Locktime"),
|
|
Columns.TXID: _("Txid"),
|
|
Columns.WILLEXECUTOR: _("Will-Executor"),
|
|
Columns.STATUS: _("Status"),
|
|
}
|
|
|
|
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
|
|
key_role = ROLE_HEIR_KEY
|
|
|
|
def __init__(self, bal_window: "BalWindow", parent, will):
|
|
super().__init__(
|
|
parent=parent,
|
|
stretch_column=self.Columns.TXID,
|
|
)
|
|
self.parent = parent
|
|
self.bal_window = bal_window
|
|
self.decimal_point = bal_window.window.get_decimal_point
|
|
self.setModel(QStandardItemModel(self))
|
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
|
|
if will is not None:
|
|
self.will = will
|
|
else:
|
|
self.will = bal_window.willitems
|
|
|
|
self.wallet = bal_window.window.wallet
|
|
self.setModel(QStandardItemModel(self))
|
|
self.setSortingEnabled(True)
|
|
self.std_model = self.model()
|
|
self.config = bal_window.bal_plugin.config
|
|
self.bal_plugin = self.bal_window.bal_plugin
|
|
|
|
self.update()
|
|
|
|
def on_activated(self, idx):
|
|
self.on_double_click(idx)
|
|
|
|
def on_double_click(self, idx):
|
|
idx = self.model().index(idx.row(), self.Columns.TXID)
|
|
sel_key = self.model().itemFromIndex(idx).data(0)
|
|
self.show_transaction([sel_key])
|
|
|
|
def create_menu(self, position):
|
|
menu = QMenu()
|
|
idx = self.indexAt(position)
|
|
column = idx.column() or self.Columns.TXID
|
|
selected_keys = []
|
|
for s_idx in self.selected_in_column(self.Columns.TXID):
|
|
sel_key = self.model().itemFromIndex(s_idx).data(0)
|
|
selected_keys.append(sel_key)
|
|
if selected_keys and idx.isValid():
|
|
column_title = self.model().horizontalHeaderItem(column).text()
|
|
# column_data = "\n".join(
|
|
# self.model().itemFromIndex(s_idx).text()
|
|
# for s_idx in self.selected_in_column(column)
|
|
# )
|
|
|
|
menu.addAction(
|
|
_("details").format(column_title),
|
|
lambda: self.show_transaction(selected_keys),
|
|
).setEnabled(len(selected_keys) < 2)
|
|
menu.addAction(
|
|
_("check ").format(column_title),
|
|
lambda: self.check_transactions(selected_keys),
|
|
)
|
|
if self.bal_plugin.ENABLE_MULTIVERSE.get():
|
|
try:
|
|
self.importaction = self.menu.addAction(
|
|
_("Import"), self.import_will
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
menu.addSeparator()
|
|
menu.addAction(
|
|
_("delete").format(column_title), lambda: self.delete(selected_keys)
|
|
)
|
|
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
def delete(self, selected_keys):
|
|
for key in selected_keys:
|
|
del self.will[key]
|
|
try:
|
|
del self.bal_window.willitems[key]
|
|
except Exception:
|
|
pass
|
|
try:
|
|
del self.bal_window.will[key]
|
|
except Exception:
|
|
pass
|
|
self.update()
|
|
|
|
def check_transactions(self, selected_keys):
|
|
wout = {}
|
|
for k in selected_keys:
|
|
wout[k] = self.will[k]
|
|
if wout:
|
|
self.bal_window.check_transactions(wout)
|
|
self.update()
|
|
|
|
def show_transaction(self, selected_keys):
|
|
for key in selected_keys:
|
|
self.bal_window.show_transaction(self.will[key].tx)
|
|
|
|
self.update()
|
|
|
|
def select(self, selected_keys):
|
|
self.selected += selected_keys
|
|
self.update()
|
|
|
|
def deselect(self, selected_keys):
|
|
for key in selected_keys:
|
|
self.selected.remove(key)
|
|
self.update()
|
|
|
|
def update_will(self, will):
|
|
self.will.update(will)
|
|
self.update()
|
|
|
|
def replace(self, set_current, current_key, txid, bal_tx):
|
|
if self.bal_window.bal_plugin._hide_replaced and bal_tx.get_status("REPLACED"):
|
|
return False
|
|
if self.bal_window.bal_plugin._hide_invalidated and bal_tx.get_status(
|
|
"INVALIDATED"
|
|
):
|
|
return False
|
|
|
|
if not isinstance(bal_tx, WillItem):
|
|
bal_tx = WillItem(bal_tx)
|
|
|
|
tx = bal_tx.tx
|
|
|
|
labels = [""] * len(self.Columns)
|
|
labels[self.Columns.LOCKTIME] = Util.locktime_to_str(tx.locktime)
|
|
labels[self.Columns.TXID] = txid
|
|
we = "None"
|
|
if bal_tx.we:
|
|
we = bal_tx.we["url"]
|
|
labels[self.Columns.WILLEXECUTOR] = we
|
|
status = bal_tx.status
|
|
if len(bal_tx.status) > 53:
|
|
status = "...{}".format(status[-50:])
|
|
labels[self.Columns.STATUS] = status
|
|
|
|
items = []
|
|
for e in labels:
|
|
if isinstance(e, list):
|
|
try:
|
|
items.append(QStandardItem(*e))
|
|
except Exception as e:
|
|
pass
|
|
else:
|
|
items.append(QStandardItem(str(e)))
|
|
|
|
items[-1].setBackground(QColor(bal_tx.get_color()))
|
|
|
|
row_count = self.model().rowCount()
|
|
self.model().insertRow(row_count, items)
|
|
if txid == current_key:
|
|
idx = self.model().index(row_count, self.Columns.TXID)
|
|
set_current = QPersistentModelIndex(idx)
|
|
self.set_current_idx(set_current)
|
|
return set_current
|
|
|
|
def update(self):
|
|
try:
|
|
self.menu.removeAction(self.importaction)
|
|
except Exception:
|
|
pass
|
|
|
|
if self.will is None:
|
|
return
|
|
|
|
current_key = self.get_role_data_for_current_item(
|
|
col=self.Columns.TXID, role=self.ROLE_HEIR_KEY
|
|
)
|
|
self.model().clear()
|
|
self.update_headers(self.__class__.headers)
|
|
|
|
set_current = None
|
|
for txid, bal_tx in self.will.items():
|
|
tmp = self.replace(set_current, current_key, txid, bal_tx)
|
|
if tmp:
|
|
set_current = tmp
|
|
|
|
def create_toolbar(self, config):
|
|
toolbar, menu = self.create_toolbar_with_menu("")
|
|
menu.addAction(_("Prepare"), self.build_transactions)
|
|
menu.addAction(_("Display"), self.bal_window.preview_modal_dialog)
|
|
menu.addAction(_("Sign"), self.ask_password_and_sign_transactions)
|
|
menu.addAction(_("Export"), self.export_will)
|
|
if self.bal_plugin.ENABLE_MULTIVERSE.get():
|
|
self.importaction = menu.addAction(_("Import"), self.import_will)
|
|
menu.addAction(_("Broadcast"), self.broadcast)
|
|
menu.addAction(_("Check"), self.check)
|
|
menu.addAction(_("Invalidate"), self.invalidate_will)
|
|
|
|
wizard = QPushButton(_("Setup Wizard"))
|
|
wizard.clicked.connect(self.bal_window.init_wizard)
|
|
display = QPushButton(_("Display"))
|
|
display.clicked.connect(self.bal_window.preview_modal_dialog)
|
|
|
|
widget = QWidget()
|
|
hlayout = QHBoxLayout(widget)
|
|
hlayout.addWidget(wizard)
|
|
hlayout.addWidget(display)
|
|
toolbar.insertWidget(2, widget)
|
|
|
|
self.menu = menu
|
|
self.toolbar = toolbar
|
|
return toolbar
|
|
|
|
def hide_replaced(self):
|
|
self.bal_window.bal_plugin.hide_replaced()
|
|
self.update()
|
|
|
|
def hide_invalidated(self):
|
|
self.bal_window.bal_plugin.hide_invalidated()
|
|
self.update()
|
|
|
|
def build_transactions(self):
|
|
will = self.bal_window.prepare_will()
|
|
if will:
|
|
self.update_will(will)
|
|
|
|
def export_json_file(self, path):
|
|
write_json_file(path, self.will)
|
|
|
|
def export_will(self):
|
|
self.bal_window.export_will()
|
|
self.update()
|
|
|
|
def import_will(self):
|
|
self.bal_window.import_will()
|
|
|
|
def ask_password_and_sign_transactions(self):
|
|
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
|
|
|
|
def broadcast(self):
|
|
self.bal_window.broadcast_transactions()
|
|
self.update()
|
|
|
|
def check(self):
|
|
will = {}
|
|
for wid, w in self.bal_window.willitems.items():
|
|
if w.get_status("VALID"):
|
|
will[wid] = w
|
|
self.bal_window.check_transactions(will)
|
|
self.update()
|
|
|
|
def invalidate_will(self):
|
|
self.bal_window.invalidate_will()
|
|
self.update()
|
|
|
|
|
|
class PreviewDialog(BalDialog, MessageBoxMixin):
|
|
def __init__(self, bal_window, will):
|
|
self.parent = bal_window.window
|
|
BalDialog.__init__(
|
|
self, bal_window=bal_window, bal_plugin=bal_window.bal_plugin
|
|
)
|
|
self.bal_plugin = bal_window.bal_plugin
|
|
self.gui_object = self.bal_plugin.gui_object
|
|
self.config = self.bal_plugin.config
|
|
self.bal_window = bal_window
|
|
self.wallet = bal_window.window.wallet
|
|
self.format_amount = bal_window.window.format_amount
|
|
self.base_unit = bal_window.window.base_unit
|
|
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
|
|
self.fx = bal_window.window.fx
|
|
self.format_fee_rate = bal_window.window.format_fee_rate
|
|
self.show_address = bal_window.window.show_address
|
|
if not will:
|
|
self.will = bal_window.willitems
|
|
else:
|
|
self.will = will
|
|
self.setWindowTitle(_("Transactions Preview"))
|
|
self.setMinimumSize(1000, 200)
|
|
self.size_label = QLabel()
|
|
self.transactions_list = PreviewList(self.bal_window, self.will)
|
|
vbox = QVBoxLayout(self)
|
|
vbox.addWidget(self.size_label)
|
|
vbox.addWidget(self.transactions_list)
|
|
buttonbox = QHBoxLayout()
|
|
|
|
b = QPushButton(_("Sign"))
|
|
b.clicked.connect(self.transactions_list.ask_password_and_sign_transactions)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Export Will"))
|
|
b.clicked.connect(self.transactions_list.export_will)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Broadcast"))
|
|
b.clicked.connect(self.transactions_list.broadcast)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Invalidate will"))
|
|
b.clicked.connect(self.transactions_list.invalidate_will)
|
|
buttonbox.addWidget(b)
|
|
|
|
vbox.addLayout(buttonbox)
|
|
|
|
self.update()
|
|
|
|
def update_will(self, will):
|
|
self.will.update(will)
|
|
self.transactions_list.update_will(will)
|
|
self.update()
|
|
|
|
def update(self):
|
|
self.transactions_list.update()
|
|
|
|
def is_hidden(self):
|
|
return self.isMinimized() or self.isHidden()
|
|
|
|
def show_or_hide(self):
|
|
if self.is_hidden():
|
|
self.bring_to_top()
|
|
else:
|
|
self.hide()
|
|
|
|
def bring_to_top(self):
|
|
self.show()
|
|
self.raise_()
|
|
|
|
def closeEvent(self, event):
|
|
event.accept()
|
|
|
|
|
|
def read_bal_QIcon(icon_basename: str = DEFAULT_ICON) -> QIcon:
|
|
return QIcon(icon_path(icon_basename))
|
|
|
|
|
|
def read_bal_QPixmap(icon_basename: str = DEFAULT_ICON) -> QPixmap:
|
|
return QPixmap(icon_path(icon_basename))
|
|
|
|
|
|
class WillDetailDialog(BalDialog):
|
|
def __init__(self, bal_window):
|
|
|
|
self.will = bal_window.willitems
|
|
self.threshold = Util.parse_locktime_string(
|
|
bal_window.will_settings["threshold"]
|
|
)
|
|
self.bal_window = bal_window
|
|
Will.add_willtree(self.will)
|
|
super().__init__(bal_window.window, bal_window.bal_plugin)
|
|
self.config = bal_window.window.config
|
|
self.wallet = bal_window.wallet
|
|
self.format_amount = bal_window.window.format_amount
|
|
self.base_unit = bal_window.window.base_unit
|
|
self.format_fiat_and_units = bal_window.window.format_fiat_and_units
|
|
self.fx = bal_window.window.fx
|
|
self.format_fee_rate = bal_window.window.format_fee_rate
|
|
self.decimal_point = bal_window.window.get_decimal_point()
|
|
self.base_unit_name = decimal_point_to_base_unit_name(self.decimal_point)
|
|
self.setWindowTitle(_("Will Details"))
|
|
self.setMinimumSize(670, 700)
|
|
self.vlayout = QVBoxLayout()
|
|
w = QWidget()
|
|
hlayout = QHBoxLayout(w)
|
|
|
|
b = QPushButton(_("Sign"))
|
|
b.clicked.connect(self.ask_password_and_sign_transactions)
|
|
hlayout.addWidget(b)
|
|
|
|
b = QPushButton(_("Broadcast"))
|
|
b.clicked.connect(self.broadcast_transactions)
|
|
hlayout.addWidget(b)
|
|
|
|
b = QPushButton(_("Export"))
|
|
b.clicked.connect(self.export_will)
|
|
hlayout.addWidget(b)
|
|
b = QPushButton(_("Invalidate"))
|
|
b.clicked.connect(bal_window.invalidate_will)
|
|
hlayout.addWidget(b)
|
|
self.vlayout.addWidget(w)
|
|
|
|
self.paint_scroll_area()
|
|
self.vlayout.addWidget(
|
|
QLabel(_("Expiration date: ") + Util.locktime_to_str(self.threshold))
|
|
)
|
|
self.vlayout.addWidget(self.scrollbox)
|
|
w = QWidget()
|
|
hlayout = QHBoxLayout(w)
|
|
hlayout.addWidget(
|
|
QLabel(_("Valid Txs:") + str(len(Will.only_valid_list(self.will))))
|
|
)
|
|
hlayout.addWidget(QLabel(_("Total Txs:") + str(len(self.will))))
|
|
self.vlayout.addWidget(w)
|
|
self.setLayout(self.vlayout)
|
|
|
|
def paint_scroll_area(self):
|
|
self.scrollbox = QScrollArea()
|
|
viewport = QWidget(self.scrollbox)
|
|
self.willlayout = QVBoxLayout(viewport)
|
|
self.detailsWidget = WillWidget(parent=self)
|
|
self.willlayout.addWidget(self.detailsWidget)
|
|
|
|
self.scrollbox.setWidget(viewport)
|
|
viewport.setLayout(self.willlayout)
|
|
|
|
def ask_password_and_sign_transactions(self):
|
|
self.bal_window.ask_password_and_sign_transactions(callback=self.update)
|
|
self.update()
|
|
|
|
def broadcast_transactions(self):
|
|
self.bal_window.broadcast_transactions()
|
|
self.update()
|
|
|
|
def export_will(self):
|
|
self.bal_window.export_will()
|
|
|
|
def toggle_replaced(self):
|
|
self.bal_window.bal_plugin.hide_replaced()
|
|
toggle = _("Hide")
|
|
if self.bal_window.bal_plugin._hide_replaced:
|
|
toggle = _("Unhide")
|
|
self.toggle_replace_button.setText(f"{toggle} {_('replaced')}")
|
|
self.update()
|
|
|
|
def toggle_invalidated(self):
|
|
self.bal_window.bal_plugin.hide_invalidated()
|
|
toggle = _("Hide")
|
|
if self.bal_window.bal_plugin._hide_invalidated:
|
|
toggle = _("Unhide")
|
|
self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}"))
|
|
self.update()
|
|
|
|
def update(self):
|
|
self.will = self.bal_window.willitems
|
|
pos = self.vlayout.indexOf(self.scrollbox)
|
|
self.vlayout.removeWidget(self.scrollbox)
|
|
self.paint_scroll_area()
|
|
self.vlayout.insertWidget(pos, self.scrollbox)
|
|
super().update()
|
|
|
|
|
|
class WillWidget(QWidget):
|
|
def __init__(self, father=None, parent=None):
|
|
super().__init__()
|
|
vlayout = QVBoxLayout()
|
|
self.setLayout(vlayout)
|
|
self.will = parent.bal_window.willitems
|
|
self.parent = parent
|
|
for w in self.will:
|
|
if (
|
|
self.will[w].get_status("REPLACED")
|
|
and self.parent.bal_window.bal_plugin._hide_replaced
|
|
):
|
|
continue
|
|
if (
|
|
self.will[w].get_status("INVALIDATED")
|
|
and self.parent.bal_window.bal_plugin._hide_invalidated
|
|
):
|
|
continue
|
|
f = self.will[w].father
|
|
if father == f:
|
|
qwidget = QWidget()
|
|
# childWidget = QWidget()
|
|
hlayout = QHBoxLayout(qwidget)
|
|
qwidget.setLayout(hlayout)
|
|
vlayout.addWidget(qwidget)
|
|
detailw = QWidget()
|
|
detaillayout = QVBoxLayout()
|
|
detailw.setLayout(detaillayout)
|
|
|
|
willpushbutton = QPushButton(w)
|
|
|
|
willpushbutton.clicked.connect(
|
|
partial(self.parent.bal_window.show_transaction, txid=w)
|
|
)
|
|
detaillayout.addWidget(willpushbutton)
|
|
locktime = Util.locktime_to_str(self.will[w].tx.locktime)
|
|
creation = Util.locktime_to_str(self.will[w].time)
|
|
|
|
def qlabel(title, value):
|
|
label = "<b>" + _(str(title)) + f":</b>\t{str(value)}"
|
|
return QLabel(label)
|
|
|
|
detaillayout.addWidget(qlabel("Locktime", locktime))
|
|
detaillayout.addWidget(qlabel("Creation Time", creation))
|
|
try:
|
|
total_fees = (
|
|
self.will[w].tx.input_value() - self.will[w].tx.output_value()
|
|
)
|
|
except Exception:
|
|
total_fees = -1
|
|
decoded_fees = total_fees
|
|
fee_per_byte = round(total_fees / self.will[w].tx.estimated_size(), 3)
|
|
fees_str = str(decoded_fees) + " (" + str(fee_per_byte) + " sats/vbyte)"
|
|
detaillayout.addWidget(qlabel("Transaction fees:", fees_str))
|
|
detaillayout.addWidget(qlabel("Status:", self.will[w].status))
|
|
detaillayout.addWidget(QLabel(""))
|
|
detaillayout.addWidget(QLabel("<b>Heirs:</b>"))
|
|
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(_("<b>Willexecutor:</b:")))
|
|
decoded_amount = Util.decode_amount(
|
|
self.will[w].we["base_fee"], self.parent.decimal_point
|
|
)
|
|
|
|
detaillayout.addWidget(
|
|
qlabel(
|
|
self.will[w].we["url"],
|
|
f"{decoded_amount} {self.parent.base_unit_name}",
|
|
)
|
|
)
|
|
detaillayout.addStretch()
|
|
pal = QPalette()
|
|
pal.setColor(
|
|
QPalette.ColorRole.Window, QColor(self.will[w].get_color())
|
|
)
|
|
detailw.setAutoFillBackground(True)
|
|
detailw.setPalette(pal)
|
|
|
|
hlayout.addWidget(detailw)
|
|
hlayout.addWidget(WillWidget(w, parent=parent))
|
|
|
|
|
|
class WillExecutorList(MyTreeView):
|
|
class Columns(MyTreeView.BaseColumnsEnum):
|
|
SELECTED = enum.auto()
|
|
URL = enum.auto()
|
|
STATUS = enum.auto()
|
|
BASE_FEE = enum.auto()
|
|
INFO = enum.auto()
|
|
ADDRESS = enum.auto()
|
|
|
|
headers = {
|
|
Columns.SELECTED: _(""),
|
|
Columns.URL: _("Url"),
|
|
Columns.STATUS: _("S"),
|
|
Columns.BASE_FEE: _("Base fee"),
|
|
Columns.INFO: _("Info"),
|
|
Columns.ADDRESS: _("Default Address"),
|
|
}
|
|
|
|
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000
|
|
key_role = ROLE_HEIR_KEY
|
|
|
|
def __init__(self, parent: "WillExecutorDialog"):
|
|
super().__init__(
|
|
parent=parent,
|
|
stretch_column=self.Columns.ADDRESS,
|
|
editable_columns=[
|
|
self.Columns.URL,
|
|
self.Columns.BASE_FEE,
|
|
self.Columns.ADDRESS,
|
|
self.Columns.INFO,
|
|
],
|
|
)
|
|
self.parent=parent
|
|
try:
|
|
self.setModel(QStandardItemModel(self))
|
|
self.sortByColumn(self.Columns.SELECTED, Qt.SortOrder.AscendingOrder)
|
|
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
|
except Exception:
|
|
pass
|
|
self.setSortingEnabled(True)
|
|
self.std_model = self.model()
|
|
self.config = parent.bal_plugin.config
|
|
self.get_decimal_point = parent.bal_plugin.get_decimal_point
|
|
|
|
self.update()
|
|
|
|
def create_menu(self, position):
|
|
menu = QMenu()
|
|
idx = self.indexAt(position)
|
|
column = idx.column() or self.Columns.URL
|
|
selected_keys = []
|
|
for s_idx in self.selected_in_column(self.Columns.URL):
|
|
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)
|
|
# )
|
|
if Willexecutors.is_selected(self.parent.willexecutors_list[sel_key]):
|
|
menu.addAction(
|
|
_("deselect").format(column_title),
|
|
lambda: self.deselect(selected_keys),
|
|
)
|
|
else:
|
|
menu.addAction(
|
|
_("select").format(column_title), lambda: self.select(selected_keys)
|
|
)
|
|
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(
|
|
_("Ping").format(column_title),
|
|
lambda: self.ping_willexecutors(selected_keys),
|
|
)
|
|
menu.addSeparator()
|
|
menu.addAction(
|
|
_("delete").format(column_title), lambda: self.delete(selected_keys)
|
|
)
|
|
|
|
menu.exec(self.viewport().mapToGlobal(position))
|
|
|
|
def ping_willexecutors(self, selected_keys):
|
|
wout = {}
|
|
for k in selected_keys:
|
|
wout[k] = self.parent.willexecutors_list[k]
|
|
self.parent.update_willexecutors(wout)
|
|
|
|
self.update()
|
|
|
|
def get_edit_key_from_coordinate(self, row, col):
|
|
a = self.get_role_data_from_coordinate(row, col, role=self.ROLE_HEIR_KEY + col)
|
|
return a
|
|
|
|
def delete(self, selected_keys):
|
|
for key in selected_keys:
|
|
del self.parent.willexecutors_list[key]
|
|
self.update()
|
|
|
|
def select(self, selected_keys):
|
|
for wid, w in self.parent.willexecutors_list.items():
|
|
if wid in selected_keys:
|
|
w["selected"] = True
|
|
self.update()
|
|
|
|
def deselect(self, selected_keys):
|
|
for wid, w in self.parent.willexecutors_list.items():
|
|
if wid in selected_keys:
|
|
w["selected"] = False
|
|
self.update()
|
|
|
|
def on_edited(self, idx, edit_key, *, text):
|
|
# prior_name = self.parent.willexecutors_list[edit_key]
|
|
col = idx.column()
|
|
try:
|
|
if col == self.Columns.URL:
|
|
self.parent.willexecutors_list[text] = self.parent.willexecutors_list[
|
|
edit_key
|
|
]
|
|
del self.parent.willexecutors_list[edit_key]
|
|
if col == self.Columns.BASE_FEE:
|
|
self.parent.willexecutors_list[edit_key]["base_fee"] = (
|
|
Util.encode_amount(text, self.get_decimal_point())
|
|
)
|
|
if col == self.Columns.ADDRESS:
|
|
self.parent.willexecutors_list[edit_key]["address"] = text
|
|
if col == self.Columns.INFO:
|
|
self.parent.willexecutors_list[edit_key]["info"] = text
|
|
self.update()
|
|
except Exception:
|
|
pass
|
|
|
|
def update(self):
|
|
|
|
if self.parent.willexecutors_list is None:
|
|
return
|
|
try:
|
|
current_key = self.get_role_data_for_current_item(
|
|
col=self.Columns.URL, role=self.ROLE_HEIR_KEY
|
|
)
|
|
self.model().clear()
|
|
self.update_headers(self.__class__.headers)
|
|
|
|
set_current = None
|
|
|
|
for url, value in self.parent.willexecutors_list.items():
|
|
labels = [""] * len(self.Columns)
|
|
labels[self.Columns.URL] = url
|
|
if Willexecutors.is_selected(value):
|
|
|
|
labels[self.Columns.SELECTED] = [read_QIcon_from_bytes(self.parent.bal_plugin.read_file("icons/confirmed.png")),""]
|
|
else:
|
|
labels[self.Columns.SELECTED] = ""
|
|
labels[self.Columns.BASE_FEE] = Util.decode_amount(
|
|
value.get("base_fee", 0), self.get_decimal_point()
|
|
)
|
|
if str(value.get("status", 0)) == "200":
|
|
labels[self.Columns.STATUS] = [read_QIcon_from_bytes(self.parent.bal_plugin.read_file("icons/status_connected.png")),""]
|
|
else:
|
|
labels[self.Columns.STATUS] = [read_QIcon_from_bytes(self.parent.bal_plugin.read_file("icons/unconfirmed.png")),""]
|
|
labels[self.Columns.ADDRESS] = str(value.get("address", ""))
|
|
labels[self.Columns.INFO] = str(value.get("info", ""))
|
|
|
|
items = []
|
|
for e in labels:
|
|
if isinstance(e, list):
|
|
try:
|
|
items.append(QStandardItem(*e))
|
|
except Exception as e:
|
|
pass
|
|
else:
|
|
items.append(QStandardItem(e))
|
|
items[self.Columns.SELECTED].setEditable(False)
|
|
items[self.Columns.URL].setEditable(True)
|
|
items[self.Columns.ADDRESS].setEditable(True)
|
|
items[self.Columns.INFO].setEditable(True)
|
|
items[self.Columns.BASE_FEE].setEditable(True)
|
|
items[self.Columns.STATUS].setEditable(False)
|
|
|
|
items[self.Columns.URL].setData(url, self.ROLE_HEIR_KEY + 1)
|
|
items[self.Columns.BASE_FEE].setData(url, self.ROLE_HEIR_KEY + 2)
|
|
items[self.Columns.INFO].setData(url, self.ROLE_HEIR_KEY + 3)
|
|
items[self.Columns.ADDRESS].setData(url, self.ROLE_HEIR_KEY + 4)
|
|
row_count = self.model().rowCount()
|
|
self.model().insertRow(row_count, items)
|
|
if url == current_key:
|
|
idx = self.model().index(row_count, self.Columns.NAME)
|
|
set_current = QPersistentModelIndex(idx)
|
|
self.set_current_idx(set_current)
|
|
|
|
self.parent.save_willexecutors()
|
|
|
|
except Exception as e:
|
|
_logger.error(e)
|
|
|
|
|
|
class WillExecutorWidget(QWidget, MessageBoxMixin):
|
|
def __init__(self, parent, bal_window, willexecutors=None):
|
|
self.bal_window = bal_window
|
|
self.bal_plugin = bal_window.bal_plugin
|
|
self.parent = parent
|
|
MessageBoxMixin.__init__(self)
|
|
QWidget.__init__(self, parent)
|
|
if willexecutors:
|
|
self.willexecutors_list = willexecutors
|
|
else:
|
|
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)
|
|
|
|
self.size_label = QLabel()
|
|
self.willexecutor_list = WillExecutorList(self)
|
|
|
|
vbox = QVBoxLayout(self)
|
|
vbox.addWidget(self.size_label)
|
|
|
|
widget = QWidget()
|
|
hbox = QHBoxLayout(widget)
|
|
hbox.addWidget(QLabel(_("Add transactions without willexecutor")))
|
|
heir_no_willexecutor = bal_checkbox(self.bal_plugin.NO_WILLEXECUTOR)
|
|
hbox.addWidget(heir_no_willexecutor)
|
|
spacer_widget = QWidget()
|
|
spacer_widget.setSizePolicy(
|
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
|
)
|
|
hbox.addWidget(spacer_widget)
|
|
vbox.addWidget(widget)
|
|
|
|
vbox.addWidget(self.willexecutor_list)
|
|
buttonbox = QHBoxLayout()
|
|
|
|
b = QPushButton(_("Add"))
|
|
b.clicked.connect(self.add)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Download List"))
|
|
b.clicked.connect(self.download_list)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Import"))
|
|
b.clicked.connect(self.import_file)
|
|
buttonbox.addWidget(b)
|
|
|
|
b = QPushButton(_("Export"))
|
|
b.clicked.connect(self.export_file)
|
|
buttonbox.addWidget(b)
|
|
|
|
#b = QPushButton(_("Ping All"))
|
|
#b.clicked.connect(self.update_willexecutors)
|
|
#buttonbox.addWidget(b)
|
|
|
|
vbox.addLayout(buttonbox)
|
|
self.willexecutor_list.update()
|
|
|
|
def add(self):
|
|
self.willexecutors_list["http://localhost:8080"] = {
|
|
"info": "New Will Executor",
|
|
"base_fee": 0,
|
|
"status": "-1",
|
|
}
|
|
self.willexecutor_list.update()
|
|
|
|
def download_list(self):
|
|
self.willexecutors_list.update(Willexecutors.download_list(self.bal_plugin))
|
|
self.willexecutor_list.update()
|
|
|
|
def export_file(self, path):
|
|
Util.export_meta_gui(
|
|
self.bal_window.window, _("willexecutors.json"), self.export_json_file
|
|
)
|
|
|
|
def export_json_file(self, path):
|
|
write_json_file(path, self.willexecutors_list)
|
|
|
|
def import_file(self):
|
|
import_meta_gui(
|
|
self.bal_window.window,
|
|
_("willexecutors.json"),
|
|
self.import_json_file,
|
|
self.willexecutors_list.update,
|
|
)
|
|
|
|
def update_willexecutors(self, wes=None):
|
|
if not wes:
|
|
self.willexecutors_list = Willexecutors.get_willexecutors(
|
|
self.bal_plugin,
|
|
update=True,
|
|
bal_window=self.bal_window,
|
|
force=True,
|
|
task=self,
|
|
)
|
|
else:
|
|
self.bal_window.ping_willexecutors(wes, self.parent)
|
|
self.willexecutors_list.update(wes)
|
|
self.willexecutor_list.update()
|
|
|
|
def import_json_file(self, path):
|
|
data = read_json_file(path)
|
|
data = self._validate(data)
|
|
self.willexecutors_list.update(data)
|
|
self.willexecutor_list.update()
|
|
|
|
# TODO validate willexecutor json import file
|
|
def _validate(self, data):
|
|
return data
|
|
|
|
def save_willexecutors(self):
|
|
Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list)
|
|
|
|
|
|
class WillExecutorDialog(BalDialog, MessageBoxMixin):
|
|
def __init__(self, bal_window, parent=None):
|
|
if not parent:
|
|
parent = bal_window.window
|
|
BalDialog.__init__(self, parent, bal_window.bal_plugin)
|
|
self.bal_plugin = bal_window.bal_plugin
|
|
self.config = self.bal_plugin.config
|
|
self.bal_window = bal_window
|
|
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)
|
|
|
|
self.setWindowTitle(_("Will-Executor Service List"))
|
|
self.setMinimumSize(1000, 200)
|
|
|
|
vbox = QVBoxLayout(self)
|
|
self.willexecutor_list = WillExecutorWidget(
|
|
self, self.bal_window, self.willexecutors_list
|
|
)
|
|
vbox.addWidget(self.willexecutor_list)
|
|
|
|
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()
|