12 Commits

7 changed files with 430 additions and 125 deletions

View File

@@ -1 +1 @@
0.2.6 0.2.8

27
bal.py
View File

@@ -47,18 +47,19 @@ class BalConfig:
class BalPlugin(BasePlugin): class BalPlugin(BasePlugin):
LATEST_VERSION = "1" _version=None
KNOWN_VERSIONS = ("0", "1") __version__ = "0.2.7" #AUTOMATICALLY GENERATED DO NOT EDIT
assert LATEST_VERSION in KNOWN_VERSIONS def version(self):
if not self._version:
def version(): try:
try: f = ""
f = "" with open("{}/VERSION".format(self.plugin_dir), "r") as fi:
with open("VERSION", "r") as fi: f = str(fi.read())
f = str(fi.readline()) self._version = f.strip()
return f except Exception as e:
except Exception: _logger.error(f"failed to get version: {e}")
return "unknown" self._version="unknown"
return self._version
SIZE = (159, 97) SIZE = (159, 97)
@@ -108,7 +109,7 @@ class BalPlugin(BasePlugin):
"base_fee": 100000, "base_fee": 100000,
"status": "New", "status": "New",
"info": "Bitcoin After Life Will Executor", "info": "Bitcoin After Life Will Executor",
"address": "bcrt1qa5cntu4hgadw8zd3n6sq2nzjy34sxdtd9u0gp7", "address": "bc1qusymuetsz2psaqzqxv8qmzcy64d9meckj3lxxf",
"selected": True, "selected": True,
} }
}, },

View File

