From b5eda4f05aee1120a4673838676269d1632f0672 Mon Sep 17 00:00:00 2001 From: svatantrya Date: Mon, 27 Apr 2026 10:03:05 -0400 Subject: [PATCH] partial commit to fix wallet utils this commit provide a lot of changes in will-settings including export to ics calendar file. --- bal.py | 62 +- heirs.py | 11 +- qt.py | 1071 ++++++++++++++-------------- util.py | 68 +- wallet_util/README.md | 50 ++ wallet_util/bal_wallet_utils.py | 18 +- wallet_util/bal_wallet_utils_qt.py | 23 +- will.py | 50 +- willexecutors.py | 121 ++-- 9 files changed, 839 insertions(+), 635 deletions(-) create mode 100644 wallet_util/README.md diff --git a/bal.py b/bal.py index b5e415f..e516c00 100644 --- a/bal.py +++ b/bal.py @@ -1,14 +1,13 @@ import os - +from datetime import date, datetime, timedelta +import platform # import random # import zipfile as zipfile_lib - -from electrum import json_db +from electrum import constants, json_db from electrum.logging import get_logger from electrum.plugin import BasePlugin from electrum.transaction import tx_from_any - _logger = get_logger(__name__) def get_will_settings(x): # print(x) @@ -50,6 +49,12 @@ class BalConfig: class BalPlugin(BasePlugin): _version=None __version__ = "0.2.8" #AUTOMATICALLY GENERATED DO NOT EDIT + default_app={ + "Linux":"xdg-open", + "Window":"start", + "Darwin":"open" + } + chainname = constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin" def version(self): if not self._version: try: @@ -60,7 +65,7 @@ class BalPlugin(BasePlugin): except Exception as e: _logger.error(f"failed to get version: {e}") self._version="unknown" - return self._version + return self._version SIZE = (159, 97) @@ -101,6 +106,7 @@ class BalPlugin(BasePlugin): self.HIDE_INVALIDATED = BalConfig(config, "bal_hide_invalidated", True) self.ALLOW_REPUSH = BalConfig(config, "bal_allow_repush", True) self.FIRST_EXECUTION = BalConfig(config, "bal_first_execution", True) + self.WELIST_SERVER = BalConfig(config,"bal_welist_server","https://welist.bitcoin-after.life/") self.WILLEXECUTORS = BalConfig( config, "bal_willexecutors", @@ -123,18 +129,33 @@ class BalPlugin(BasePlugin): "selected": True, } }, + "testnet4": { + "https://we.bitcoin-after.life": { + "base_fee": 100000, + "status": "New", + "info": "Bitcoin After Life Will Executor", + "address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7", + "selected": True, + } + }, + "regtest": { + "https://we.bitcoin-after.life": { + "base_fee": 100000, + "status": "New", + "info": "Bitcoin After Life Will Executor", + "address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7", + "selected": True, + } + }, }, ) self.WILL_SETTINGS = BalConfig( config, "bal_will_settings", - { - "baltx_fees": 100, - "threshold": "180d", - "locktime": "1y", - }, + BalPlugin.default_will_settings(), ) - + self.system = platform.system() + self.CALENDAR_APP = BalConfig(config,"bal_open_app",self.default_app[self.system]) self._hide_invalidated = self.HIDE_INVALIDATED.get() self._hide_replaced = self.HIDE_REPLACED.get() @@ -150,15 +171,22 @@ class BalPlugin(BasePlugin): self.HIDE_REPLACED.set(self._hide_replaced) def validate_will_settings(self, will_settings): + defaults=BalPlugin.default_will_settings() if not will_settings: will_settings=[] - if int(will_settings.get("baltx_fees", 1)) < 1: - will_settings["baltx_fees"] = 1 + if int(will_settings.get("baltx_fees", 0)) < 1: + will_settings["baltx_fees"] = defaults['baltx_fees'] if not will_settings.get("threshold"): - will_settings["threshold"] = "180d" + will_settings["threshold"] = defaults['threshold'] if not will_settings.get("locktime"): - will_settings["locktime"] = "1y" + will_settings["locktime"] = defaults['locktime'] return will_settings - def default_will_settings(self): - return {"baltx_fees": 100, "threshold": "180d", "locktime": "1y"} + @staticmethod + def default_will_settings(): + today = date.today() + dt = datetime(today.year, today.month, today.day, 0, 0, 0) + threshold =(dt + timedelta(days=180)).timestamp() + locktime =(dt + timedelta(days=365)).timestamp() + + return {"baltx_fees": 100, "threshold": threshold, "locktime": locktime} diff --git a/heirs.py b/heirs.py index af6c7e8..b9021c3 100644 --- a/heirs.py +++ b/heirs.py @@ -34,6 +34,7 @@ from electrum.transaction import ( # TxOutput, ) from electrum.util import ( + BitcoinException, bfh, read_json_file, to_string, @@ -43,7 +44,7 @@ from electrum.util import ( from .util import Util from .willexecutors import Willexecutors -from electrum.util import BitcoinException + if TYPE_CHECKING: from .simple_config import SimpleConfig @@ -73,10 +74,10 @@ def reduce_outputs(in_amount, out_amount, fee, outputs): def create_op_return_script(data_hex: str) -> bytes: """Crea scriptpubkey OP_RETURN in bytes""" data = bytes.fromhex(data_hex) - + if len(data) > 80: raise ValueError("OP_RETURN data too big (max 80 bytes)") - + # Costruzione manuale: OP_RETURN + push data if len(data) <= 75: # Formato più comune: OP_RETURN + 1-byte length + data @@ -84,7 +85,7 @@ def create_op_return_script(data_hex: str) -> bytes: else: # Per dati più grandi (fino a 80) si usa OP_PUSHDATA1 script = b'\x6a\x4c' + bytes([len(data)]) + data - + return script def prepare_transactions(locktimes, available_utxos, fees, wallet): @@ -162,7 +163,7 @@ def prepare_transactions(locktimes, available_utxos, fees, wallet): random.shuffle(outputs) #op_return_text = "Hello Bal!" - + ## Convert text to hex #op_return_hex = op_return_text.encode('utf-8').hex() #op_return_script = create_op_return_script(op_return_hex) diff --git a/qt.py b/qt.py index 0a31250..59faedf 100644 --- a/qt.py +++ b/qt.py @@ -1,95 +1,67 @@ """ -Bal +BAL Bitcoin after life - """ - +import subprocess +import os import copy import enum import sys import time -from datetime import datetime +import traceback +from datetime import datetime,timedelta,timezone 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, - QPainter, - QPalette, - QStandardItem, - QStandardItemModel, - ) - from PyQt5.QtWidgets import ( - QAbstractItemView, - QCheckBox, - QComboBox, - QDateTimeEdit, - QGridLayout, - QHBoxLayout, - QLabel, - QLineEdit, - QMenu, - QMenuBar, - QPushButton, - QScrollArea, - QSizePolicy, - QSpinBox, - QStyle, - QStyleOptionFrame, - QVBoxLayout, - QWidget, - ) -else: # QT6 - from PyQt6.QtCore import ( - QDateTime, - QModelIndex, - QPersistentModelIndex, - Qt, - pyqtSignal, - ) - from PyQt6.QtGui import ( - QColor, - QPainter, - QPalette, - QStandardItem, - QStandardItemModel, - ) - from PyQt6.QtWidgets import ( - QAbstractItemView, - QCheckBox, - QComboBox, - QDateTimeEdit, - QGridLayout, - QHBoxLayout, - QLabel, - QLineEdit, - QMenu, - QMenuBar, - QPushButton, - QScrollArea, - QSizePolicy, - QSpinBox, - QStyle, - QStyleOptionFrame, - QVBoxLayout, - QWidget, - ) +from electrum.gui.qt.util import getSaveFileName +from electrum.util import FileExportFailed +from typing import Any, Optional, Union +from PyQt6.QtWidgets import QWidget, QHBoxLayout, QLabel, QComboBox, QLineEdit, QDateTimeEdit +from PyQt6.QtCore import pyqtSignal, QDateTime, Qt +from PyQt6.QtGui import QPainter +#from PyQt6.QtStyle import QStyle, QStyleOptionFrame +import tempfile + +#DELETEME + +from PyQt6.QtCore import ( + QDateTime, + QModelIndex, + QPersistentModelIndex, + Qt, + pyqtSignal, +) +from PyQt6.QtGui import ( + QColor, + QPainter, + QPalette, + QStandardItem, + QStandardItemModel, +) +from PyQt6.QtWidgets import ( + QAbstractItemView, + QCheckBox, + QComboBox, + QDateTimeEdit, + QGridLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QMenu, + QMenuBar, + QPushButton, + QScrollArea, + QSizePolicy, + QSpinBox, + QStyle, + QStyleOptionFrame, + QVBoxLayout, + QWidget, +) from electrum.bitcoin import ( NLOCKTIME_BLOCKHEIGHT_MAX, @@ -138,7 +110,7 @@ from electrum.util import ( ) from .bal import BalPlugin -from .heirs import Heirs, HEIR_REAL_AMOUNT, HEIR_DUST_AMOUNT +from .heirs import HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT, Heirs from .util import Util from .will import ( AmountException, @@ -162,13 +134,13 @@ _logger = get_logger(__name__) class Plugin(BalPlugin, Logger): def __init__(self, parent, config, name): Logger.__init__(self) - self.logger.info("INIT BALPLUGIN") + _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") + _logger.info("HOOK bal init qt") try: self.gui_object = gui_object for window in gui_object.windows: @@ -190,18 +162,18 @@ class Plugin(BalPlugin, Logger): w.init_menubar_tools(menu_child) except Exception as e: - self.logger.error( + _logger.error( ("init_qt except:", menu_child.text()) ) raise e except Exception as e: - self.logger.error("Error loading plugini {}".format(e)) + _logger.error("Error loading plugini {}".format(e)) raise e @hook def create_status_bar(self, sb): - self.logger.info("HOOK create status bar") + _logger.info("HOOK create status bar") return b = StatusBarButton( read_QIcon_from_bytes(self.bal_plugin.read_file("icons/bal32x32.png")), @@ -213,13 +185,13 @@ class Plugin(BalPlugin, Logger): @hook def init_menubar(self, window): - self.logger.info("HOOK init_menubar") + _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") + _logger.debug("HOOK load wallet") w = self.get_window(main_window) # havetoupdate = Util.fix_will_settings_tx_fees(wallet.db) w.wallet = wallet @@ -232,18 +204,18 @@ class Plugin(BalPlugin, Logger): @hook def close_wallet(self, wallet): - self.logger.debug("HOOK close wallet") - for winid, win in self.bal_windows.items(): + _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") + _logger.debug("init keystore") @hook def daemon_wallet_loaded(self, boh, wallet): - self.logger.debug("daemon wallet loaded") + _logger.debug("daemon wallet loaded") def get_window(self, window): w = self.bal_windows.get(window.winId, None) @@ -304,9 +276,12 @@ class Plugin(BalPlugin, Logger): 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)) + bal_mode = QComboBox() + options =["Easy","Advanced","Experimental"] + bal_mode.addItems(options) + grid = QGridLayout(d) add_widget( grid, @@ -322,6 +297,10 @@ class Plugin(BalPlugin, Logger): 2, "Hide invalidated transactions from will detail and list", ) + add_widget(grid,"Calendar App", QLineEdit(self.CALENDAR_APP.get()),3,"Default app used to open calendar",) + + add_widget(grid,"Bal Mode",bal_mode,4,"choose bal mode") + # add_widget( # grid, # "Ping Willexecutors", @@ -362,11 +341,11 @@ class Plugin(BalPlugin, Logger): return False def broadcast_transactions(self, force): - for k, w in self.bal_windows.items(): + for _k, w in self.bal_windows.items(): w.broadcast_transactions(force) def update_all(self): - for k, w in self.bal_windows.items(): + for _k, w in self.bal_windows.items(): w.update_all() def get_window_title(self, title): @@ -386,9 +365,9 @@ class shown_cv: self.value = value -class BalWindow(Logger): +class BalWindow(): + def __init__(self, bal_plugin: "BalPlugin", window: "ElectrumWindow"): - Logger.__init__(self) self.bal_plugin = bal_plugin self.window = window self.heirs = {} @@ -466,7 +445,7 @@ class BalWindow(Logger): self.will[wid] = w.to_dict() def init_will(self): - self.logger.info("********************init_____will____________**********") + _logger.info("********************init_____will____________**********") if not self.willexecutors: self.willexecutors = Willexecutors.get_willexecutors( self.bal_plugin, update=False, bal_window=self @@ -493,10 +472,10 @@ class BalWindow(Logger): # 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)) + # _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)) + # _logger.debug("not_will_settings {}".format(self.will_settings)) # self.bal_plugin.validate_will_settings(self.will_settings) # self.heir_list_widget.update_will_settings() # self.heir_list_widget.update() @@ -548,9 +527,9 @@ class BalWindow(Logger): heir_amount.setText( str(Util.decode_amount(heir[1], self.window.get_decimal_point())) ) - self.heir_locktime = HeirsLockTimeEdit(self.window, 0) + self.heir_locktime = LockTimeWidget(self.bal_window, self, heir[2]) if heir: - self.heir_locktime.set_locktime(heir[2]) + self.heir_locktime.set_value(heir[2]) # heir_is_xpub = QCheckBox() @@ -594,7 +573,7 @@ class BalWindow(Logger): heir_name.text(), heir_address.text(), Util.encode_amount(heir_amount.text(), self.window.get_decimal_point()), - str(self.heir_locktime.get_locktime()), + str(self.heir_locktime.get_value()), ] try: self.set_heir(heir) @@ -631,7 +610,7 @@ class BalWindow(Logger): ) def export_heirs(self): - Util.export_meta_gui(self.window, _("heirs"), self.heirs.export_file) + export_meta_gui(self.window, "heirs.json", self.heirs.export_file) def prepare_will(self, ignore_duplicate=False, keep_original=False): will = self.build_inheritance_transaction( @@ -660,7 +639,7 @@ class BalWindow(Logger): if not self.no_willexecutor: f = False - for u, w in self.willexecutors.items(): + for _u, w in self.willexecutors.items(): if Willexecutors.is_selected(w): f = True if not f: @@ -676,7 +655,7 @@ class BalWindow(Logger): self.date_to_check, ) - self.logger.info(f"txs built: {txs}") + _logger.info(f"txs built: {txs}") creation_time = time.time() if txs: for txid in txs: @@ -696,13 +675,13 @@ class BalWindow(Logger): will[txid] = WillItem(tx, _id=txid, wallet=self.wallet) self.update_will(will) else: - self.logger.info("No transactions was built") - self.logger.info(f"will-settings: {self.will_settings}") - self.logger.info(f"date_to_check:{self.date_to_check}") - self.logger.info(f"heirs: {self.heirs}") + _logger.info("No transactions was built") + _logger.info(f"will-settings: {self.will_settings}") + _logger.info(f"date_to_check:{self.date_to_check}") + _logger.info(f"heirs: {self.heirs}") return {} except Exception as e: - self.logger.info(f"Exception build_will: {e}") + _logger.info(f"Exception build_will: {e}") raise e pass return self.willitems @@ -733,34 +712,20 @@ class BalWindow(Logger): def show_critical(self, text): self.window.show_critical(text) - def update_locktime_widgets(self,locktime): - locktime = self.will_settings["locktime"] = ( - locktime - if locktime - else "1y" - ) + def update_setting_widgets(self,new_value,field,update_all=False,update_will_dialog=False,update_heirs_dialog=False,): + new_value = self.will_settings[field] = new_value if new_value else BalPlugin.default_will_settings()[field] + self.will_settings[field]=new_value self.bal_plugin.WILL_SETTINGS.set(self.will_settings) - try: - self.heir_list_widget.heir_locktime.set_locktime(locktime) - except Exception: - pass - #self.preview_list.heirs_locktime.set_locktim(will_settings['thershold']) - - def update_threshold_widgets(self,threshold): - threshold = self.will_settings["threshold"] = ( - threshold - if threshold - else "1y" - ) - self.bal_plugin.WILL_SETTINGS.set(self.will_settings) - try: - self.heir_list_widget.heir_threshold.set_locktime(threshold) - except Exception: - pass - try: - self.will_list.heir_threshold.set_locktime(threshold) - except Exception: - pass + if update_all or update_will_dialog: + try: + self.will_list.will_settings_widget.widgets[field].set_value(new_value) + except Exception as e: + _logger.error(f"error setting will_settings_widgets[{field}].set_value({new_value})") + if update_all or update_heirs_dialog: + try: + self.heir_list_widget.will_settings_widget.widgets[field].set_value(new_value) + except Exception as e: + _logger.error(f"error updating settings widget {e}") def init_heirs_to_locktime(self, multiverse=False): for heir in self.heirs: @@ -785,19 +750,22 @@ class BalWindow(Logger): ) if self.date_to_check < datetime.now().timestamp(): raise CheckAliveException(self.date_to_check) + self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get()) + except Exception as e: - self.logger.error(f"init_class_variables: {e}") + _logger.error(f"init_class_variables: {e}") + raise e def build_inheritance_transaction(self, ignore_duplicate=True, keep_original=True): try: if self.disable_plugin: - self.logger.info("plugin is disabled") + _logger.info("plugin is disabled") return if not self.heirs: - self.logger.warning("not heirs {}".format(self.heirs)) + _logger.warning("not heirs {}".format(self.heirs)) return try: self.init_class_variables() @@ -823,7 +791,7 @@ class BalWindow(Logger): return if not self.no_willexecutor: f = False - for k, we in self.willexecutors.items(): + for _k, we in self.willexecutors.items(): if Willexecutors.is_selected(we): f = True if not f: @@ -840,7 +808,7 @@ class BalWindow(Logger): except NoHeirsException: return except NotCompleteWillException as e: - self.logger.info("{}:{}".format(type(e), e)) + _logger.info("{}:{}".format(type(e), e)) message = False if isinstance(e, HeirChangeException): message = "Heirs changed:" @@ -858,12 +826,12 @@ class BalWindow(Logger): f"{_(message)}:\n {e}\n{_('will have to be built')}" ) - self.logger.info("build will") + _logger.info("build will") self.build_will(ignore_duplicate, keep_original) try: self.check_will() - for wid, w in self.willitems.items(): + for wid, _w in self.willitems.items(): self.wallet.set_label(wid, "BAL Transaction") except WillExpiredException as e: self.invalidate_will() @@ -903,7 +871,7 @@ class BalWindow(Logger): read_QIcon_from_bytes(self.bal_plugin.read_file("icons/bal32x32.png")) ) except SerializationError as e: - self.logger.error("unable to deserialize the transaction") + _logger.error("unable to deserialize the transaction") parent.show_critical( _("Electrum was unable to deserialize the transaction:") + "\n" + str(e) ) @@ -934,8 +902,7 @@ class BalWindow(Logger): self.show_message(_("No transactions to invalidate")) def on_failure(exec_info): - self.show_error(f"ERROR:{exec_info}") - + log_error(exec_info,self.bal_window) 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") @@ -1037,9 +1004,8 @@ class BalWindow(Logger): except Exception as e: raise e - def on_failure(exc_info): - self.logger.info("sign fail: {}".format(exc_info)) - self.show_error(exc_info) + def on_failure(exec_info): + log_error(exec_info,self.bal_window) password = self.get_wallet_password() task = partial(self.sign_transactions, password) @@ -1053,27 +1019,28 @@ class BalWindow(Logger): def on_success(sulcess): self.will_list.update() if sulcess: - self.logger.info("error, some transaction was not sent") + _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") + _logger.debug("OK, sulcess transaction was sent") self.show_message( _("All transactions are broadcasted to respective Will-Executors") ) - def on_failure(err): - a,b,c = err - self.logger.error(f"fail to broadcast transactions:{err}") - self.logger.error(f"error: {b}") - self.logger.error("traceback ") - tb = c - while tb is not None: - frame = tb.tb_frame - self.logger.error("file:", frame.f_code.co_filename) - self.logger.error("name:", frame.f_code.co_name) - self.logger.error("line:", tb.tb_lineno) - self.logger.error("lasti:", tb.tb_lasti) - tb = tb.tb_next + def on_failure(exec_info): + log_error(exec_info,self.bal_window) + #a,b,c = err + #_logger.error(f"fail to broadcast transactions:{err}") + #_logger.error(f"error: {b}") + #_logger.error("traceback ") + #tb = c + #while tb is not None: + # frame = tb.tb_frame + # _logger.error("file:", frame.f_code.co_filename) + # _logger.error("name:", frame.f_code.co_name) + # _logger.error("line:", tb.tb_lineno) + # _logger.error("lasti:", tb.tb_lasti) + # tb = tb.tb_next task = partial(self.push_transactions_to_willexecutors, force) msg = _("Selecting Will-Executors") @@ -1119,7 +1086,7 @@ class BalWindow(Logger): w = self.willitems[wid] w.set_check_willexecutor( Willexecutors.check_transaction( - wid, + wid, w.we["url"] ) ) @@ -1142,7 +1109,7 @@ class BalWindow(Logger): def export_will(self): try: - Util.export_meta_gui(self.window, _("will.json"), self.export_json_file) + export_meta_gui(self.window, "will.json", self.export_json_file) except Exception as e: self.show_error(str(e)) raise e @@ -1184,9 +1151,10 @@ class BalWindow(Logger): self.update_all() pass - def on_failure(e): - self.logger.error(f"error checking transactions {e}") - pass + def on_failure(exec_info): + log_error(exec_info,self.bal_window) + #_logger.error(f"error checking transactions {e}") + #pass task = partial(self.check_transactions_task, will) msg = _("Check Transaction") @@ -1195,8 +1163,33 @@ class BalWindow(Logger): ) self.waiting_dialog.exe() + def update_willexecutor_list_widget(self,parent,willexecutors): + try: + parent.willexecutors_list.update(willexecutors) + parent.will_executor_list_widget.update() + except Exception as e: + _logger.error(f"impossible to update will_executor_list_widget {e}") + self.will_executors.update() + + def download_list(self,willexecutors,fn_on_success,fn_on_failure=None): + + def on_success(result): + #self.willexecutors.update(result) + fn_on_success(result) + def on_failure(exec_info): + fn_on_failure(exec_info) + + if not fn_on_failure: + fn_on_success=log_error + welist_server = self.bal_plugin.WELIST_SERVER.get() + task = partial(Willexecutors.download_list,willexecutors,welist_server) + msg = _(f"Downloadinf willexecutors list from {welist_server}") + 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") + _logger.info("ping willexecutots task") pinged = [] failed = [] @@ -1227,26 +1220,15 @@ class BalWindow(Logger): else: pinged.append(url) - def ping_willexecutors(self, wes, parent=None): - if not parent: - parent = self - + def ping_willexecutors(self, wes, fn_on_success,fn_on_failure=None): def on_success(result): - # del self.waiting_dialog - try: - parent.will_executor_list_widget.update() - except Exception: - pass - try: - parent.willexecutors_list.update() - except Exception: - pass + fn_on_success(result) - def on_failure(e): - self.logger.error(f"fail to ping willexecutors: {e}") - pass - - self.logger.info("ping willexecutors") + def on_failure(exec_info): + fn_on_failure(exec_info) + if not fn_on_failure: + fn_on_failure=log_error + _logger.info("ping willexecutors") task = partial(self.ping_willexecutors_task, wes) msg = _("Ping Will-Executors") self.waiting_dialog = BalWaitingDialog( @@ -1281,16 +1263,49 @@ def add_widget(grid, label, widget, row, help_): grid.addWidget(HelpButton(help_), row, 2) + +class BalTxFeesWidget(QWidget): + valueChanged = pyqtSignal() + def __init__(self,bal_window,parent,value=None): + super().__init__(parent) + self.bal_window=bal_window + layout = QHBoxLayout(self) + self.txfee_widget=QSpinBox(self) + self.txfee_widget.setMinimum(1) + self.txfee_widget.setMaximum(10000) + value = value if value else self.bal_window.will_settings['baltx_fees'] + self.txfee_widget.setValue(value) + self.txfee_widget.valueChanged.connect(self.on_heir_tx_fees) + layout.addWidget(QLabel("Tx Fees:")) + layout.addWidget(self.txfee_widget) + + def get_value(self): + return self.txfee_widget.value() + + def set_value(self,value): + if not value: + value=self.bal_window.bal_plugin.default_will_settings()['baltx_fees'] + return self.txfee_widget.setValue(value) + + def on_heir_tx_fees(self,update_all=False): + try: + if update_all: + self.bal_window.update_setting_widgets(self.get_value(),'baltx_fees',True) + except Exception as e: + _logger.error("error while trying to update txfees",e ) + + class _LockTimeEditor: min_allowed_value = NLOCKTIME_MIN max_allowed_value = NLOCKTIME_MAX - def get_locktime(self) -> Optional[int]: + def get_value(self) -> Optional[int]: raise NotImplementedError() - def set_locktime(self, x: Any, force=True) -> None: + def set_value(self, x: Any, force=True) -> None: raise NotImplementedError() + @classmethod def is_acceptable_locktime(cls, x: Any) -> bool: if not x: # e.g. empty string return True @@ -1299,21 +1314,38 @@ class _LockTimeEditor: except Exception: return False return cls.min_allowed_value <= x <= cls.max_allowed_value + @staticmethod + 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 HeirsLockTimeEdit(QWidget, _LockTimeEditor): +class BalTimeEditWidget(QWidget, _LockTimeEditor): valueEdited = pyqtSignal() - locktime_threshold = 50000000 + _setting_locktime = False + _current_value = None - def __init__(self, parent=None, default_index=1): - QWidget.__init__(self, parent) + help_text = "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" + label_text ="locktime" + + def __init__(self, bal_window, parent, default_locktime=None): + super().__init__(parent) + self.bal_window=bal_window hbox = QHBoxLayout() self.setLayout(hbox) hbox.setContentsMargins(0, 0, 0, 0) hbox.setSpacing(0) - - self.locktime_raw_e = LockTimeRawEdit(self, time_edit=self) + self.setMinimumWidth(50*char_width_in_lineedit()) + self.locktime_raw_e = TimeRawEditWidget(self, time_edit=self) self.locktime_date_e = LockTimeDateEdit(self, time_edit=self) self.editors = [self.locktime_raw_e, self.locktime_date_e] @@ -1324,65 +1356,113 @@ class HeirsLockTimeEdit(QWidget, _LockTimeEditor): 1: self.locktime_date_e, } self.combo.addItems(options) - - self.editor = self.option_index_to_editor_map[default_index] + default_index = 0 + if not default_locktime: + default_locktime = self.bal_window.will_settings[self.base_field] + try: + int(default_locktime) + default_index=1 + except Exception as e: + default_index=0 + hbox.addWidget(QLabel(self.label_text)) + hbox.addWidget(HelpButton(self.help_text)) self.combo.currentIndexChanged.connect(self.on_current_index_changed) + self.editor = self.option_index_to_editor_map[default_index] + self.editor.set_value(default_locktime, force=False) 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.valueEdited.connect(lambda: self.update_will_settings(True)) 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): + + + def update_will_settings(self,update_all=False,update_will_dialog=False,update_heirs_dialog=False,): + self.bal_window.update_setting_widgets(self.get_value(),self.base_field,update_all,update_will_dialog,update_heirs_dialog) + + def on_current_index_changed(self, i,force=False): for w in self.editors: w.setVisible(False) w.setEnabled(False) - #prev_locktime = self.editor.get_locktime() + #prev_locktime = self.editor.get_value() self.editor = self.option_index_to_editor_map[i] #if self.editor.is_acceptable_locktime(prev_locktime): - # self.editor.set_locktime(prev_locktime, force=False) + # self.editor.set_value(prev_locktime, force=False) self.editor.setVisible(True) self.editor.setEnabled(True) - def get_locktime(self) -> Optional[str]: - return self.editor.get_locktime() + def get_value(self) -> Optional[str]: + val = self.editor.get_value() + return val - def set_index(self, index): + def set_index(self, index,force = False): self.combo.setCurrentIndex(index) - self.on_current_index_changed(index) + self.on_current_index_changed(index,force) - def set_locktime(self, x: Any, force=None) -> None: - if force is None: - force=True + def set_value(self, x: Any, force=None,update_all=False,update_will_dialog=False,update_heirs_dialog=False) -> None: + current_val=self.get_value() + if self._setting_locktime: + return + newtime=x + current = self.get_value() + if x==current: + return + + self._setting_locktime = True try: - int(x) - self.set_index(1) - except Exception: - if isinstance(x,str): - self.set_index(0) - self.editor.set_locktime(x, force=False) + if not newtime: + newtime=self.bal_window.bal_plugin.WILL_SETTINGS.get()[self.base_field] + try: + int(x) + if force is not None: + self.set_index(1,True) + except Exception: + if isinstance(x,str): + if force is not None: + self.set_index(0,True) + self.current_locktime = newtime + self.editor.set_value(newtime, force=False) + self.update_will_settings(update_all,update_will_dialog,update_heirs_dialog) + finally: + self._setting_locktime = False + +class TimeRawEditWidget(QWidget): + editingFinished = pyqtSignal() + def __init__(self,parent,time_edit=None): + super().__init__(parent) + self.editor=LockTimeRawEdit(parent,time_edit) + self.label=QLabel("") + self.label.setFixedWidth(17 * char_width_in_lineedit()) + self.layout=QHBoxLayout(self) + self.layout.addWidget(self.editor) + self.layout.addWidget(self.label) + self.editor.editingFinished.connect(self.editingFinished.emit) + self.get_value=self.editor.get_value + self.set_value=self.editor.set_value class LockTimeRawEdit(QLineEdit, _LockTimeEditor): def __init__(self, parent=None, time_edit=None): QLineEdit.__init__(self, parent) - self.setFixedWidth(14 * char_width_in_lineedit()) + self.setFixedWidth(6 * 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): + @staticmethod + def replace_str(text): return str(text).replace("d", "").replace("y", "").replace("b", "") def checkbdy(self, s, pos, appendix): @@ -1425,72 +1505,33 @@ class LockTimeRawEdit(QLineEdit, _LockTimeEditor): s = self.replace_str(s) + "y" if self.isblocks: s = self.replace_str(s) + "b" - - self.set_locktime(s, force=False) + self.blockSignals(True) + self.setText(s) + self.blockSignals(False) + #self.set_value(s, force=False) + self._current_value = s # 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]: + def get_absolute_value(self): + return Util.locktime_to_str(Util.parse_locktime_string(self.get_value())) + + def get_value(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 + def set_value(self, x: Any, force=True) -> None: + self.setText(str(x)) + self.numbify() class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor): min_allowed_value = NLOCKTIME_BLOCKHEIGHT_MAX + 1 - max_allowed_value = get_max_allowed_timestamp() + max_allowed_value = _LockTimeEditor.get_max_allowed_timestamp() def __init__(self, parent=None, time_edit=None): QDateTimeEdit.__init__(self, parent) @@ -1499,12 +1540,12 @@ class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor): self.setDateTime(QDateTime.currentDateTime()) self.time_edit = time_edit - def get_locktime(self) -> Optional[int]: + def get_value(self) -> Optional[int]: dt = self.dateTime().toPyDateTime() locktime = int(time.mktime(dt.timetuple())) return locktime - def set_locktime(self, x: Any, force=False) -> None: + def set_value(self, x: Any, force=False) -> None: if not self.is_acceptable_locktime(x): self.setDateTime(QDateTime.currentDateTime()) return @@ -1515,14 +1556,155 @@ class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor): return dt = datetime.fromtimestamp(x) self.setDateTime(dt) + self.alarm=dt + +class ThresholdTimeWidget(BalTimeEditWidget): + help_text = ("Check to ask for invalidation.\n\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\n" + f"{BalTimeEditWidget.help_text}") + label_text="Check Alive:" + base_field = "threshold" + +class LockTimeWidget(BalTimeEditWidget): + help_text = ("Set Locktime for transactions.\n" + "Any time is needed transaction will be anticipated by 1day\n" + f"{BalTimeEditWidget.help_text}") + label_text = "Delivery Time:" + base_field = "locktime" + +class WillSettingsWidget(QWidget): + widgets={} + def __init__(self,bal_window,parent,layout_type='h'): + super().__init__(parent) + self.bal_window=bal_window + box=QHBoxLayout(self) if layout_type=='h' else QVBoxLayout(self) + + self.calendar_button=QPushButton(_("Calendar")) + self.calendar_button.setIcon(read_QIcon_from_bytes(self.bal_window.bal_plugin.read_file("icons/calendar.png"))) + self.calendar_button.clicked.connect(self.open_or_save_calendar) + self.widgets['locktime']=LockTimeWidget(bal_window,self) + self.widgets['threshold']= ThresholdTimeWidget(bal_window,self) + self.widgets['baltx_fees'] =BalTxFeesWidget(bal_window,self) + box.addWidget(self.widgets['locktime']) + box.addWidget(self.widgets['threshold']) + box.addWidget(self.calendar_button) + box.addWidget(self.widgets['baltx_fees']) + self.widgets['locktime'].valueEdited.connect(self.on_locktime_change) + self.widgets['threshold'].valueEdited.connect(self.on_locktime_change) + self.on_locktime_change() + + @staticmethod + def write_temp_ics(content): + fd, path = tempfile.mkstemp(prefix="event_", suffix=".ics") + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(content) + return path + + def open_with_default_app(self,path): + try: + subprocess.check_call([self.bal_window.bal_plugin.CALENDAR_APP.get(), path]) + return True + except Exception as e: + return False + @staticmethod + def save_to_cwd(path, filename="evento.ics"): + target = os.path.abspath(filename) + # se il file esiste, sovrascrive + with open(path, "rb") as src, open(target, "wb") as dst: + dst.write(src.read()) + return target + + @staticmethod + def format_time(time): + return time.astimezone(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + + @staticmethod + def ical_escape(text: str) -> str: + # escape per RFC5545: backslash, ; , newlines + text = text.replace("\\", "\\\\").replace(";", r"\;").replace(",", r"\,").replace("\n", r"\n") + return WillSettingsWidget.fold_ical_line(text) + + @staticmethod + def fold_ical_line(line: str, limit: int = 75) -> str: + # ritorna linee separate da CRLF e folding con spazio iniziale sulle righe successive + encoded = line.encode("utf-8") + parts = [] + while len(encoded) > limit: + # taglia senza spezzare byte UTF-8 + cut = limit + while (encoded[cut] & 0xC0) == 0x80: # byte di continuazione UTF-8 + cut -= 1 + parts.append(encoded[:cut].decode("utf-8")) + encoded = encoded[cut:] + parts.append(encoded.decode("utf-8")) + return "\r\n ".join(parts) -_NOT_GIVEN = object() # sentinel value + def open_or_save_calendar(self): + now = self.format_time(datetime.now()) + + alarm_start = self.format_time(self.widgets['threshold'].alarm) + alarm_end = self.format_time(self.widgets['locktime'].alarm) + + heirs_details = "\n".join(f"{heir} - {self.bal_window.heirs[heir][1]}" for heir in self.bal_window.heirs) + event_description = self.ical_escape(f"Your will for wallet {self.bal_window.wallet} is going to expire\n{heirs_details}") + uid = f"{int(datetime.now().timestamp())}-{os.getpid()}@local" # UID univoco semplice + summary = self.ical_escape(f"Bitcoin After Life {self.bal_window.wallet} expiry") + lines = [ + "BEGIN:VCALENDAR", + "VERSION:2.0", + "PRODID:-//Example//EN", + "BEGIN:VEVENT", + f"UID:{uid}", + f"DTSTAMP:{now}", + f"DTSTART:{alarm_start}", + f"DTEND:{alarm_end}", + f"SUMMARY:{summary}", + f"DESCRIPTION:{event_description}", + "END:VEVENT", + "END:VCALENDAR", + ] + + ics_content = "\r\n".join(lines) + "\r\n" + temp_path = self.write_temp_ics(ics_content) + opened = self.open_with_default_app(temp_path) + if opened: + _logger.info(f"File opened with default app: {temp_path}") + else: + export_meta_gui(self.bal_window.window, f"{self.base_field}_event.csv", self.save_to_cwd) + + def on_locktime_change(self): + threshold_value = str(self.widgets['threshold'].get_value()) + locktime_value = str(self.widgets['locktime'].get_value()) + + dt=None + if threshold_value[-1] in ['d','y']: + ts=int(threshold_value[:-1]) + min_locktime = min(Will.get_min_locktime(self.bal_window.willitems,NLOCKTIME_MAX),Util.parse_locktime_string(locktime_value)) + threshold_value = ts*365 if threshold_value[-1] == 'y' else ts + dt = datetime.fromtimestamp(min_locktime) + dt = dt - timedelta(days = threshold_value) + self.widgets['threshold'].editor.label.setText(dt.strftime("%Y-%m-%d")) + self.widgets['threshold'].alarm = dt + else: + self.widgets['threshold'].alarm = datetime.fromtimestamp(Util.parse_locktime_string(self.widgets['threshold'].get_value())) + + if locktime_value[-1] in ['d','y']: + lt = int(locktime_value[:-1]) + locktime_value = lt*365 if locktime_value[-1] == 'y' else lt + dt =datetime.now() + dt += timedelta(days= locktime_value) + dt=dt.replace(hour=0,minute=0,second=0,microsecond=0) + self.widgets['locktime'].editor.label.setText(dt.strftime("%Y-%m-%d")) + self.widgets['locktime'].alarm = dt + else: + self.widgets['locktime'].alarm=datetime.fromtimestamp(Util.parse_locktime_string(self.widgets['locktime'].get_value())) class PercAmountEdit(BTCAmountEdit): def __init__( - self, decimal_point, is_int=False, parent=None, *, max_amount=_NOT_GIVEN + self, decimal_point, is_int=False, parent=None, *, max_amount=None ): super().__init__(decimal_point, is_int, parent, max_amount=max_amount) @@ -1590,9 +1772,18 @@ class PercAmountEdit(BTCAmountEdit): class BalDialog(WindowModalDialog): def __init__(self, parent, bal_plugin, title=None, icon="icons/bal32x32.png"): self.parent = parent + self.thread = None WindowModalDialog.__init__(self, parent, title) # WindowModalDialog.__init__(self,parent) self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon))) + def closeEvent(self,event): + self._stopping=True + if self.thread: + self.thread.stop() + def hideEvent(self,event): + self._stopping=True + if self.thread: + self.thread.stop() class BalWizardDialog(BalDialog): @@ -1679,8 +1870,10 @@ class BalWizardDialog(BalDialog): pass def closeEvent(self, event): + self._stopping = True + self.thread.stop() - self.bal_window.heir_list_widget.update_will_settings() + #self.bal_window.heir_list_widget.will_settings_widget.update_will_settings() pass @@ -1834,28 +2027,24 @@ class BalWizardWEDownloadWidget(BalWizardWidget): if index < 2: def on_success(willexecutors): - self.bal_window.willexecutors.update(willexecutors) + def ping_on_success(result): + ping_on_done() + def ping_on_failure(exec_info): + ping_on_done() + def ping_on_done(): + 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 + ) + 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 + self.bal_window.willexecutors,ping_on_success,ping_on_failure ) - def on_failure(fail): - _logger.debug(f"Failed to download willexecutors list {fail}") - pass - - task = partial(Willexecutors.download_list,self.bal_window.willexecutors) - msg = _("Downloading Will-Executors list") - self.waiting_dialog = BalWaitingDialog( - self.bal_window, msg, task, on_success, on_failure, exe=False - ) - self.waiting_dialog.exe() + self.bal_window.download_list(self.bal_window.willexecutors,on_success) elif index == 3: # TODO DO NOTHING @@ -1896,92 +2085,11 @@ class BalWizardLocktimeAndFeeWidget(BalWizardWidget): def get_content(self): widget = QWidget() - self.heir_locktime = HeirsLockTimeEdit(widget, 0) - #will_settings = self.bal_window.bal_plugin.WILL_SETTINGS.get() - will_settings = self.bal_window.will_settings - self.heir_locktime.set_locktime(will_settings["locktime"]) - - def on_heir_locktime(): - if not self.heir_locktime.get_locktime(): - self.heir_locktime.set_locktime("1y") - self.bal_window.update_locktime_widgets(self.heir_locktime.get_locktime()) - - 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.update_threshold_widgets(self.heir_threshold.get_locktime()) - - 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"), - ) - ) + layout.addWidget(LockTimeWidget(self.bal_window,widget)) + layout.addWidget(ThresholdTimeWidget(self.bal_window,widget)) + layout.addWidget(BalTxFeesWidget(self.bal_window,widget)) spacer_widget = QWidget() spacer_widget.setSizePolicy( @@ -2053,6 +2161,7 @@ class BalWaitingDialog(BalDialog): return self.message_label.text() def closeEvent(self, event): + self._stopping = True self.thread.stop() @@ -2573,7 +2682,7 @@ class BalBuildWillDialog(BalDialog): # self.qwidget.adjustSize() # from PyQt6.QtWidgets import QApplication # QApplication.processEvents() - # + # # self.adjustSize() def msg_update(self): full_text = "

