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:
@@ -21,6 +21,8 @@ Will-executor "heirs" are synthetic entries whose key starts with the
|
||||
``w!ll3x3c"`` marker; they are skipped by most heir comparisons.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import math
|
||||
import random
|
||||
import re
|
||||
@@ -31,6 +33,7 @@ from typing import (
|
||||
Dict,
|
||||
Optional,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
|
||||
import dns
|
||||
@@ -65,6 +68,19 @@ if TYPE_CHECKING:
|
||||
|
||||
_logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _query_txt_records(url: str) -> Tuple[Any, bool]:
|
||||
"""Resolve TXT records with DNSSEC validation.
|
||||
|
||||
``electrum.dnssec.query`` became an ``async`` function in Electrum 4.7.2,
|
||||
so adapt the call for this synchronous context while staying compatible
|
||||
with older synchronous implementations.
|
||||
"""
|
||||
query = dnssec.query
|
||||
if inspect.iscoroutinefunction(query):
|
||||
return asyncio.run(query(url, dns.rdatatype.TXT))
|
||||
return cast(Tuple[Any, bool], query(url, dns.rdatatype.TXT))
|
||||
|
||||
# Column layout of a stored heir list. These indices are part of the on-disk
|
||||
# wallet format and are relied upon all over the codebase, so they must NEVER
|
||||
# be reordered.
|
||||
@@ -520,8 +536,8 @@ class Heirs(dict, Logger):
|
||||
)
|
||||
|
||||
def prepare_lists(
|
||||
self, balance, total_fees, wallet, willexecutor=False, from_locktime=0,
|
||||
max_fee=None,
|
||||
self, balance, total_fees, wallet, willexecutor: Optional[dict] = None,
|
||||
from_locktime=0, max_fee=None,
|
||||
):
|
||||
if balance<total_fees or balance < wallet.dust_threshold():
|
||||
raise BalanceTooLowException(balance,wallet.dust_threshold(),total_fees)
|
||||
@@ -541,7 +557,7 @@ class Heirs(dict, Logger):
|
||||
willexecutor, max_fee
|
||||
)
|
||||
willexecutors_amount += base_fee
|
||||
h = [None] * 4
|
||||
h: list = [None] * 4
|
||||
h[HEIR_AMOUNT] = base_fee
|
||||
h[HEIR_REAL_AMOUNT] = base_fee
|
||||
h[HEIR_LOCKTIME] = locktime
|
||||
@@ -661,7 +677,7 @@ class Heirs(dict, Logger):
|
||||
self.decimal_point = bal_plugin.get_decimal_point()
|
||||
no_willexecutors = bal_plugin.NO_WILLEXECUTOR.get()
|
||||
for utxo in utxos:
|
||||
if utxo.value_sats() > 0 * tx_fees:
|
||||
if utxo.value_sats() > 0:
|
||||
balance += utxo.value_sats()
|
||||
len_utxo_set += 1
|
||||
available_utxos.append(utxo)
|
||||
@@ -685,11 +701,12 @@ class Heirs(dict, Logger):
|
||||
elif j == -1:
|
||||
if not no_willexecutors:
|
||||
continue
|
||||
url = willexecutor = False
|
||||
url = willexecutor = None
|
||||
else:
|
||||
break
|
||||
fees = {}
|
||||
i = 0
|
||||
txs = {}
|
||||
while i < 10:
|
||||
txs = {}
|
||||
redo = False
|
||||
@@ -829,7 +846,7 @@ class Heirs(dict, Logger):
|
||||
# support email-style addresses, per the OA standard
|
||||
url = url.replace("@", ".")
|
||||
try:
|
||||
records, validated = dnssec.query(url, dns.rdatatype.TXT)
|
||||
records, validated = _query_txt_records(url)
|
||||
except DNSException as e:
|
||||
_logger.info(f"Error resolving openalias: {repr(e)}")
|
||||
return None
|
||||
@@ -838,15 +855,15 @@ class Heirs(dict, Logger):
|
||||
string = to_string(record.strings[0], "utf8")
|
||||
if string.startswith("oa1:" + prefix):
|
||||
address = cls.find_regex(string, r"recipient_address=([A-Za-z0-9]+)")
|
||||
if not address:
|
||||
continue
|
||||
name = cls.find_regex(string, r"recipient_name=([^;]+)")
|
||||
if not name:
|
||||
name = address
|
||||
if not address:
|
||||
continue
|
||||
return address, name, validated
|
||||
|
||||
@staticmethod
|
||||
def find_regex(haystack, needle):
|
||||
def find_regex(haystack, needle) -> Optional[str]:
|
||||
regex = re.compile(needle)
|
||||
try:
|
||||
return regex.search(haystack).groups()[0]
|
||||
|
||||
@@ -108,7 +108,7 @@ def get_will(x):
|
||||
|
||||
try:
|
||||
# Electrum >= 4.8.0
|
||||
from electrum.stored_dict import register_name as _electrum_register_name
|
||||
from electrum.stored_dict import register_name as _electrum_register_name # pyright: ignore[reportMissingImports]
|
||||
|
||||
def _register_will_dict(name, method, _type=None):
|
||||
"""Register a plugin dict in the wallet DB (Electrum >= 4.8.0 API)."""
|
||||
@@ -118,7 +118,7 @@ except ImportError:
|
||||
# Electrum <= 4.7.2
|
||||
def _register_will_dict(name, method, _type=None):
|
||||
"""Register a plugin dict in the wallet DB (Electrum <= 4.7.2 API)."""
|
||||
json_db.register_dict(name, method, _type)
|
||||
json_db.register_dict(name, method, _type) # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
|
||||
_register_will_dict("heirs", tuple)
|
||||
@@ -405,7 +405,7 @@ class BalPlugin(BasePlugin):
|
||||
"""Fill in any missing will-setting with its default value."""
|
||||
defaults = BalPlugin.default_will_settings()
|
||||
if not will_settings:
|
||||
will_settings = []
|
||||
will_settings = {}
|
||||
if int(will_settings.get("baltx_fees", 0)) < 1:
|
||||
will_settings["baltx_fees"] = defaults['baltx_fees']
|
||||
if not will_settings.get("threshold"):
|
||||
@@ -427,7 +427,7 @@ class BalPlugin(BasePlugin):
|
||||
@staticmethod
|
||||
def default_will_settings():
|
||||
"""Default will settings: a fee rate plus absolute threshold/locktime."""
|
||||
will_settings = {"baltx_fees": 20}
|
||||
will_settings: dict[str, float] = {"baltx_fees": 20}
|
||||
will_settings.update(BalPlugin.default_will_settings_absolute())
|
||||
return will_settings
|
||||
|
||||
@@ -460,10 +460,12 @@ class BalTimestamp:
|
||||
* an integer -> an absolute UNIX timestamp (``unit is None``)
|
||||
"""
|
||||
|
||||
value = None
|
||||
unit = None
|
||||
value: int
|
||||
unit: str | None
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = 1
|
||||
self.unit = None
|
||||
str_value = str(value)
|
||||
if str_value and str_value[-1].lower() in ("y", "d"):
|
||||
self.value = int(str_value[:-1])
|
||||
|
||||
@@ -443,11 +443,12 @@ class Will:
|
||||
for wid in will:
|
||||
wtx = will[wid].tx
|
||||
found = False
|
||||
inp = None
|
||||
for inp in wtx.inputs():
|
||||
if inp.prevout.txid.hex() in will:
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
if not found and inp is not None:
|
||||
out[inp.prevout.to_str()] = inp
|
||||
return out
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ interaction is handled by the Qt layer.
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponse
|
||||
from electrum import bitcoin, constants
|
||||
@@ -154,7 +155,7 @@ class Willexecutors:
|
||||
|
||||
@staticmethod
|
||||
def get_willexecutors(
|
||||
bal_plugin, update=False, bal_window=False, force=False, task=True
|
||||
bal_plugin, update=False, bal_window: Any = None, force=False, task=True
|
||||
):
|
||||
willexecutors = bal_plugin.WILLEXECUTORS.get()
|
||||
willexecutors = willexecutors.get(chainname, {})
|
||||
@@ -205,15 +206,11 @@ class Willexecutors:
|
||||
return w_sorted
|
||||
|
||||
@staticmethod
|
||||
def is_selected(willexecutor, value=None, max_fee=None):
|
||||
def is_selected(willexecutor, value=None):
|
||||
if not willexecutor:
|
||||
return False
|
||||
if value is not None:
|
||||
willexecutor["selected"] = value
|
||||
if max_fee is not None:
|
||||
base_fee = willexecutor.get("base_fee", 0)
|
||||
if int(base_fee) >= max_fee:
|
||||
return False
|
||||
try:
|
||||
return willexecutor["selected"]
|
||||
except Exception:
|
||||
@@ -228,9 +225,9 @@ class Willexecutors:
|
||||
if not address or not bitcoin.is_address(address, net=constants.net):
|
||||
return False
|
||||
base_fee = int(willexecutor.get("base_fee", 0))
|
||||
if dust is not None and base_fee <= dust:
|
||||
if dust is not None and base_fee < dust:
|
||||
return False
|
||||
if max_fee is not None and base_fee >= max_fee:
|
||||
if max_fee is not None and base_fee > max_fee:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -426,7 +423,7 @@ class Willexecutors:
|
||||
else:
|
||||
base_fee = w.get("base_fee")
|
||||
try:
|
||||
base_fee = int(base_fee)
|
||||
base_fee = int(base_fee or 0)
|
||||
if base_fee < 0:
|
||||
raise ValueError("negative fee")
|
||||
if base_fee > TOTAL_COIN_SUPPLY_LIMIT_IN_BTC * COIN:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user