@@ -424,6 +424,8 @@ class Heirs(dict, Logger):
def prepare_lists( def prepare_lists(
self, balance, total_fees, wallet, willexecutor=False, from_locktime=0 self, balance, total_fees, wallet, willexecutor=False, from_locktime=0
): ):
if balance<total_fees or balance < wallet.dust_threshold():
raise BalanceTooLowException(balance,wallet.dust_threshold(),total_fees)
willexecutors_amount = 0 willexecutors_amount = 0
willexecutors = {} willexecutors = {}
heir_list = {} heir_list = {}
@@ -461,7 +463,6 @@ class Heirs(dict, Logger):
percent_amount, percent_amount,
fixed_amount_with_dust, fixed_amount_with_dust,
) = self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold()) ) = self.fixed_percent_lists_amount(from_locktime, wallet.dust_threshold())
if fixed_amount > newbalance: if fixed_amount > newbalance:
fixed_amount = self.normalize_perc( fixed_amount = self.normalize_perc(
fixed_heirs, newbalance, fixed_amount, wallet fixed_heirs, newbalance, fixed_amount, wallet
@@ -471,14 +472,12 @@ class Heirs(dict, Logger):
heir_list.update(fixed_heirs) heir_list.update(fixed_heirs)
newbalance -= fixed_amount newbalance -= fixed_amount
if newbalance > 0: if newbalance > 0:
perc_amount = self.normalize_perc( perc_amount = self.normalize_perc(
percent_heirs, newbalance, percent_amount, wallet percent_heirs, newbalance, percent_amount, wallet
) )
newbalance -= perc_amount newbalance -= perc_amount
heir_list.update(percent_heirs) heir_list.update(percent_heirs)
if newbalance > 0: if newbalance > 0:
newbalance += fixed_amount newbalance += fixed_amount
fixed_amount = self.normalize_perc( fixed_amount = self.normalize_perc(
@@ -537,7 +536,7 @@ class Heirs(dict, Logger):
break break
elif 0 <= j: elif 0 <= j:
url, willexecutor = willexecutorsitems[j] url, willexecutor = willexecutorsitems[j]
if not Willexecutors.is_selected(willexecutor): if not Willexecutors.is_selected(willexecutor) or willexecutor["base_fee"] < wallet.dust_threshold():
continue continue
else: else:
willexecutor["url"] = url willexecutor["url"] = url
@@ -782,3 +781,10 @@ class WillExecutorFeeException(Exception):
return "WillExecutorFeeException: {} fee:{}".format( return "WillExecutorFeeException: {} fee:{}".format(
self.willexecutor["url"], self.willexecutor["base_fee"] self.willexecutor["url"], self.willexecutor["base_fee"]
) )
class BalanceTooLowException(Exception):
def __init__(self,balance, dust_threshold, fees):
self.balance=balance
self.dust_threshold = dust_threshold
self.fees = fees
def __str__(self):
return f"Balance too low, balance: {self.balance}, dust threshold: {self.dust_threshold}, fees: {self.fees}"

View File

@@ -1,7 +1,7 @@
{ {
"name": "BAL", "name": "BAL",
"fullname": "Bitcoin After Life", "fullname": "Bitcoin After Life",
"description": "Provides free and decentralized inheritance support<br> Version: 0.2.5", "description": "Provides free and decentralized inheritance support<br> Version: 0.2.7",
"author":"Svatantrya", "author":"Svatantrya",
"available_for": ["qt"], "available_for": ["qt"],
"icon":"icons/bal32x32.png" "icon":"icons/bal32x32.png"

483
qt.py
View File

@@ -15,13 +15,14 @@ from datetime import datetime
from decimal import Decimal from decimal import Decimal
from functools import partial from functools import partial
from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Union from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Union
import traceback
try: try:
QT_VERSION = sys._GUI_QT_VERSION QT_VERSION = sys._GUI_QT_VERSION
except Exception: except Exception:
QT_VERSION = 6 QT_VERSION = 6
if QT_VERSION == 5: if QT_VERSION == 5:
from PyQt5.QtCore import QThread, QCoreApplication
from PyQt5.QtCore import ( from PyQt5.QtCore import (
QDateTime, QDateTime,
QModelIndex, QModelIndex,
@@ -57,6 +58,7 @@ if QT_VERSION == 5:
QWidget, QWidget,
) )
else: # QT6 else: # QT6
from PyQt6.QtCore import QThread, QCoreApplication
from PyQt6.QtCore import ( from PyQt6.QtCore import (
QDateTime, QDateTime,
QModelIndex, QModelIndex,
@@ -495,8 +497,8 @@ class BalWindow(Logger):
Util.copy(self.will_settings, self.bal_plugin.default_will_settings()) Util.copy(self.will_settings, self.bal_plugin.default_will_settings())
self.logger.debug("not_will_settings {}".format(self.will_settings)) self.logger.debug("not_will_settings {}".format(self.will_settings))
self.bal_plugin.validate_will_settings(self.will_settings) self.bal_plugin.validate_will_settings(self.will_settings)
self.heir_list.update_will_settings() self.heir_list_widget.update_will_settings()
self.heir_list.update() self.heir_list_widget.update()
def init_wizard(self): def init_wizard(self):
wizard_dialog = BalWizardDialog(self) wizard_dialog = BalWizardDialog(self)
@@ -507,9 +509,9 @@ class BalWindow(Logger):
self.willexecutor_dialog.show() self.willexecutor_dialog.show()
def create_heirs_tab(self): def create_heirs_tab(self):
self.heir_list = HeirList(self, self.window) self.heir_list_widget = HeirListWidget(self, self.window)
tab = self.window.create_list_tab(self.heir_list) tab = self.window.create_list_tab(self.heir_list_widget)
tab.is_shown_cv = shown_cv(False) tab.is_shown_cv = shown_cv(False)
return tab return tab
@@ -601,14 +603,6 @@ class BalWindow(Logger):
except Exception as e: except Exception as e:
self.show_error(str(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): def set_heir(self, heir):
heir = list(heir) heir = list(heir)
if not self.bal_plugin.ENABLE_MULTIVERSE.get(): if not self.bal_plugin.ENABLE_MULTIVERSE.get():
@@ -616,7 +610,7 @@ class BalWindow(Logger):
h = Heirs.validate_heir(heir[0], heir[1:]) h = Heirs.validate_heir(heir[0], heir[1:])
self.heirs[heir[0]] = h self.heirs[heir[0]] = h
self.heir_list.update() self.heir_list_widget.update()
return True return True
def delete_heirs(self, heirs): def delete_heirs(self, heirs):
@@ -627,14 +621,12 @@ class BalWindow(Logger):
_logger.debug(f"error deleting heir: {heir} {e}") _logger.debug(f"error deleting heir: {heir} {e}")
pass pass
self.heirs.save() self.heirs.save()
self.heir_list.update() self.heir_list_widget.update()
return True return True
def import_heirs( def import_heirs(self):
self,
):
import_meta_gui( import_meta_gui(
self.window, _("heirs"), self.heirs.import_file, self.heir_list.update self.window, _("heirs"), self.heirs.import_file, self.heir_list_widget.update
) )
def export_heirs(self): def export_heirs(self):
@@ -748,8 +740,7 @@ class BalWindow(Logger):
def init_class_variables(self): def init_class_variables(self):
if not self.heirs: if not self.heirs:
raise NoHeirsException() raise NoHeirsException(_("Heirs are not defined"))
return
try: try:
self.date_to_check = Util.parse_locktime_string( self.date_to_check = Util.parse_locktime_string(
self.will_settings["threshold"] self.will_settings["threshold"]
@@ -757,11 +748,13 @@ class BalWindow(Logger):
# found = False # found = False
self.locktime_blocks = self.bal_plugin.LOCKTIME_BLOCKS.get() self.locktime_blocks = self.bal_plugin.LOCKTIME_BLOCKS.get()
self.current_block = Util.get_current_height(self.wallet.network) self.current_block = Util.get_current_height(self.wallet.network)
self.block_to_check = self.current_block + self.locktime_blocks self.block_to_check=0
self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get() self.no_willexecutor = self.bal_plugin.NO_WILLEXECUTOR.get()
self.willexecutors = Willexecutors.get_willexecutors( self.willexecutors = Willexecutors.get_willexecutors(
self.bal_plugin, update=True, bal_window=self, task=False self.bal_plugin, update=True, bal_window=self, task=False
) )
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()) self.init_heirs_to_locktime(self.bal_plugin.ENABLE_MULTIVERSE.get())
except Exception as e: except Exception as e:
@@ -776,8 +769,8 @@ class BalWindow(Logger):
if not self.heirs: if not self.heirs:
self.logger.warning("not heirs {}".format(self.heirs)) self.logger.warning("not heirs {}".format(self.heirs))
return return
self.init_class_variables()
try: try:
self.init_class_variables()
Will.check_amounts( Will.check_amounts(
self.heirs, self.heirs,
self.willexecutors, self.willexecutors,
@@ -788,9 +781,12 @@ class BalWindow(Logger):
except AmountException as e: except AmountException as e:
self.show_warning( 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}" f"In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{e}"
) )
) )
except CheckAliveException:
self.show_error(_("CheckAlive is in the past please update it to a date in the future but less than locktime"))
return
locktime = Util.parse_locktime_string(self.will_settings["locktime"]) locktime = Util.parse_locktime_string(self.will_settings["locktime"])
if locktime < self.date_to_check: if locktime < self.date_to_check:
self.show_error(_("locktime is lower than threshold")) self.show_error(_("locktime is lower than threshold"))
@@ -1036,7 +1032,18 @@ class BalWindow(Logger):
) )
def on_failure(err): def on_failure(err):
a,b,c = err
self.logger.error(f"fail to broadcast transactions:{err}") self.logger.error(f"fail to broadcast transactions:{err}")
self.logger.error(f"error: {b}")
self.logger.error(f"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
task = partial(self.push_transactions_to_willexecutors, force) task = partial(self.push_transactions_to_willexecutors, force)
msg = _("Selecting Will-Executors") msg = _("Selecting Will-Executors")
@@ -1079,7 +1086,13 @@ class BalWindow(Logger):
self.willitems[wid].we["url"], wid, "Waiting" self.willitems[wid].we["url"], wid, "Waiting"
) )
) )
self.willitems[wid].check_willexecutor() w = self.willitems[wid]
w.set_check_willexecutor(
Willexecutors.check_transaction(
wid,
w.we["url"]
)
)
self.waiting_dialog.update( self.waiting_dialog.update(
"checked {} - {} : {}".format( "checked {} - {} : {}".format(
self.willitems[wid].we["url"], self.willitems[wid].we["url"],
@@ -1128,7 +1141,9 @@ class BalWindow(Logger):
self.waiting_dialog.update( self.waiting_dialog.update(
"checking transaction: {}\n willexecutor: {}".format(wid, w.we["url"]) "checking transaction: {}\n willexecutor: {}".format(wid, w.we["url"])
) )
w.check_willexecutor()
w.set_check_willexecutor(Willexecutors.check_transaction(wid, w.we["url"]))
if time.time() - start < 3: if time.time() - start < 3:
time.sleep(3 - (time.time() - start)) time.sleep(3 - (time.time() - start))
@@ -1189,7 +1204,7 @@ class BalWindow(Logger):
def on_success(result): def on_success(result):
# del self.waiting_dialog # del self.waiting_dialog
try: try:
parent.willexecutor_list.update() parent.will_executor_list_widget.update()
except Exception as e: except Exception as e:
pass pass
try: try:
@@ -1214,10 +1229,20 @@ class BalWindow(Logger):
self.dw.show() self.dw.show()
def update_all(self): def update_all(self):
self.will_list.update_will(self.willitems) try:
self.heirs_tab.update() Will.add_willtree(self.willitems)
self.will_tab.update() all_utxos = self.wallet.get_utxos()
self.will_list.update() utxos_list = Will.utxos_strs(all_utxos)
Will.check_invalidated(
self.willitems, utxos_list, self.wallet
)
self.will_list.update_will(self.willitems)
self.heirs_tab.update()
self.will_tab.update()
self.will_list.update()
except Exception as e:
_logger.error(f"error while updating window: {e}")
def add_widget(grid, label, widget, row, help_): def add_widget(grid, label, widget, row, help_):
@@ -1246,6 +1271,41 @@ class _LockTimeEditor:
return cls.min_allowed_value <= x <= cls.max_allowed_value return cls.min_allowed_value <= x <= cls.max_allowed_value
<<<<<<< HEAD
=======
"""
HeirsLockTimeEdit - A custom QWidget for editing locktime values in the context of heirs distribution.
This widget combines raw locktime editing with date-based selection and provides
additional functionality for managing locktime values in a heir inheritance scenario.
Features:
- Supports both raw locktime values and human-readable date formats
- Emits valueEdited signal when the locktime value is edited
- Provides threshold-based validation for locktime values
- Integrates with heir distribution workflows
Behavior:
The class handles three types of locktime values:
1. **Timestamps**: Raw integer values representing Unix timestamps
2. **Day intervals**: Values ending with 'd' (e.g., '3d' = 3 days from now)
3. **Year intervals**: Values ending with 'y' (e.g., '2y' = 2 years from now)
Only these formats are valid:
- Examples: '1609459200', '3d', '2y'
- Invalid: 'invalid', '5m', '10x'
The widget automatically converts day/year intervals to appropriate timestamps.
Attributes:
valueEdited (pyqtSignal): Signal emitted when the locktime value is edited
locktime_threshold (int): Minimum threshold value for locktime (default: 50000000)
Args:
parent: Optional parent QWidget
default_index (int): Default index for the combo box (default: 1)
"""
class HeirsLockTimeEdit(QWidget, _LockTimeEditor): class HeirsLockTimeEdit(QWidget, _LockTimeEditor):
valueEdited = pyqtSignal() valueEdited = pyqtSignal()
locktime_threshold = 50000000 locktime_threshold = 50000000
@@ -1532,6 +1592,38 @@ class BalDialog(WindowModalDialog):
self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon))) self.setWindowIcon(read_QIcon_from_bytes(bal_plugin.read_file(icon)))
"""
BalWizardDialog - A custom QDialog that implements a multi-step wizard interface.
This dialog provides a structured, step-by-step workflow for complex operations
in the Bal Electrum plugin, guiding users through a sequence of pages with
forward/backward navigation and validation.
Features:
- Multi-page navigation with Previous/Next buttons
- Automatic validation before proceeding to next page
- Progress tracking with visual indicators
- Customizable page flow and validation rules
- Integration with BalDialog base class for consistent styling
Usage:
The wizard follows a standard pattern:
1. Initialize with a list of page constructors
2. Each page is responsible for its own setup and validation
3. The dialog manages navigation and state between pages
4. Finalize action is triggered when all pages are completed
Attributes:
pages (list): List of page constructors for the wizard
current_page (int): Index of the currently displayed page
page_widgets (list): List of instantiated page widgets
Args:
parent: Optional parent QWidget
title (str): Title to display in the dialog header
pages (list): List of page constructors (callables) for each step
"""
class BalWizardDialog(BalDialog): class BalWizardDialog(BalDialog):
def __init__(self, bal_window: "BalWindow"): def __init__(self, bal_window: "BalWindow"):
assert bal_window assert bal_window
@@ -1605,17 +1697,19 @@ class BalWizardDialog(BalDialog):
) )
def on_accept(self): def on_accept(self):
self.bal_window.update_all()
pass pass
def on_reject(self): def on_reject(self):
pass pass
def on_close(self): def on_close(self):
self.bal_window.update_all()
pass pass
def closeEvent(self, event): def closeEvent(self, event):
self.bal_window.update_all()
self.bal_window.heir_list.update_will_settings() self.bal_window.heir_list_widget.update_will_settings()
pass pass
@@ -1692,7 +1786,7 @@ class BalWizardHeirsWidget(BalWizardWidget):
) )
def get_content(self): def get_content(self):
self.heirs_list = HeirList(self.bal_window, self) self.heir_list_widget = HeirListWidget(self.bal_window, self)
button_add = QPushButton(_("Add")) button_add = QPushButton(_("Add"))
button_add.clicked.connect(self.add_heir) button_add.clicked.connect(self.add_heir)
button_import = QPushButton(_("Import")) button_import = QPushButton(_("Import"))
@@ -1701,20 +1795,20 @@ class BalWizardHeirsWidget(BalWizardWidget):
button_export.clicked.connect(self.export_to_file) button_export.clicked.connect(self.export_to_file)
widget = QWidget() widget = QWidget()
vbox = QVBoxLayout(widget) vbox = QVBoxLayout(widget)
vbox.addWidget(self.heirs_list) vbox.addWidget(self.heir_list_widget)
vbox.addLayout(Buttons(button_add, button_import, button_export)) vbox.addLayout(Buttons(button_add, button_import, button_export))
return widget return widget
def import_from_file(self): def import_from_file(self):
self.bal_window.import_heirs() self.bal_window.import_heirs()
self.heirs_list.update() self.heir_list_widget.update()
def export_to_file(self): def export_to_file(self):
self.bal_window.export_heirs() self.bal_window.export_heirs()
def add_heir(self): def add_heir(self):
self.bal_window.new_heir_dialog() self.bal_window.new_heir_dialog()
self.heirs_list.update() self.heir_list_widget.update()
def validate(self): def validate(self):
return True return True
@@ -1785,7 +1879,7 @@ class BalWizardWEDownloadWidget(BalWizardWidget):
_logger.debug(f"Failed to download willexecutors list {fail}") _logger.debug(f"Failed to download willexecutors list {fail}")
pass pass
task = partial(Willexecutors.download_list, self.bal_window.bal_plugin,self.bal_window.willexecutors) task = partial(Willexecutors.download_list,self.bal_window.willexecutors)
msg = _("Downloading Will-Executors list") msg = _("Downloading Will-Executors list")
self.waiting_dialog = BalWaitingDialog( self.waiting_dialog = BalWaitingDialog(
self.bal_window, msg, task, on_success, on_failure, exe=False self.bal_window, msg, task, on_success, on_failure, exe=False
@@ -2042,18 +2136,78 @@ class BalBuildWillDialog(BalDialog):
COLOR_OK = "#05ad05" COLOR_OK = "#05ad05"
def __init__(self, bal_window, parent=None): def __init__(self, bal_window, parent=None):
<<<<<<< HEAD
=======
"""Initialize the Build Will dialog.
Args:
bal_window (BalWindow): The main application window
parent (QWidget, optional): Parent widget. Defaults to None.
Initializes:
- Main UI components (message label, container widget)
- Message queue system with debounce timer
- Layout management
- Network connection
"""
>>>>>>> origin/doc
if not parent: if not parent:
parent = bal_window.window parent = bal_window.window
BalDialog.__init__(self, parent, bal_window.bal_plugin, _("Building Will")) BalDialog.__init__(self, parent, bal_window.bal_plugin, _("Building Will"))
self.parent = parent self.parent = parent
self.updatemessage.connect(self.update) self.updatemessage.connect(self.msg_update)
self.bal_window = bal_window self.bal_window = bal_window
self.bal_plugin = bal_window.bal_plugin
<<<<<<< HEAD
self.message_label = QLabel(_("Building Will:")) self.message_label = QLabel(_("Building Will:"))
self.vbox = QVBoxLayout(self) self.vbox = QVBoxLayout(self)
self.vbox.addWidget(self.message_label) self.vbox.addWidget(self.message_label,0)
self.qwidget = QWidget() self.qwidget = QWidget(self)
self.vbox.addWidget(self.qwidget) self.vbox.addWidget(self.qwidget,1)
self.labelsbox=QVBoxLayout(self.qwidget)
self.setMinimumWidth(600)
self.setMinimumHeight(100)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
self.labels = [] self.labels = []
=======
# Main message label
self.message_label = QLabel(_("Building Will:"))
self.vbox = QVBoxLayout(self)
self.vbox.addWidget(self.message_label, 0)
# Container for dynamic messages
self.qwidget = QWidget(self)
self.vbox.addWidget(self.qwidget, 1)
# Layout for messages with reduced spacing
self.labelsbox = QVBoxLayout(self.qwidget)
self.labelsbox.setContentsMargins(0, 0, 0, 0)
self.labelsbox.setSpacing(4) # Reduced spacing between messages
# Set minimum dimensions
self.setMinimumWidth(600)
self.setMinimumHeight(100)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
# Message queue implementation for efficient updates
self._message_queue = [] # Thread-safe message queue
self._message_timer = QTimer(self)
self._message_timer.setSingleShot(True)
self._message_timer.setInterval(50) # Debounce interval: 50ms
self._message_timer.timeout.connect(self._process_message_queue)
# Other initialization
self.labels = [] # Immediate message storage
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
>>>>>>> origin/doc
self.check_row = None self.check_row = None
self.inval_row = None self.inval_row = None
self.build_row = None self.build_row = None
@@ -2079,13 +2233,26 @@ class BalBuildWillDialog(BalDialog):
self.exec() self.exec()
def task_phase1(self): def task_phase1(self):
txs=None
_logger.debug("close plugin phase 1 started") _logger.debug("close plugin phase 1 started")
varrow = self.msg_set_status("checking variables")
try: try:
self.bal_window.init_class_variables() self.bal_window.init_class_variables()
except NoHeirsException: except CheckAliveException as cae:
_logger.error("no heirs exception") fee_per_byte = self.bal_window.will_settings.get("baltx_fees", 1)
return False, None tx = Will.invalidate_will(
varrow = self.msg_set_status("checking variables") self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
)
if tx:
_logger.debug("during phase1 CAE: {}, Continue to invalidate".format(cae))
self.msg_set_checking(self.msg_warning("Check Alive Threshold Passed: you have to Invalidate your old Will"))
else:
raise cae
return None, tx
except Exception as e:
raise e
try: try:
_logger.debug("checking variables") _logger.debug("checking variables")
Will.check_amounts( Will.check_amounts(
@@ -2121,9 +2288,10 @@ class BalBuildWillDialog(BalDialog):
return None, Will.invalidate_will( return None, Will.invalidate_will(
self.bal_window.willitems, self.bal_window.wallet, fee_per_byte self.bal_window.willitems, self.bal_window.wallet, fee_per_byte
) )
except NoHeirsException: except NoHeirsException as e:
_logger.debug("no heirs") _logger.debug("no heirs")
self.msg_set_checking("No Heirs") self.msg_set_checking("No Heirs")
raise e
except NotCompleteWillException as e: except NotCompleteWillException as e:
_logger.debug(f"not complete {e} true") _logger.debug(f"not complete {e} true")
message = False message = False
@@ -2147,13 +2315,15 @@ class BalBuildWillDialog(BalDialog):
if have_to_build: if have_to_build:
self.msg_set_building() self.msg_set_building()
try: try:
if not self.bal_window.build_will(): txs = self.bal_window.build_will()
if not txs:
self.msg_set_status( self.msg_set_status(
_("Balance is too low. No transaction was built"), _("Balance is too low. No transaction was built"),
None, None,
_("Skipped"), _("Skipped"),
self.COLOR_ERROR, self.COLOR_ERROR,
) )
return False,None
self.bal_window.check_will() self.bal_window.check_will()
for wid in Will.only_valid(self.bal_window.willitems): for wid in Will.only_valid(self.bal_window.willitems):
@@ -2185,12 +2355,14 @@ class BalBuildWillDialog(BalDialog):
if not self.bal_window.willitems[wid].get_status("COMPLETE"): if not self.bal_window.willitems[wid].get_status("COMPLETE"):
have_to_sign = True have_to_sign = True
break break
return have_to_sign, None return have_to_sign, txs
def on_accept(self): def on_accept(self):
self.bal_window.update_all()
pass pass
def on_accept_phase2(self): def on_accept_phase2(self):
self.bal_window.update_all()
pass pass
def on_error_push(self): def on_error_push(self):
@@ -2228,6 +2400,7 @@ class BalBuildWillDialog(BalDialog):
self.msg_set_pushing(_("Broadcasting")) self.msg_set_pushing(_("Broadcasting"))
retry = False retry = False
try: try:
willexecutors = Willexecutors.get_willexecutor_transactions( willexecutors = Willexecutors.get_willexecutor_transactions(
self.bal_window.willitems self.bal_window.willitems
) )
@@ -2257,7 +2430,10 @@ class BalBuildWillDialog(BalDialog):
self.bal_window.willitems[wid].we["url"], wid, "Waiting" self.bal_window.willitems[wid].we["url"], wid, "Waiting"
) )
) )
self.bal_window.willitems[wid].check_willexecutor() self.bal_plugin = self.bal_window.bal_plugin
w = self.bal_window.willitems[wid]
w.set_check_willexecutor(Willexecutors.check_transaction(wid, w.we["url"]))
row = self.msg_edit_row( row = self.msg_edit_row(
"checked {} - {} : {}".format( "checked {} - {} : {}".format(
self.bal_window.willitems[wid].we["url"], self.bal_window.willitems[wid].we["url"],
@@ -2268,7 +2444,6 @@ class BalBuildWillDialog(BalDialog):
) )
except Exception as e: except Exception as e:
_logger.error(f"loop push error:{e}") _logger.error(f"loop push error:{e}")
raise e raise e
if retry: if retry:
@@ -2306,12 +2481,11 @@ class BalBuildWillDialog(BalDialog):
on_error=self.on_error_phase1, on_error=self.on_error_phase1,
) )
def on_error(self, error):
_logger.error(error)
pass
def on_success_phase1(self, result): def on_success_phase1(self, result):
self.have_to_sign, tx = list(result) self.have_to_sign, tx = list(result)
#if not tx:
# self.msg_edit_row(self.msg_error("Error, no tx was built"))
# return
_logger.debug("have to sign {}".format(self.have_to_sign)) _logger.debug("have to sign {}".format(self.have_to_sign))
password = None password = None
if self.have_to_sign is None: if self.have_to_sign is None:
@@ -2359,7 +2533,6 @@ class BalBuildWillDialog(BalDialog):
self.close() self.close()
def closeEvent(self, event): def closeEvent(self, event):
self.bal_window.update_all()
self._stopping = True self._stopping = True
self.thread.stop() self.thread.stop()
@@ -2388,15 +2561,26 @@ class BalBuildWillDialog(BalDialog):
self.msg_set_pushing(self.msg_ok()) self.msg_set_pushing(self.msg_ok())
except Exception as e: except Exception as e:
td = traceback.format_exc()
self.msg_set_pushing(self.msg_error(e)) self.msg_set_pushing(self.msg_error(e))
self.msg_edit_row(self.msg_ok()) self.msg_edit_row(self.msg_ok())
self.wait(5) self.wait(5)
def on_error(self, error):
_logger.error(error)
pass
def on_error_phase1(self, error): def on_error_phase1(self, error):
_logger.error(f"error phase1: {error}") self.bal_window.update_all()
a,b,c = error
self.msg_edit_row(self.msg_error(f"Error: {b}"))
_logger.error(f"error phase1: {b}")
def on_error_phase2(self, error): def on_error_phase2(self, error):
_logger.error(f"error phase2: { error}") self.bal_window.upade_all()
a,b,c = error
self.msg_edit_row(self.msg_error(f"Error: {b}"))
_logger.error(f"error phase2: {b}")
def msg_set_checking(self, status="Waiting", row=None): def msg_set_checking(self, status="Waiting", row=None):
row = self.check_row if row is None else row row = self.check_row if row is None else row
@@ -2448,10 +2632,11 @@ class BalBuildWillDialog(BalDialog):
def msg_edit_row(self, line, row=None): def msg_edit_row(self, line, row=None):
try: try:
self.labels[row] = line self.labels[row] = line
except Exception: except Exception as e:
self.labels.append(line) self.labels.append(line)
row = len(self.labels) - 1 row = len(self.labels) - 1
self.updatemessage.emit() self.updatemessage.emit()
return row return row
@@ -2463,13 +2648,92 @@ class BalBuildWillDialog(BalDialog):
pass pass
self.updatemessage.emit() self.updatemessage.emit()
def update(self): def clear_layout(self,layout):
self.vbox.removeWidget(self.qwidget) while layout.count():
self.qwidget = QWidget(self) item = layout.takeAt(0)
labelsbox = QVBoxLayout(self.qwidget) w = item.widget()
if w:
w.setParent(None)
w.deleteLater()
def msg_update(self):
<<<<<<< HEAD
self.clear_layout(self.labelsbox)
for label in self.labels: for label in self.labels:
labelsbox.addWidget(QLabel(label)) label=label.replace("\n","<br>")
self.vbox.addWidget(self.qwidget) qlabel=QLabel(label)
self.labelsbox.addWidget(QLabel(label),1)
self.setMinimumHeight(30*(len(self.labels)+2))
=======
"""Updates the UI with new messages using a debounced queue system.
This method implements the following logic:
1. Adds all pending messages to the queue
2. Clears the immediate message storage
3. Starts the debounce timer if not already active
The actual UI update happens in _process_message_queue after the
debounce interval to prevent excessive UI updates.
Note:
Thread-safe operation - can be called from any thread
"""
self._message_queue.extend(self.labels)
self.labels = [] # Clear immediate labels after queuing
if not self._message_timer.isActive():
self._message_timer.start()
def _process_message_queue(self):
"""Processes queued messages with debounce for efficient UI updates.
This method:
1. Clears the existing layout
2. Processes all queued messages
3. Updates the UI once with all new messages
4. Resets the queue
5. Adjusts dialog height based on content
The debounce interval (50ms) ensures rapid message bursts are
processed in a single batch, reducing UI flicker.
Note:
Called automatically by QTimer after debounce interval
"""
if not self._message_queue:
return
# Clear existing layout
self.clear_layout(self.labelsbox)
# Process all queued messages
for text in self._message_queue:
try:
# Format text for rich display
formatted_text = text.replace("\n", "<br>")
# Create label with proper settings
label = QLabel(formatted_text)
label.setWordWrap(True)
label.setTextFormat(Qt.TextFormat.RichText)
label.setOpenExternalLinks(False) # Security
# Set size policy
label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
# Add to layout
self.labelsbox.addWidget(label)
except Exception as e:
# Log errors without interrupting processing
import logging
logging.error(f"Error creating label in BalBuildWillDialog: {e}")
# Reset queue and update dimensions
self._message_queue = []
self.setMinimumHeight(min(30 * (len(self.labels) + 2), 400)) # Max height limit
>>>>>>> origin/doc
def get_text(self): def get_text(self):
return self.message_label.text() return self.message_label.text()
@@ -2477,7 +2741,7 @@ class BalBuildWillDialog(BalDialog):
pass pass
class HeirList(MyTreeView, MessageBoxMixin): class HeirListWidget(MyTreeView, MessageBoxMixin):
class Columns(MyTreeView.BaseColumnsEnum): class Columns(MyTreeView.BaseColumnsEnum):
NAME = enum.auto() NAME = enum.auto()
ADDRESS = enum.auto() ADDRESS = enum.auto()
@@ -2492,9 +2756,16 @@ class HeirList(MyTreeView, MessageBoxMixin):
ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000 ROLE_SORT_ORDER = Qt.ItemDataRole.UserRole + 1000
ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 1001 ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 4000
key_role = ROLE_HEIR_KEY key_role = ROLE_HEIR_KEY
def createEditor(self, parent, option, index):
return QLineEdit(parent)
def setEditorData(self, editor, index):
editor.setText(index.data())
def setModelData(self, editor, model, index):
model.setData(index, editor.text())
def __init__(self, bal_window: "BalWindow", parent): def __init__(self, bal_window: "BalWindow", parent):
super().__init__( super().__init__(
parent=parent, parent=parent,
@@ -2509,6 +2780,7 @@ class HeirList(MyTreeView, MessageBoxMixin):
self.decimal_point = bal_window.window.get_decimal_point() self.decimal_point = bal_window.window.get_decimal_point()
self.bal_window = bal_window self.bal_window = bal_window
try: try:
self.setModel(QStandardItemModel(self)) self.setModel(QStandardItemModel(self))
self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder) self.sortByColumn(self.Columns.NAME, Qt.SortOrder.AscendingOrder)
@@ -2592,12 +2864,6 @@ class HeirList(MyTreeView, MessageBoxMixin):
menu.addAction(_("Delete"), lambda: self.delete_heirs(selected_keys)) menu.addAction(_("Delete"), lambda: self.delete_heirs(selected_keys))
menu.exec(self.viewport().mapToGlobal(position)) 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): def update(self):
current_key = self.get_role_data_for_current_item( current_key = self.get_role_data_for_current_item(
col=self.Columns.NAME, role=self.ROLE_HEIR_KEY col=self.Columns.NAME, role=self.ROLE_HEIR_KEY
@@ -2747,6 +3013,7 @@ class HeirList(MyTreeView, MessageBoxMixin):
self.heir_tx_fees.setValue(int(self.bal_window.will_settings["baltx_fees"])) self.heir_tx_fees.setValue(int(self.bal_window.will_settings["baltx_fees"]))
except Exception as e: except Exception as e:
pass
_logger.debug(f"Exception update_will_settings {e}") _logger.debug(f"Exception update_will_settings {e}")
def build_transactions(self): def build_transactions(self):
@@ -2965,16 +3232,16 @@ class PreviewList(MyTreeView):
wizard = QPushButton(_("Setup Wizard")) wizard = QPushButton(_("Setup Wizard"))
wizard.clicked.connect(self.bal_window.init_wizard) wizard.clicked.connect(self.bal_window.init_wizard)
display = QPushButton(_("Display")) #display = QPushButton(_("Display"))
display.clicked.connect(self.bal_window.preview_modal_dialog) #display.clicked.connect(self.bal_window.preview_modal_dialog)
display = QPushButton(_("Refresh")) refresh = QPushButton(_("Refresh"))
display.clicked.connect(self.check) refresh.clicked.connect(self.check)
widget = QWidget() widget = QWidget()
hlayout = QHBoxLayout(widget) hlayout = QHBoxLayout(widget)
hlayout.addWidget(wizard) hlayout.addWidget(wizard)
hlayout.addWidget(display) hlayout.addWidget(refresh)
toolbar.insertWidget(2, widget) toolbar.insertWidget(2, widget)
self.menu = menu self.menu = menu
@@ -3012,13 +3279,6 @@ class PreviewList(MyTreeView):
self.update() self.update()
def check(self): def check(self):
Will.add_willtree(self.bal_window.willitems)
all_utxos = self.bal_window.wallet.get_utxos()
utxos_list = Will.utxos_strs(all_utxos)
Will.check_invalidated(
self.bal_window.willitems, utxos_list, self.bal_window.wallet
)
close_window = BalBuildWillDialog(self.bal_window) close_window = BalBuildWillDialog(self.bal_window)
close_window.build_will_task() close_window.build_will_task()
@@ -3065,7 +3325,10 @@ class PreviewDialog(BalDialog, MessageBoxMixin):
self.size_label = QLabel() self.size_label = QLabel()
self.transactions_list = PreviewList(self.bal_window, self.will) self.transactions_list = PreviewList(self.bal_window, self.will)
self.bal_window.init_class_variables() try:
self.bal_window.init_class_variables()
except Exception as e:
_logger.error(f"PreviewDialog Exception: {e}")
self.check_will() self.check_will()
vbox = QVBoxLayout(self) vbox = QVBoxLayout(self)
@@ -3311,7 +3574,7 @@ class WillWidget(QWidget):
hlayout.addWidget(WillWidget(w, parent=parent)) hlayout.addWidget(WillWidget(w, parent=parent))
class WillExecutorList(MyTreeView): class WillExecutorListWidget(MyTreeView):
class Columns(MyTreeView.BaseColumnsEnum): class Columns(MyTreeView.BaseColumnsEnum):
SELECTED = enum.auto() SELECTED = enum.auto()
URL = enum.auto() URL = enum.auto()
@@ -3556,7 +3819,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin):
self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin) self.willexecutors_list = Willexecutors.get_willexecutors(self.bal_plugin)
self.size_label = QLabel() self.size_label = QLabel()
self.willexecutor_list = WillExecutorList(self) self.will_executor_list_widget = WillExecutorListWidget(self)
vbox = QVBoxLayout(self) vbox = QVBoxLayout(self)
vbox.addWidget(self.size_label) vbox.addWidget(self.size_label)
@@ -3573,7 +3836,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin):
hbox.addWidget(spacer_widget) hbox.addWidget(spacer_widget)
vbox.addWidget(widget) vbox.addWidget(widget)
vbox.addWidget(self.willexecutor_list) vbox.addWidget(self.will_executor_list_widget)
buttonbox = QHBoxLayout() buttonbox = QHBoxLayout()
b = QPushButton(_("Add")) b = QPushButton(_("Add"))
@@ -3597,7 +3860,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin):
buttonbox.addWidget(b) buttonbox.addWidget(b)
vbox.addLayout(buttonbox) vbox.addLayout(buttonbox)
# self.willexecutor_list.update() # self.will_executor_list_widget.update()
def add(self): def add(self):
self.willexecutors_list["http://localhost:8080"] = { self.willexecutors_list["http://localhost:8080"] = {
@@ -3605,11 +3868,11 @@ class WillExecutorWidget(QWidget, MessageBoxMixin):
"base_fee": 0, "base_fee": 0,
"status": "-1", "status": "-1",
} }
self.willexecutor_list.update() self.will_executor_list_widget.update()
def download_list(self): def download_list(self):
self.willexecutors_list.update(Willexecutors.download_list(self.bal_plugin,self.bal_window.willexecutors)) self.willexecutors_list.update(Willexecutors.download_list(self.bal_window.willexecutors))
self.willexecutor_list.update() self.will_executor_list_widget.update()
def export_file(self, path): def export_file(self, path):
Util.export_meta_gui( Util.export_meta_gui(
@@ -3632,13 +3895,13 @@ class WillExecutorWidget(QWidget, MessageBoxMixin):
wes = self.willexecutors_list wes = self.willexecutors_list
self.bal_window.ping_willexecutors(wes, self.parent) self.bal_window.ping_willexecutors(wes, self.parent)
self.willexecutors_list.update(wes) self.willexecutors_list.update(wes)
self.willexecutor_list.update() self.will_executor_list_widget.update()
def import_json_file(self, path): def import_json_file(self, path):
data = read_json_file(path) data = read_json_file(path)
data = self._validate(data) data = self._validate(data)
self.willexecutors_list.update(data) self.willexecutors_list.update(data)
self.willexecutor_list.update() self.will_executor_list_widget.update()
# TODO validate willexecutor json import file # TODO validate willexecutor json import file
def _validate(self, data): def _validate(self, data):
@@ -3662,10 +3925,10 @@ class WillExecutorDialog(BalDialog, MessageBoxMixin):
self.setMinimumSize(1000, 200) self.setMinimumSize(1000, 200)
vbox = QVBoxLayout(self) vbox = QVBoxLayout(self)
self.willexecutor_list = WillExecutorWidget( self.will_executor_list_widget = WillExecutorWidget(
self, self.bal_window, self.willexecutors_list self, self.bal_window, self.willexecutors_list
) )
vbox.addWidget(self.willexecutor_list) vbox.addWidget(self.will_executor_list_widget)
def is_hidden(self): def is_hidden(self):
return self.isMinimized() or self.isHidden() return self.isMinimized() or self.isHidden()
@@ -3682,3 +3945,29 @@ class WillExecutorDialog(BalDialog, MessageBoxMixin):
def closeEvent(self, event): def closeEvent(self, event):
event.accept() event.accept()
class CheckAliveException(Exception):
def __init__(self,timestamp_to_check):
self.timestamp_to_check = timestamp_to_check
def __str__(self):
<<<<<<< HEAD
=======
def __del__(self):
"""Explicit cleanup to prevent memory leaks.
This destructor ensures proper cleanup of:
- Message queue timer
- All widgets in the layout
- Network connections
Called automatically when the dialog is destroyed.
"""
if hasattr(self, '_message_timer') and self._message_timer:
self._message_timer.stop()
self._message_timer.deleteLater()
self.clear_layout(self.labelsbox)
>>>>>>> origin/doc
return "Check alive expired please update it: {}".format(datetime.fromtimestamp(self.timestamp_to_check).isoformat())

21
will.py
View File

@@ -339,6 +339,7 @@ class Will:
utxos = wallet.get_utxos() utxos = wallet.get_utxos()
filtered_inputs = [] filtered_inputs = []
prevout_to_spend = [] prevout_to_spend = []
current_height = Util.get_current_height(wallet.network)
for prevout_str, ws in inputs.items(): for prevout_str, ws in inputs.items():
for w in ws: for w in ws:
if w[0] not in filtered_inputs: if w[0] not in filtered_inputs:
@@ -348,6 +349,8 @@ class Will:
balance = 0 balance = 0
utxo_to_spend = [] utxo_to_spend = []
for utxo in utxos: for utxo in utxos:
if utxo.is_coinbase_output() and utxo.block_height < current_height+100:
continue
utxo_str = utxo.prevout.to_str() utxo_str = utxo.prevout.to_str()
if utxo_str in prevout_to_spend: if utxo_str in prevout_to_spend:
balance += inputs[utxo_str][0][2].value_sats() balance += inputs[utxo_str][0][2].value_sats()
@@ -356,7 +359,7 @@ class Will:
change_addresses = wallet.get_change_addresses_for_new_transaction() change_addresses = wallet.get_change_addresses_for_new_transaction()
out = PartialTxOutput.from_address_and_value(change_addresses[0], balance) out = PartialTxOutput.from_address_and_value(change_addresses[0], balance)
out.is_change = True out.is_change = True
locktime = Util.get_current_height(wallet.network) locktime = current_height
tx = PartialTransaction.from_io( tx = PartialTransaction.from_io(
utxo_to_spend, [out], locktime=locktime, version=2 utxo_to_spend, [out], locktime=locktime, version=2
) )
@@ -560,6 +563,9 @@ class Will:
raise WillExpiredException( raise WillExpiredException(
f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}" f"Will Expired {wid[0][0]}: {locktime}<{timestamp_to_check}"
) )
else:
from datetime import datetime
_logger.debug(f"Will Not Expired {wid[0][0]}: {datetime.fromtimestamp(locktime).isoformat()} > {datetime.fromtimestamp(timestamp_to_check).isoformat()}")
# def check_all_input_spent_are_in_wallet(): # def check_all_input_spent_are_in_wallet():
# _logger.info("check all input spent are in wallet or valid txs") # _logger.info("check all input spent are in wallet or valid txs")
@@ -793,9 +799,9 @@ class WillItem(Logger):
iw = inp[1] iw = inp[1]
self.set_anticipate(iw) self.set_anticipate(iw)
def check_willexecutor(self): def set_check_willexecutor(self,resp):
try: try:
if resp := Willexecutors.check_transaction(self._id, self.we["url"]): if resp :
if "tx" in resp and resp["tx"] == str(self.tx): if "tx" in resp and resp["tx"] == str(self.tx):
self.set_status("PUSHED") self.set_status("PUSHED")
self.set_status("CHECKED") self.set_status("CHECKED")
@@ -835,7 +841,12 @@ class WillItem(Logger):
class WillException(Exception): class WillException(Exception):
pass def __init__(self,msg="WillException"):
self.msg=msg
Exception.__init__(self)
def __str__(self):
return self.msg
class WillExpiredException(WillException): class WillExpiredException(WillException):
@@ -872,8 +883,6 @@ class WillExecutorNotPresent(NotCompleteWillException):
class NoHeirsException(WillException): class NoHeirsException(WillException):
pass pass
class AmountException(WillException): class AmountException(WillException):
pass pass

View File

@@ -132,7 +132,7 @@ class Willexecutors:
raise Exception("You are offline.") raise Exception("You are offline.")
_logger.debug(f"<-- {method} {url} {data}") _logger.debug(f"<-- {method} {url} {data}")
headers = {} headers = {}
headers["user-agent"] = f"BalPlugin v:{BalPlugin.version()}" headers["user-agent"] = f"BalPlugin v:{BalPlugin.__version__}"
headers["Content-Type"] = "text/plain" headers["Content-Type"] = "text/plain"
if not handle_response: if not handle_response:
handle_response = Willexecutors.handle_response handle_response = Willexecutors.handle_response
@@ -260,7 +260,7 @@ class Willexecutors:
def download_list(bal_plugin,old_willexecutors): def download_list(old_willexecutors):
try: try:
willexecutors = Willexecutors.send_request( willexecutors = Willexecutors.send_request(
"get", "get",
@@ -280,7 +280,7 @@ class Willexecutors:
_logger.error(f"Failed to download willexecutors list: {e}") _logger.error(f"Failed to download willexecutors list: {e}")
return {} return {}
def get_willexecutors_list_from_json(bal_plugin): def get_willexecutors_list_from_json():
try: try:
with open("willexecutors.json") as f: with open("willexecutors.json") as f:
willexecutors = json.load(f) willexecutors = json.load(f)