".join(self.labels).replace("\n", "
") @@ -2581,7 +2690,7 @@ class BalBuildWillDialog(BalDialog): self.message_label.adjustSize() #self.setMinimumHeight(len(self.labels)*40) self.resize(self.sizeHint()) - + def get_text(self): return self.message_label.text() @@ -2610,7 +2719,7 @@ class HeirListWidget(MyTreeView, MessageBoxMixin): def createEditor(self, parent, option, index): return QLineEdit(parent) def setEditorData(self, editor, index): - editor.setText(index.data()) + editor.setText(index.data()) def setModelData(self, editor, model, index): model.setData(index, editor.text()) @@ -2752,7 +2861,6 @@ class HeirListWidget(MyTreeView, MessageBoxMixin): # 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 @@ -2768,98 +2876,19 @@ class HeirListWidget(MyTreeView, MessageBoxMixin): menu.addAction(_("Import"), self.bal_window.import_heirs) menu.addAction(_("Export"), lambda: self.bal_window.export_heirs()) - threshold = self.bal_window.will_settings.get('threshold',None) if self.bal_window.will_settings else self.bal_window.window.wallet.db.get_dict("will_settings")['threshold'] - locktime = self.bal_window.will_settings.get('locktime',None) if self.bal_window.will_settings else self.bal_window.window.wallet.db.get_dict("will_settings")['locktime'] + widget = QWidget() + layout = QHBoxLayout(widget) + self.will_settings_widget=WillSettingsWidget(self.bal_window,widget) - 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.update_locktime_widgets(self.heir_locktime.get_locktime()) - - self.heir_locktime.valueEdited.connect(on_heir_locktime) - - self.heir_locktime.set_locktime(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.update_threshold_widgets(self.heir_threshold.get_locktime()) - - self.heir_threshold.valueEdited.connect(on_heir_threshold) - self.heir_threshold.set_locktime(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(" ")) + layout.addWidget(self.will_settings_widget) newHeirButton = QPushButton(_("New Heir")) newHeirButton.clicked.connect(self.bal_window.new_heir_dialog) layout.addWidget(newHeirButton) - toolbar.insertWidget(2, self.heirs_widget) + toolbar.insertWidget(2, widget) return toolbar - def update_will_settings(self): - try: - self.heir_locktime.set_locktime(self.bal_window.will_settings["locktime"]) - self.heir_threshold.set_locktime(self.bal_window.will_settings["threshold"]) - self.heir_tx_fees.setValue(int(self.bal_window.will_settings["baltx_fees"])) - - except Exception as e: - pass - _logger.debug(f"Exception update_will_settings {e}") - def build_transactions(self): # will = self.bal_window.prepare_will() self.bal_window.prepare_will() @@ -3074,21 +3103,6 @@ class PreviewList(MyTreeView): menu.addAction(_("Check"), self.check) menu.addAction(_("Invalidate"), self.invalidate_will) - 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 - - wizard = QPushButton(_("Setup Wizard")) wizard.clicked.connect(self.bal_window.init_wizard) #display = QPushButton(_("Display")) @@ -3099,32 +3113,9 @@ class PreviewList(MyTreeView): widget = QWidget() hlayout = QHBoxLayout(widget) - hlayout.addWidget(QLabel(_("Check Alive:"))) + self.will_settings_widget=WillSettingsWidget(self.bal_window,widget) - threshold = self.bal_window.will_settings.get('threshold',None) if self.bal_window.will_settings else self.bal_window.window.wallet.db.get_dict("will_settings")['threshold'] - - self.heir_threshold = HeirsLockTimeEdit(widget, 0) - self.heir_threshold.set_locktime(threshold) - - def on_heir_threshold(): - if not self.heir_threshold.get_locktime(): - self.heir_threshold.set_locktime("180d") - self.bal_window.update_threshold_widgets(self.heir_threshold.get_locktime()) - - self.heir_threshold.valueEdited.connect(on_heir_threshold) - hlayout.addWidget(self.heir_threshold) - hlayout.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" - ) - ) - ) + hlayout.addWidget(self.will_settings_widget) hlayout.addWidget(wizard) hlayout.addWidget(refresh) toolbar.insertWidget(2, widget) @@ -3598,6 +3589,7 @@ class WillExecutorListWidget(MyTreeView): self.parent.willexecutors_list[edit_key]["address"] = text if col == self.Columns.INFO: self.parent.willexecutors_list[edit_key]["info"] = text + self.parent.save_willexecutors() self.update() except Exception: pass @@ -3684,7 +3676,10 @@ class WillExecutorListWidget(MyTreeView): set_current = QPersistentModelIndex(idx) self.set_current_idx(set_current) self.filter() - self.parent.save_willexecutors() + #try: + # self.parent.save_willexecutors() + #except Exception as e: + # print("exception saving willexecutors",e) except Exception as e: _logger.error(f"error updating willexcutor {e}") @@ -3755,13 +3750,14 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): } self.will_executor_list_widget.update() - def download_list(self): - self.willexecutors_list.update(Willexecutors.download_list(self.bal_window.willexecutors)) - self.will_executor_list_widget.update() + def download_list(self,wes=None): + if not wes: + wes=self.willexecutors_list + self.bal_window.download_list(wes,self.save_willexecutors) def export_file(self, path): - Util.export_meta_gui( - self.bal_window.window, _("willexecutors.json"), self.export_json_file + export_meta_gui( + self.bal_window.window, "willexecutors.json", self.export_json_file ) def export_json_file(self, path): @@ -3777,10 +3773,8 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): def update_willexecutors(self, wes=None): if not wes: - wes = self.willexecutors_list - self.bal_window.ping_willexecutors(wes, self.parent) - self.willexecutors_list.update(wes) - self.will_executor_list_widget.update() + wes=self.willexecutors_list + self.bal_window.ping_willexecutors(wes,self.save_willexecutors) def import_json_file(self, path): data = read_json_file(path) @@ -3792,7 +3786,11 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): def _validate(self, data): return data - def save_willexecutors(self): + def save_willexecutors(self,wes=None): + if not wes: + wes=self.willexecutors_list + self.willexecutors_list.update(wes) + self.will_executor_list_widget.update() Willexecutors.save(self.bal_window.bal_plugin, self.willexecutors_list) @@ -3837,3 +3835,42 @@ class CheckAliveException(Exception): self.timestamp_to_check = timestamp_to_check def __str__(self): return "Check alive expired please update it: {}".format(datetime.fromtimestamp(self.timestamp_to_check).isoformat()) + +def log_error(exec_info,window=None): + _logger.error(exec_info) + tb=traceback.format_exc() + while tb is not None: + try: + frame = tb.tb_frame + _logger.error("file:", frame.f_code.co_filename) + _logger.error("name:", frame.f_code.co_name) + _logger.error("line:", tb.tb_lineno) + _logger.error("lasti:", tb.tb_lasti) + tb = tb.tb_next + except Exception as e: + _logger.error(tb,e) + tb= None + break + if window is not None: + window.show_error(exec_info) + +def export_meta_gui(electrum_window, title, exporter): + filter_ = "All files (*)" + filename = getSaveFileName( + parent=electrum_window, + title=_("Select file to save your {}".format(title)), + filename="BALplugin_{}_{}_{}".format(BalPlugin.chainname,str(electrum_window.wallet),title), + filter=filter_, + config=electrum_window.config, + ) + if not filename: + return + try: + exporter(filename) + except FileExportFailed as e: + electrum_window.show_critical(str(e)) + else: + electrum_window.show_message( + _("Your {0} were exported to '{1}'".format(title, str(filename))) + ) + diff --git a/util.py b/util.py index af79d28..df513ad 100644 --- a/util.py +++ b/util.py @@ -1,15 +1,13 @@ import bisect from datetime import datetime, timedelta -from electrum.gui.qt.util import getSaveFileName -from electrum.i18n import _ from electrum.transaction import PartialTxOutput -from electrum.util import FileExportFailed LOCKTIME_THRESHOLD = 500000000 class Util: + @staticmethod def locktime_to_str(locktime): try: locktime = int(locktime) @@ -21,6 +19,7 @@ class Util: pass return str(locktime) + @staticmethod def str_to_locktime(locktime): try: if locktime[-1] in ("y", "d", "b"): @@ -33,6 +32,7 @@ class Util: timestamp = dt_object.timestamp() return int(timestamp) + @staticmethod def parse_locktime_string(locktime, w=None): try: return int(locktime) @@ -60,6 +60,7 @@ class Util: pass return 0 + @staticmethod def int_locktime(seconds=0, minutes=0, hours=0, days=0, blocks=0): return int( seconds @@ -69,6 +70,7 @@ class Util: + blocks * 600 ) + @staticmethod def encode_amount(amount, decimal_point): if Util.is_perc(amount): return amount @@ -78,6 +80,7 @@ class Util: except Exception: return 0 + @staticmethod def decode_amount(amount, decimal_point): if Util.is_perc(amount): return amount @@ -88,12 +91,14 @@ class Util: except Exception: return str(amount) + @staticmethod def is_perc(value): try: return value[-1] == "%" except Exception: return False + @staticmethod def cmp_array(heira, heirb): try: if len(heira) != len(heirb): @@ -105,11 +110,13 @@ class Util: except Exception: return False + @staticmethod def cmp_heir(heira, heirb): if heira[0] == heirb[0] and heira[1] == heirb[1]: return True return False + @staticmethod def cmp_willexecutor(willexecutora, willexecutorb): if willexecutora == willexecutorb: return True @@ -124,6 +131,7 @@ class Util: return False return False + @staticmethod def search_heir_by_values(heirs, heir, values): for h, v in heirs.items(): found = False @@ -135,12 +143,14 @@ class Util: return h return False + @staticmethod def cmp_heir_by_values(heira, heirb, values): for v in values: if heira[v] != heirb[v]: return False return True + @staticmethod def cmp_heirs_by_values( heirsa, heirsb, values, exclude_willexecutors=False, reverse=True ): @@ -165,6 +175,7 @@ class Util: else: return True + @staticmethod def cmp_heirs( heirsa, heirsb, @@ -187,6 +198,7 @@ class Util: raise e return False + @staticmethod def cmp_inputs(inputsa, inputsb): if len(inputsa) != len(inputsb): return False @@ -195,6 +207,7 @@ class Util: return False return True + @staticmethod def cmp_outputs(outputsa, outputsb, willexecutor_output=None): if len(outputsa) != len(outputsb): return False @@ -204,6 +217,7 @@ class Util: return False return True + @staticmethod def cmp_txs(txa, txb): if not Util.cmp_inputs(txa.inputs(), txb.inputs()): return False @@ -211,6 +225,7 @@ class Util: return False return True + @staticmethod def get_value_amount(txa, txb): outputsa = txa.outputs() # outputsb = txb.outputs() @@ -229,6 +244,7 @@ class Util: return value_amount + @staticmethod def chk_locktime(timestamp_to_check, block_height_to_check, locktime): # TODO BUG: WHAT HAPPEN AT THRESHOLD? locktime = int(locktime) @@ -239,6 +255,7 @@ class Util: else: return False + @staticmethod def anticipate_locktime(locktime, blocks=0, hours=0, days=0): locktime = int(locktime) out = 0 @@ -255,6 +272,7 @@ class Util: out = 1 return out + @staticmethod def cmp_locktime(locktimea, locktimeb): if locktimea == locktimeb: return 0 @@ -268,17 +286,20 @@ class Util: else: return int(locktimea) - (locktimeb) + @staticmethod def get_lowest_valid_tx(available_utxos, will): will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime) for txid, willitem in will.items(): pass + @staticmethod def get_locktimes(will): locktimes = {} for txid, willitem in will.items(): locktimes[willitem["tx"].locktime] = True return locktimes.keys() + @staticmethod def get_lowest_locktimes(locktimes): sorted_timestamp = [] sorted_block = [] @@ -291,18 +312,22 @@ class Util: return sorted(sorted_timestamp), sorted(sorted_block) + @staticmethod def get_lowest_locktimes_from_will(will): return Util.get_lowest_locktimes(Util.get_locktimes(will)) + @staticmethod def search_willtx_per_io(will, tx): for wid, w in will.items(): if Util.cmp_txs(w["tx"], tx["tx"]): return wid, w return None, None + @staticmethod def invalidate_will(will): raise Exception("not implemented") + @staticmethod def get_will_spent_utxos(will): utxos = [] for txid, willitem in will.items(): @@ -310,6 +335,7 @@ class Util: return utxos + @staticmethod def utxo_to_str(utxo): try: return utxo.to_str() @@ -321,6 +347,7 @@ class Util: pass return str(utxo) + @staticmethod def cmp_utxo(utxoa, utxob): utxoa = Util.utxo_to_str(utxoa) utxob = Util.utxo_to_str(utxob) @@ -329,21 +356,25 @@ class Util: else: return False + @staticmethod def in_utxo(utxo, utxos): for s_u in utxos: if Util.cmp_utxo(s_u, utxo): return True return False + @staticmethod def txid_in_utxo(txid, utxos): for s_u in utxos: if s_u.prevout.txid == txid: return True return False + @staticmethod def cmp_output(outputa, outputb): return outputa.address == outputb.address and outputa.value == outputb.value + @staticmethod def in_output(output, outputs): for s_o in outputs: if Util.cmp_output(s_o, output): @@ -355,6 +386,7 @@ class Util: # return true false same amount different address # return false false different amount, different address not found + @staticmethod def din_output(out, outputs): same_amount = [] for s_o in outputs: @@ -370,6 +402,7 @@ class Util: else: return False, False + @staticmethod def get_change_output(wallet, in_amount, out_amount, fee): change_amount = int(in_amount - out_amount - fee) if change_amount > wallet.dust_threshold(): @@ -380,6 +413,7 @@ class Util: out.is_change = True return out + @staticmethod def get_current_height(network): # if no network or not up to date, just set locktime to zero if not network: @@ -401,6 +435,7 @@ class Util: height = min(chain_height, server_height) return height + @staticmethod def print_var(var, name="", veryverbose=False): print(f"---{name}---") if var is not None: @@ -435,6 +470,7 @@ class Util: print(f"---end {name}---") + @staticmethod def print_utxo(utxo, name=""): print(f"---utxo-{name}---") Util.print_var(utxo, name) @@ -446,36 +482,20 @@ class Util: print("_TxInput__value_sats:", utxo._TxInput__value_sats) print(f"---utxo-end {name}---") + @staticmethod def print_prevout(prevout, name=""): print(f"---prevout-{name}---") Util.print_var(prevout, f"{name}-prevout") Util.print_var(prevout._asdict()) print(f"---prevout-end {name}---") - def export_meta_gui(electrum_window, title, exporter): - filter_ = "All files (*)" - filename = getSaveFileName( - parent=electrum_window, - title=_("Select file to save your {}".format(title)), - filename="BALplugin_{}".format(title), - filter=filter_, - config=electrum_window.config, - ) - if not filename: - return - try: - exporter(filename) - except FileExportFailed as e: - electrum_window.show_critical(str(e)) - else: - electrum_window.show_message( - _("Your {0} were exported to '{1}'".format(title, str(filename))) - ) + @staticmethod def copy(dicto, dictfrom): for k, v in dictfrom.items(): dicto[k] = v + @staticmethod def fix_will_settings_tx_fees(will_settings): tx_fees = will_settings.get("tx_fees", False) have_to_update = False @@ -485,6 +505,7 @@ class Util: have_to_update = True return have_to_update + @staticmethod def fix_will_tx_fees(will): have_to_update = False for txid, willitem in will.items(): @@ -495,15 +516,18 @@ class Util: have_to_update = True return have_to_update + @staticmethod def text_to_hex(text: str) -> str: """Convert text to hexadecimal string""" hex_string = text.encode('utf-8').hex() return hex_string + @staticmethod def hex_to_text(hex_string: str) -> str: """Convert hexadecimal string back to text (for verification)""" try: return bytes.fromhex(hex_string).decode('utf-8') except Exception: return "Error: Invalid hex string" + diff --git a/wallet_util/README.md b/wallet_util/README.md new file mode 100644 index 0000000..0d8c02f --- /dev/null +++ b/wallet_util/README.md @@ -0,0 +1,50 @@ +## README + +### Overview +This tool provides two entry points: a CLI script (bal_wallet_utils.py) and a Qt GUI script (bal_wallet_utils_qt.py) that operate against an Electrum source tree. + +### Installation / Preparation +1. Copy both files into the Electrum project root (the folder that contains the Electrum source package): + - bal_wallet_utils.py + - bal_wallet_utils_qt.py + +2. Activate the Electrum Python environment (the virtualenv used to run Electrum). Example (PowerShell, adjust path to your venv): +``` +.\env\Scripts\Activate.ps1 +``` +or (cmd): +``` +env\Scripts\activate.bat +``` + +### Running +- CLI version: +``` +python bal_wallet_utils.py +``` +- Qt GUI version: +``` +python bal_wallet_utils_qt.py +``` + +### Building a Windows executable with PyInstaller +From the project root (with the Electrum environment active), you can build the Qt executable using PyInstaller. Example command (adjust the paths if your environment path differs): +``` +pyinstaller.exe --onefile --noconsole --add-data "electrum\currencies.json;electrum" --add-data "electrum\bip39_wallet_formats.json;electrum" --add-data "electrum\lnwire\peer_wire.csv;electrum\lnwire" --add-data "electrum\lnwire\onion_wire.csv;electrum\lnwire" --add-binary "env/Lib/site-packages\electrum_ecc\libsecp256k1-6.dll;electrum_ecc" bal_wallet_utils_qt.py +``` + +Notes: +- Run the command from the project root so relative paths resolve correctly. +- On Windows the --add-data and --add-binary arguments use ";" to separate source and destination. +- If electrum expects additional data files or native DLLs, include them with additional --add-data / --add-binary flags. +- For debugging include --onedir first to inspect the created folder before using --onefile. + +### Troubleshooting +- If PyInstaller is not found, run it via Python: +``` +python -m PyInstaller +``` +- If the frozen exe fails because DLLs or JSON files are missing, add those files explicitly with --add-data or --add-binary. +- Test the build on a clean Windows VM to ensure all runtime dependencies are included. + +License and attribution: include your preferred license or attribution details here. diff --git a/wallet_util/bal_wallet_utils.py b/wallet_util/bal_wallet_utils.py index f97b162..b81e0a0 100755 --- a/wallet_util/bal_wallet_utils.py +++ b/wallet_util/bal_wallet_utils.py @@ -1,10 +1,11 @@ #!env/bin/python3 -from electrum.storage import WalletStorage -import json -from electrum.util import MyEncoder -import sys import getpass +import json import os +import sys + +from electrum.storage import WalletStorage +from electrum.util import MyEncoder default_fees = 100 @@ -26,9 +27,12 @@ def fix_will_settings_tx_fees(json_wallet): def uninstall_bal(json_wallet): - del json_wallet["will_settings"] - del json_wallet["will"] - del json_wallet["heirs"] + if "will_settings" in json_wallet: + del json_wallet["will_settings"] + if "will" in json_wallet: + del json_wallet["will"] + if "heirs" in json_wallet: + del json_wallet["heirs"] return True diff --git a/wallet_util/bal_wallet_utils_qt.py b/wallet_util/bal_wallet_utils_qt.py index e3744ee..4da6b57 100755 --- a/wallet_util/bal_wallet_utils_qt.py +++ b/wallet_util/bal_wallet_utils_qt.py @@ -1,30 +1,31 @@ #!/usr/bin/env python3 -import sys -import os import json +import os +import sys + +from bal_wallet_utils import fix_will_settings_tx_fees, save, uninstall_bal +from electrum.storage import WalletStorage from PyQt6.QtWidgets import ( QApplication, - QMainWindow, - QVBoxLayout, + QFileDialog, + QGroupBox, QHBoxLayout, QLabel, QLineEdit, + QMainWindow, QPushButton, - QWidget, - QFileDialog, - QGroupBox, QTextEdit, + QVBoxLayout, + QWidget, ) -from electrum.storage import WalletStorage -from bal_wallet_utils import fix_will_settings_tx_fees, uninstall_bal, save class WalletUtilityGUI(QMainWindow): def __init__(self): super().__init__() - self.initUI() + self.init_ui() - def initUI(self): + def init_ui(self): self.setWindowTitle("BAL Wallet Utility") self.setFixedSize(500, 400) diff --git a/will.py b/will.py index fcc8b61..eccd8f9 100644 --- a/will.py +++ b/will.py @@ -24,6 +24,7 @@ _logger = get_logger(__name__) class Will: + @staticmethod def get_children(will, willid): out = [] for _id in will: @@ -35,6 +36,7 @@ class Will: return out # build a tree with parent transactions + @staticmethod def add_willtree(will): for willid in will: will[willid].children = Will.get_children(will, willid) @@ -43,14 +45,17 @@ class Will: will[child[0]].father = willid # return a list of will sorted by locktime + @staticmethod def get_sorted_will(will): return sorted(will.items(), key=lambda x: x[1]["tx"].locktime) + @staticmethod def only_valid(will): for k, v in will.items(): if v.get_status("VALID"): yield k + @staticmethod def search_equal_tx(will, tx, wid): for w in will: if w != wid and not tx.to_json() != will[w]["tx"].to_json(): @@ -59,6 +64,7 @@ class Will: return will[w]["tx"] return False + @staticmethod def get_tx_from_any(x): try: a = str(x) @@ -69,6 +75,7 @@ class Will: return x + @staticmethod def add_info_from_will(will, wid, wallet): if isinstance(will[wid].tx, str): will[wid].tx = Will.get_tx_from_any(will[wid].tx) @@ -89,7 +96,9 @@ class Will: txin._TxInput__value_sats = change.value txin._trusted_value_sats = change.value - def normalize_will(will, wallet=None, others_inputs={}): + @staticmethod + def normalize_will(will, wallet=None, others_inputs=None): + others_input = others_inputs if others_inputs is not None else {} to_delete = [] to_add = {} # add info from wallet @@ -138,6 +147,7 @@ class Will: if wid in will: del will[wid] + @staticmethod def new_input(txid, idx, change): prevout = TxOutpoint(txid=bfh(txid), out_idx=idx) inp = PartialTxInput(prevout=prevout) @@ -148,6 +158,7 @@ class Will: inp._TxInput__value_sats = change.value return inp + @staticmethod def check_anticipate(ow: "WillItem", nw: "WillItem"): anticipate = Util.anticipate_locktime(ow.tx.locktime, days=1) if int(nw.tx.locktime) >= int(anticipate): @@ -177,6 +188,7 @@ class Will: return anticipate return 4294967295 + 1 + @staticmethod def change_input(will, otxid, idx, change, others_inputs, to_delete, to_append): ow = will[otxid] ntxid = ow.tx.txid() @@ -217,6 +229,7 @@ class Will: to_append, ) + @staticmethod def get_all_inputs(will, only_valid=False): all_inputs = {} for w, wi in will.items(): @@ -231,6 +244,7 @@ class Will: all_inputs[prevout_str].append(inp) return all_inputs + @staticmethod def get_all_inputs_min_locktime(all_inputs): all_inputs_min_locktime = {} @@ -245,6 +259,7 @@ class Will: return all_inputs_min_locktime + @staticmethod def search_anticipate_rec(will, old_inputs): redo = False to_delete = [] @@ -284,6 +299,7 @@ class Will: Will.search_anticipate_rec(will, old_inputs) + @staticmethod def update_will(old_will, new_will): all_old_inputs = Will.get_all_inputs(old_will, only_valid=True) # all_inputs_min_locktime = Will.get_all_inputs_min_locktime(all_old_inputs) @@ -310,6 +326,7 @@ class Will: else: continue + @staticmethod def get_higher_input_for_tx(will): out = {} for wid in will: @@ -323,6 +340,7 @@ class Will: out[inp.prevout.to_str()] = inp return out + @staticmethod def invalidate_will(will, wallet, fees_per_byte): will_only_valid = Will.only_valid_list(will) inputs = Will.get_all_inputs(will_only_valid) @@ -374,11 +392,13 @@ class Will: _logger.debug("len utxo_to_spend <=0") pass + @staticmethod def is_new(will): for wid, w in will.items(): if w.get_status("VALID") and not w.get_status("COMPLETE"): return True + @staticmethod def search_rai(all_inputs, all_utxos, will, wallet): # will_only_valid = Will.only_valid_or_replaced_list(will) for inp, ws in all_inputs.items(): @@ -412,20 +432,25 @@ class Will: else: pass + @staticmethod def utxos_strs(utxos): return [Util.utxo_to_str(u) for u in utxos] - def set_invalidate(wid, will=[]): + @staticmethod + def set_invalidate(wid, will=None): + will = will if will is not None else {} will[wid].set_status("INVALIDATED", True) if will[wid].children: for c in will[wid].children.items(): Will.set_invalidate(c[0], will) + @staticmethod def check_tx_height(tx, wallet): info = wallet.get_tx_info(tx) return info.tx_mined_status.height() # check if transactions are stil valid tecnically valid + @staticmethod def check_invalidated(willtree, utxos_list, wallet): for wid, w in willtree.items(): if ( @@ -458,6 +483,7 @@ class Will: # if wc.children: # Will.reflect_to_children(wc) + @staticmethod def check_amounts(heirs, willexecutors, all_utxos, timestamp_to_check, dust): fixed_heirs, fixed_amount, perc_heirs, perc_amount, fixed_amount_with_dust = ( heirs.fixed_percent_lists_amount(timestamp_to_check, dust, reverse=True) @@ -481,6 +507,7 @@ class Will: f"Willexecutor{url} excess base fee({wex['base_fee']}), {fixed_amount} >={temp_balance}" ) + @staticmethod def check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check): Will.add_willtree(will) utxos_list = Will.utxos_strs(all_utxos) @@ -497,18 +524,28 @@ class Will: Will.search_rai(all_inputs, all_utxos, will, wallet) + @staticmethod + def get_min_locktime(will,default_value=None): + return min((v.tx.locktime for v in will.values() if v.get_status('VALID')), default=default_value) + + + + @staticmethod def is_will_valid( will, block_to_check, timestamp_to_check, tx_fees, all_utxos, - heirs={}, - willexecutors={}, + heirs=None, + willexecutors=None, self_willexecutor=False, wallet=False, callback_not_valid_tx=None, ): + heirs = heirs if heirs is not None else {} + willexecutors= willexecutors if willexecutors is not None else {} + Will.check_will(will, all_utxos, wallet, block_to_check, timestamp_to_check) if heirs: if not Will.check_willexecutors_and_heirs( @@ -537,6 +574,7 @@ class Will: _logger.info("will ok") return True + @staticmethod def check_will_expired(all_inputs_min_locktime, block_to_check, timestamp_to_check): _logger.info("check if some transaction is expired") for prevout_str, wid in all_inputs_min_locktime.items(): @@ -568,6 +606,7 @@ class Will: # if not parentwill or not parentwill.get_status("VALID"): # w[1].set_status("INVALIDATED", True) + @staticmethod def only_valid_list(will): out = {} for wid, w in will.items(): @@ -575,6 +614,7 @@ class Will: out[wid] = w return out + @staticmethod def only_valid_or_replaced_list(will): out = [] for wid, w in will.items(): @@ -583,6 +623,7 @@ class Will: out.append(wid) return out + @staticmethod def check_willexecutors_and_heirs( will, heirs, willexecutors, self_willexecutor, check_date, tx_fees ): @@ -645,6 +686,7 @@ class Will: return True + class WillItem(Logger): STATUS_DEFAULT = { "ANTICIPATED": ["Anticipated", False], diff --git a/willexecutors.py b/willexecutors.py index b86655d..717f359 100644 --- a/willexecutors.py +++ b/willexecutors.py @@ -1,9 +1,8 @@ import json -from datetime import datetime import time +from datetime import datetime from aiohttp import ClientResponse -from electrum import constants from electrum.i18n import _ from electrum.logging import get_logger from electrum.network import Network @@ -14,11 +13,12 @@ DEFAULT_TIMEOUT = 5 _logger = get_logger(__name__) -chainname = constants.net.NET_NAME if constants.net.NET_NAME != "mainnet" else "bitcoin" +chainname = BalPlugin.chainname class Willexecutors: + @staticmethod def save(bal_plugin, willexecutors): _logger.debug(f"save {willexecutors},{chainname}") aw = bal_plugin.WILLEXECUTORS.get() @@ -27,6 +27,7 @@ class Willexecutors: _logger.debug(f"saved: {aw}") # bal_plugin.WILLEXECUTORS.set(willexecutors) + @staticmethod def get_willexecutors( bal_plugin, update=False, bal_window=False, force=False, task=True ): @@ -78,6 +79,7 @@ class Willexecutors: ) return w_sorted + @staticmethod def is_selected(willexecutor, value=None): if not willexecutor: return False @@ -89,6 +91,7 @@ class Willexecutors: willexecutor["selected"] = False return False + @staticmethod def get_willexecutor_transactions(will, force=False): willexecutors = {} for wid, willitem in will.items(): @@ -124,6 +127,7 @@ class Willexecutors: # willexecutors[url]["txs"], url # ) + @staticmethod def send_request( method, url, data=None, *, timeout=10, handle_response=None, count_reply=0 ): @@ -177,12 +181,14 @@ class Willexecutors: _logger.debug(f"--> {response}") return response + @staticmethod def get_we_url_from_response(resp): url_slices = str(resp.url).split("/") if len(url_slices) > 2: url_slices = url_slices[:-2] return "/".join(url_slices) + @staticmethod async def handle_response(resp: ClientResponse): r = await resp.text() try: @@ -196,9 +202,11 @@ class Willexecutors: pass return r + @staticmethod class AlreadyPresentException(Exception): pass + @staticmethod def push_transactions_to_willexecutor(willexecutor): out = True try: @@ -224,10 +232,12 @@ class Willexecutors: return out + @staticmethod def ping_servers(willexecutors): for url, we in willexecutors.items(): Willexecutors.get_info_task(url, we) + @staticmethod def get_info_task(url, willexecutor): w = None try: @@ -248,7 +258,9 @@ class Willexecutors: willexecutor["last_update"] = datetime.now().timestamp() return willexecutor - def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor={}): + @staticmethod + def initialize_willexecutor(willexecutor, url, status=None, old_willexecutor=None): + old_willexecutor=old_willexecutor if old_willexecutor is not None else {} willexecutor["url"] = url if status is not None: willexecutor["status"] = status @@ -260,11 +272,13 @@ class Willexecutors: - def download_list(old_willexecutors): + @staticmethod + def download_list(old_willexecutors,welist_server): try: + welist_server = welist_server if welist_server[-1] == '/' else welist_server+'/' willexecutors = Willexecutors.send_request( "get", - f"https://welist.bitcoin-after.life/data/{chainname}?page=0&limit=100", + f"{welist_server}data/{chainname}?page=0&limit=100", ) # del willexecutors["status"] for w in willexecutors: @@ -280,6 +294,7 @@ class Willexecutors: _logger.error(f"Failed to download willexecutors list: {e}") return {} + @staticmethod def get_willexecutors_list_from_json(): try: with open("willexecutors.json") as f: @@ -294,6 +309,7 @@ class Willexecutors: return {} + @staticmethod def check_transaction(txid, url): _logger.debug(f"{url}:{txid}") try: @@ -305,53 +321,54 @@ class Willexecutors: _logger.error(f"error contacting {url} for checking txs {e}") raise e + @staticmethod def compute_id(willexecutor): return "{}-{}".format(willexecutor.get("url"), willexecutor.get("chain")) -class WillExecutor: - def __init__( - self, - url, - base_fee, - chain, - info, - version, - status, - is_selected=False, - promo_code="", - ): - self.url = url - self.base_fee = base_fee - self.chain = chain - self.info = info - self.version = version - self.status = status - self.promo_code = promo_code - self.is_selected = is_selected - self.id = self.compute_id() - - def from_dict(d): - return WillExecutor( - url=d.get("url", "http://localhost:8000"), - base_fee=d.get("base_fee", 1000), - chain=d.get("chain", chainname), - info=d.get("info", ""), - version=d.get("version", 0), - status=d.get("status", "Ko"), - is_selected=d.get("is_selected", "False"), - promo_code=d.get("promo_code", ""), - ) - - def to_dict(self): - return { - "url": self.url, - "base_fee": self.base_fee, - "chain": self.chain, - "info": self.info, - "version": self.version, - "promo_code": self.promo_code, - } - - def compute_id(self): - return f"{self.url}-{self.chain}" +#class WillExecutor: +# def __init__( +# self, +# url, +# base_fee, +# chain, +# info, +# version, +# status, +# is_selected=False, +# promo_code="", +# ): +# self.url = url +# self.base_fee = base_fee +# self.chain = chain +# self.info = info +# self.version = version +# self.status = status +# self.promo_code = promo_code +# self.is_selected = is_selected +# self.id = self.compute_id() +# +# def from_dict(d): +# return WillExecutor( +# url=d.get("url", "http://localhost:8000"), +# base_fee=d.get("base_fee", 1000), +# chain=d.get("chain", chainname), +# info=d.get("info", ""), +# version=d.get("version", 0), +# status=d.get("status", "Ko"), +# is_selected=d.get("is_selected", "False"), +# promo_code=d.get("promo_code", ""), +# ) +# +# def to_dict(self): +# return { +# "url": self.url, +# "base_fee": self.base_fee, +# "chain": self.chain, +# "info": self.info, +# "version": self.version, +# "promo_code": self.promo_code, +# } +# +# def compute_id(self): +# return f"{self.url}-{self.chain}"