core: extract GUI-free logic into bal.core (reminders/checkalive/input_rules); fix save/sign tx RLock pickle by serializing tx; keep invalid heirs in wallet on build; tb1 testnet will-executor addresses; move tests to core modules
This commit is contained in:
193
bal/core/input_rules.py
Normal file
193
bal/core/input_rules.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
bal.core.input_rules
|
||||
====================
|
||||
|
||||
Pure, GUI-free rules for the plugin's text-input widgets: locktime bounds and
|
||||
acceptance, the RAW locktime sanitisation ("30d"/"1y"), and the percentage-or-
|
||||
amount field normalisation.
|
||||
|
||||
These used to live inside the Qt widget classes in ``bal.gui.qt.widgets``.
|
||||
Keeping them here makes them testable without Qt and lets any GUI front-end
|
||||
reuse the exact same parsing rules.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Optional, Tuple, Union
|
||||
|
||||
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX, NLOCKTIME_MIN
|
||||
|
||||
__all__ = [
|
||||
"NLOCKTIME_BLOCKHEIGHT_MAX",
|
||||
"NLOCKTIME_MAX",
|
||||
"NLOCKTIME_MIN",
|
||||
"LockTimeEditor",
|
||||
"normalize_locktime_raw_text",
|
||||
"normalize_perc_amount_text",
|
||||
"parse_perc_amount",
|
||||
"replace_dy_suffixes",
|
||||
]
|
||||
|
||||
|
||||
class LockTimeEditor:
|
||||
"""Acceptance bounds shared by the RAW and Date locktime editors.
|
||||
|
||||
A Qt widget mixin historically; the pure parts (bounds and acceptance test)
|
||||
live here so they can be tested and reused without Qt. Widget subclasses
|
||||
override ``min_allowed_value``/``max_allowed_value`` to tighten the bounds.
|
||||
"""
|
||||
|
||||
min_allowed_value = NLOCKTIME_MIN
|
||||
max_allowed_value = NLOCKTIME_MAX
|
||||
alarm = None
|
||||
|
||||
def get_value(self) -> Optional[int]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_value(self, x: Any, force=True) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def is_acceptable_locktime(cls, x: Any) -> bool:
|
||||
"""True when ``x`` (string or int) is within the allowed locktime bounds.
|
||||
|
||||
An empty/falsy value is accepted (the field is not yet filled in).
|
||||
"""
|
||||
if not x: # e.g. empty string
|
||||
return True
|
||||
try:
|
||||
x = int(x)
|
||||
except Exception as _e:
|
||||
return False
|
||||
return cls.min_allowed_value <= x <= cls.max_allowed_value
|
||||
|
||||
@staticmethod
|
||||
def get_max_allowed_timestamp() -> int:
|
||||
"""Highest locktime timestamp accepted on this platform.
|
||||
|
||||
On 32-bit ``time_t`` Windows builds ``datetime.fromtimestamp`` overflows
|
||||
past 2038, so the ceiling is clamped to INT32_MAX (see #6170).
|
||||
"""
|
||||
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 replace_dy_suffixes(text: str) -> str:
|
||||
"""Strip the relative-time suffixes (d/y) from ``text``.
|
||||
|
||||
Only days ("d") and years ("y") are supported. The block-height suffix
|
||||
("b") was removed (A1): locktimes are always timestamps now.
|
||||
"""
|
||||
return str(text).replace("d", "").replace("y", "")
|
||||
|
||||
|
||||
def _checkbdy(s: str, pos: int, appendix: str) -> Tuple[int, str]:
|
||||
"""Keep a ``d``/``y`` suffix typed right after an existing suffix.
|
||||
|
||||
When the character just before ``pos`` equals ``appendix``, the text is
|
||||
re-normalised so only one suffix remains.
|
||||
"""
|
||||
try:
|
||||
charpos = pos - 1
|
||||
charpos = max(0, charpos)
|
||||
charpos = min(len(s) - 1, charpos)
|
||||
if appendix == s[charpos]:
|
||||
s = replace_dy_suffixes(s) + appendix
|
||||
pos = charpos
|
||||
except Exception:
|
||||
pass
|
||||
return pos, s
|
||||
|
||||
|
||||
def normalize_locktime_raw_text(
|
||||
text: str, pos: int
|
||||
) -> Tuple[str, bool, bool, int]:
|
||||
"""Sanitise the RAW locktime field text.
|
||||
|
||||
Only digits plus the day ("d") and year ("y") suffixes are kept; the block
|
||||
suffix ("b") is removed (A1). Exactly one ``d``/``y`` suffix survives.
|
||||
|
||||
Args:
|
||||
text: the raw field text.
|
||||
pos: the cursor position within ``text``.
|
||||
|
||||
Returns:
|
||||
``(clean, isdays, isyears, new_pos)`` where ``clean`` is the sanitised
|
||||
text and ``new_pos`` the adjusted cursor position.
|
||||
"""
|
||||
text = text.strip()
|
||||
chars = "0123456789dy"
|
||||
pos = len("".join([i for i in text[:pos] if i in chars]))
|
||||
s = "".join([i for i in text if i in chars])
|
||||
isdays = False
|
||||
isyears = False
|
||||
|
||||
pos, s = _checkbdy(s, pos, "d")
|
||||
pos, s = _checkbdy(s, pos, "y")
|
||||
|
||||
if "d" in s:
|
||||
isdays = True
|
||||
if "y" in s:
|
||||
isyears = True
|
||||
|
||||
if isdays:
|
||||
s = replace_dy_suffixes(s) + "d"
|
||||
if isyears:
|
||||
s = replace_dy_suffixes(s) + "y"
|
||||
return s, isdays, isyears, pos
|
||||
|
||||
|
||||
def normalize_perc_amount_text(text: str, decimal_point: str) -> Tuple[str, bool]:
|
||||
"""Sanitise the amount-or-percentage field text.
|
||||
|
||||
Keeps digits, ``%`` and the decimal point; a trailing ``%`` marks the value
|
||||
as a percentage (``is_perc``). At most 8 decimal digits after the point.
|
||||
|
||||
Args:
|
||||
text: the raw field text.
|
||||
decimal_point: the decimal separator character (``electrum`` uses
|
||||
``DECIMAL_POINT``, locale dependent).
|
||||
|
||||
Returns:
|
||||
``(clean, is_perc)``.
|
||||
"""
|
||||
text = text.strip()
|
||||
chars = "0123456789%"
|
||||
chars += decimal_point
|
||||
|
||||
s = "".join([i for i in text if i in chars])
|
||||
|
||||
if "%" in s:
|
||||
is_perc = True
|
||||
s = s.replace("%", "")
|
||||
else:
|
||||
is_perc = False
|
||||
|
||||
if decimal_point in s:
|
||||
p = s.find(decimal_point)
|
||||
s = s.replace(decimal_point, "")
|
||||
s = s[:p] + decimal_point + s[p : p + 8]
|
||||
if is_perc:
|
||||
s += "%"
|
||||
|
||||
return s, is_perc
|
||||
|
||||
|
||||
def parse_perc_amount(text: str, decimal_point: str) -> Union[None, Decimal, int]:
|
||||
"""Parse an amount-or-percentage field text into a numeric value.
|
||||
|
||||
Returns ``None`` when the text cannot be parsed.
|
||||
"""
|
||||
try:
|
||||
text = text.replace(decimal_point, ".")
|
||||
text = text.replace("%", "")
|
||||
return Decimal(text)
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user