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

@@ -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]

View File

@@ -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])

View File

@@ -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

View File

@@ -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: