willexecutors: fee-bound is_valid (extremes allowed), is_selected flag-only; fix merge_will crash on missing date_to_check; DNSSEC async, ical folding, pyright/ruff cleanup; refresh karen7/samanta7 fixtures

This commit is contained in:
2026-08-01 22:08:53 -04:00
parent 06d61d78d0
commit 693479b0da
24 changed files with 272 additions and 204 deletions

View File

@@ -194,30 +194,15 @@ class BalCalendar:
@staticmethod
def ical_escape(text: str) -> str:
# escape per RFC5545: backslash, ; , newlines
text = text.encode("utf-8")
text = (
text.replace(b"\\", b"\\\\")
.replace(b";", b"\\;")
.replace(b",", b"\\,")
text.replace("\\", "\\\\")
.replace(";", "\\;")
.replace(",", "\\,")
)
return "\r\n".join(
BalCalendar.fold_ical_line(line)
for line in text.split("\r\n")
)
out =""
temp=text.split(b"\r\n")
for s in temp:
encoded= s
cut =0
while len(encoded) >75:
cut+=5
encoded=f"{s[:len(s)-cut]}"
if encoded[-1]==b"\\" and encoded[-2]!=b"\\\\":
cut += 1
encoded=f"{s[:len(s)-cut]}"
encoded=f"{encoded}...\r\n".encode("utf-8")
if cut>0:
out+=str(f"{s[:len(s)-cut].decode()}...\r\n")
else:
out+=str(f"{s.decode()}\r\n")
return out[:-2]
@staticmethod
def fold_ical_line(line: str, limit: int = 75) -> str:

View File

@@ -17,6 +17,8 @@ the few list classes they reference are imported lazily inside the methods that
use them (see ``lists`` imports below).
"""
from typing import TYPE_CHECKING
from .calendar import BalCalendar, BalCalendarButton
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
@@ -27,6 +29,9 @@ from .widgets import (
compute_reminder_offsets,
)
if TYPE_CHECKING:
from .window import BalWindow
# NOTE: list views (HeirListWidget, PreviewList, WillExecutorWidget) are
# imported lazily where needed to avoid a dialogs<->lists import cycle.
@@ -467,7 +472,6 @@ class BalWaitingDialog(BalDialog):
def exe(self):
self.thread = TaskThread(self)
self.thread.finished.connect(self.deleteLater) # see #3956
self.thread.finished.connect(self.finished)
self.thread.add(self.task, self.on_success, self.accept, self.on_error)
# IMPORTANT: keep the *application-modal* exec() of the original code.
# This dialog is driven by a TaskThread whose result (on_success, e.g.
@@ -482,9 +486,6 @@ class BalWaitingDialog(BalDialog):
def hello(self):
pass
def finished(self):
pass
def on_accepted(self):
pass
@@ -1147,7 +1148,6 @@ class BalBuildWillDialog(BalDialog):
for url, we in willexecutors.items()
if Willexecutors.is_selected(
self.bal_window.willexecutors.get(url),
max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
) and Willexecutors.is_valid(
self.bal_window.willexecutors.get(url),
max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),

View File

@@ -14,6 +14,8 @@ construction) for all business actions, so the heavy logic stays in ``window``
and ``dialogs``.
"""
from typing import TYPE_CHECKING
from PyQt6.QtWidgets import QLineEdit as _QLineEdit
from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate
@@ -22,6 +24,9 @@ from .common import _, _logger # underscore names are not re-exported by "impor
from .dialogs import BalBuildWillDialog, BalDialog
from .widgets import BalCheckBox, WillSettingsWidget
if TYPE_CHECKING:
from .window import BalWindow
def _can_sign(will_item):
"""True if a will transaction may still be signed (not fully signed)."""
@@ -127,7 +132,7 @@ class HeirListWidget(MyTreeView, MessageBoxMixin):
self.bal_window.new_heir_dialog(edit_key)
def on_edited(self, idx, edit_key, *, text):
original = prior_name = self.bal_window.heirs.get(edit_key)
prior_name = self.bal_window.heirs.get(edit_key)
if not prior_name:
return
col = idx.column()
@@ -148,12 +153,7 @@ class HeirListWidget(MyTreeView, MessageBoxMixin):
try:
self.bal_window.set_heir(prior_name)
except Exception:
pass
try:
self.bal_window.set_heir((edit_key,) + original)
except Exception:
self.update()
self.update()
def delete_heirs(self, selected_keys):
self.bal_window.delete_heirs(selected_keys)
@@ -938,6 +938,7 @@ class WillExecutorListWidget(MyTreeView):
idx = self.indexAt(position)
column = idx.column() or self.Columns.URL
selected_keys = []
sel_key = None
for s_idx in self.selected_in_column(self.Columns.URL):
item = self.model().itemFromIndex(s_idx)
# Use the FULL url stored in the key role, NOT item.data(0): the
@@ -1079,7 +1080,7 @@ class WillExecutorListWidget(MyTreeView):
# are shown unchanged.
display_url = url if len(url) <= 40 else url[:37] + "\u2026"
labels[self.Columns.URL] = display_url
if Willexecutors.is_selected(value, max_fee=float("inf")):
if Willexecutors.is_selected(value):
labels[self.Columns.SELECTED] = [
read_QIcon_from_bytes(
@@ -1368,6 +1369,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin):
add_another_btn.clicked.connect(add_another)
else:
self._add_another = False
add_another_btn = None
row = 0
grid.addWidget(QLabel(_("URL")), row, 0)

View File

@@ -18,10 +18,15 @@ Contents:
* WillWidget - single will-tx box
"""
from typing import TYPE_CHECKING
from .calendar import BalCalendar, BalCalendarButton
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
if TYPE_CHECKING:
from .window import BalWindow
def compute_reminder_offsets(days, count):
"""Return the reminder offsets (in days BEFORE the deadline) for an .ics event.

View File

@@ -385,7 +385,7 @@ class BalWindow:
f = False
for _u, w in self.willexecutors.items():
if Willexecutors.is_selected(
w, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get()
w
) and Willexecutors.is_valid(
w, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
dust=self.window.wallet.dust_threshold()
@@ -521,8 +521,8 @@ class BalWindow:
def show_message(self, text):
self.window.show_message(text)
def show_warning(self, text, parent=None):
self.window.show_warning(text, parent=None)
def show_warning(self, text, parent=None, title=None):
self.window.show_warning(text, parent=parent, title=title)
def show_error(self, text):
self.window.show_error(text)
@@ -703,7 +703,7 @@ class BalWindow:
f = False
for _k, we in self.willexecutors.items():
if Willexecutors.is_selected(
we, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get()
we
) and Willexecutors.is_valid(
we, max_fee=self.bal_plugin.MAX_WILLEXECUTOR_FEE.get(),
dust=self.window.wallet.dust_threshold()
@@ -903,7 +903,7 @@ class BalWindow:
self.show_message(_("No transactions to invalidate"))
def on_failure(exec_info):
log_error(exec_info, self.bal_window)
log_error(exec_info, self)
willitems = will if will is not None else self.willitems
fee_per_byte = self.will_settings.get("baltx_fees", 1)
@@ -1080,7 +1080,7 @@ class BalWindow:
raise e
def on_failure(exec_info):
log_error(exec_info, self.bal_window)
log_error(exec_info, self)
password = self.get_wallet_password()
task = partial(self.sign_transactions, password, will=will, txids=txids)
@@ -1106,7 +1106,7 @@ class BalWindow:
)
def on_failure(exec_info):
log_error(exec_info, self.bal_window)
log_error(exec_info, self)
# a,b,c = err
# _logger.error(f"fail to broadcast transactions:{err}")
# _logger.error(f"error: {b}")
@@ -1246,6 +1246,13 @@ class BalWindow:
valid/invalidated/replaced statuses (no server contact, no expiry
raise).
"""
# The reference timestamp is normally set by init_class_variables(),
# which the merge flow does not run (Merge -> file import can be the
# very first action in a session). Fall back to "now" so the local
# validity check and the trailing update_all() always have it.
if not hasattr(self, "date_to_check") or self.date_to_check is None:
self.date_to_check = datetime.now().timestamp()
for wid, wi in imported.items():
if wid in self.willitems:
live = self.willitems[wid]
@@ -1298,7 +1305,7 @@ class BalWindow:
)
Will.check_signatures(self.willitems, self.wallet)
except Exception as e:
log_error(e, self.bal_window)
log_error(e, self)
self.save_willitems()
self.update_all()
@@ -1726,14 +1733,15 @@ class BalWindow:
Willexecutors.ping_servers_parallel(wes, on_each=on_each, on_tick=on_tick)
def ping_willexecutors(self, wes, fn_on_success, fn_on_failure=None):
if not fn_on_failure:
fn_on_failure = log_error
def on_success(result):
fn_on_success(result)
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")