core+gui: timezone-correct datetimes, deep-copy WillItem, dead code removal, explicit imports

This commit is contained in:
2026-08-19 20:55:53 -04:00
parent 5b109384cf
commit 0bacbf1997
30 changed files with 1435 additions and 10150 deletions

View File

@@ -10,7 +10,7 @@ Pure, GUI-free. The GUI raises :class:`CheckAliveError` to trigger the
postpone/invalidate flow; the decision that it *should* be raised lives here.
"""
from datetime import datetime
from datetime import datetime, timezone
from typing import Any
from .plugin_base import BalTimestamp
@@ -24,7 +24,7 @@ class CheckAliveError(Exception):
def __str__(self):
return "Check alive expired please update it: {}".format(
datetime.fromtimestamp(self.timestamp_to_check).isoformat()
datetime.fromtimestamp(self.timestamp_to_check, tz=timezone.utc).isoformat()
)
@@ -70,7 +70,7 @@ def resolve_date_to_check(
The reference timestamp (float, UNIX seconds).
"""
if is_basic_mode:
return (now if now is not None else datetime.now().timestamp())
return (now if now is not None else datetime.now(tz=timezone.utc).timestamp())
threshold = BalTimestamp(will_settings["threshold"])
# A RELATIVE threshold ("30d"/"1y") means "N days BEFORE the delivery":
@@ -107,5 +107,5 @@ def check_alive_expired(
"""
if is_basic_mode:
return False
current = now if now is not None else datetime.now().timestamp()
current = now if now is not None else datetime.now(tz=timezone.utc).timestamp()
return date_to_check < current

View File

@@ -24,7 +24,7 @@ This module performs **no** GUI work and imports nothing from PyQt / electrum.gu
import json
import os
import platform
from datetime import date, datetime, timedelta
from datetime import date, datetime, timedelta, timezone
from electrum import constants, json_db
from electrum.logging import get_logger
@@ -460,8 +460,8 @@ class BalPlugin(BasePlugin):
def default_will_settings_absolute():
"""Convert the default relative dates into absolute timestamps (from today)."""
relative_dates = BalPlugin.default_will_settings_relative()
today = date.today()
dt = datetime(today.year, today.month, today.day, 0, 0, 0)
today = datetime.now(tz=timezone.utc).date()
dt = datetime(today.year, today.month, today.day, 0, 0, 0, tzinfo=timezone.utc)
threshold = (
dt + timedelta(days=BalTimestamp(relative_dates["threshold"]).duration_to_days())
).timestamp()
@@ -521,12 +521,12 @@ class BalTimestamp:
"""
int32_max = 2 ** 31 - 1
try:
return datetime.fromtimestamp(ts)
return datetime.fromtimestamp(ts, tz=timezone.utc)
except (OSError, OverflowError, ValueError):
try:
return datetime.fromtimestamp(min(int(ts), int32_max))
return datetime.fromtimestamp(min(int(ts), int32_max), tz=timezone.utc)
except (OSError, OverflowError, ValueError):
return datetime.fromtimestamp(int32_max)
return datetime.fromtimestamp(int32_max, tz=timezone.utc)
def to_date(self, from_date=None, reverse=False):
"""Resolve to a ``datetime``.
@@ -539,7 +539,7 @@ class BalTimestamp:
return self._safe_fromtimestamp(self.value)
else:
if from_date is None:
from_date = datetime.now()
from_date = datetime.now(tz=timezone.utc)
if isinstance(from_date, (int, float)):
from_date = self._safe_fromtimestamp(from_date)
reverse = 1 if not reverse else -1

View File

@@ -38,7 +38,7 @@ def compute_reminder_offsets(days, count):
count: requested number of reminders.
Returns:
A list of integer day-offsets (each ``>= 1``), e.g. ``[22, 15, 8]`` for
A list of integer day-offsets (each ``>= 1``), e.g. ``[30, 16, 1]`` for
``days=30, count=3``. Empty if there is no room for any reminder.
"""
# No room for any reminder (deadline today or already passed).

View File

@@ -18,7 +18,7 @@ original implementation.
"""
import bisect
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL
from electrum.transaction import PartialTxOutput
@@ -103,7 +103,7 @@ class Util:
except Exception:
pass
try:
now = datetime.now()
now = datetime.now(tz=timezone.utc)
if locktime[-1] == "y":
locktime = str(int(locktime[:-1]) * 365) + "d"
if locktime[-1] == "d":
@@ -189,7 +189,7 @@ class Util:
# moment, so fall back to the legacy forward-from-now resolution.
return Util.parse_locktime_string(current)
try:
base = datetime.fromtimestamp(int(tx_locktime)).replace(
base = datetime.fromtimestamp(int(tx_locktime), tz=timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
)
build_moment = base - timedelta(days=built_days)
@@ -440,9 +440,9 @@ class Util:
# On Windows datetime.fromtimestamp raises OverflowError past 2038
# (e.g. NLOCKTIME_MAX); clamp to INT32_MAX (Electrum issue #6170).
try:
dt = datetime.fromtimestamp(locktime)
dt = datetime.fromtimestamp(locktime, tz=timezone.utc)
except (OverflowError, OSError, ValueError):
dt = datetime.fromtimestamp(min(locktime, 2 ** 31 - 1))
dt = datetime.fromtimestamp(min(locktime, 2 ** 31 - 1), tz=timezone.utc)
dt -= timedelta(seconds=seconds)
out = dt.timestamp()
@@ -450,34 +450,6 @@ class Util:
out = 1
return out
@staticmethod
def cmp_locktime(locktimea, locktimeb):
"""Compare two relative locktime strings sharing the same unit."""
if locktimea == locktimeb:
return 0
strlocktimea = str(locktimea)
strlocktimeb = str(locktimeb)
if locktimea[-1] in "ydb":
if locktimeb[-1] == locktimea[-1]:
return int(strlocktimea[-1]) - int(strlocktimeb[-1])
else:
return int(locktimea) - (locktimeb)
@staticmethod
def get_lowest_valid_tx(available_utxos, will):
"""Placeholder kept from the original code (sorts the will by locktime)."""
will = sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
for _txid, _willitem in will.items():
pass
@staticmethod
def get_locktimes(will):
"""Return the distinct locktimes used by the transactions in ``will``."""
locktimes = {}
for _, willitem in will.items():
locktimes[willitem["tx"].locktime] = True
return locktimes.keys()
@staticmethod
def get_lowest_locktimes(locktimes):
"""Split a list of locktimes into (sorted_timestamps, sorted_blocks)."""
@@ -492,32 +464,6 @@ class Util:
return sorted(sorted_timestamp), sorted(sorted_block)
@staticmethod
def get_lowest_locktimes_from_will(will):
"""Convenience wrapper: lowest locktimes directly from a will dict."""
return Util.get_lowest_locktimes(Util.get_locktimes(will))
@staticmethod
def search_willtx_per_io(will, tx):
"""Find a will entry whose tx has the same inputs/outputs as ``tx``."""
for wid, w in will.items():
if Util.cmp_txs(w["tx"], tx["tx"]):
return wid, w
return None, None
@staticmethod
def invalidate_will(will):
raise Exception("not implemented")
@staticmethod
def get_will_spent_utxos(will):
"""Collect every input spent by any transaction in ``will``."""
utxos = []
for _, willitem in will.items():
utxos += willitem["tx"].inputs()
return utxos
# ------------------------------------------------------------------ #
# UTXO helpers
# ------------------------------------------------------------------ #

View File

@@ -74,11 +74,6 @@ class Will:
if not will[child[0]].father:
will[child[0]].father = willid
# return a list of will sorted by locktime
@staticmethod
def get_sorted_will(will):
return sorted(will.items(), key=lambda x: x[1]["tx"].locktime)
@staticmethod
def only_valid(will):
for k, v in will.items():
@@ -107,15 +102,6 @@ class Will:
and not w.get_status("CHECKED")
)
@staticmethod
def search_equal_tx(will, tx, wid):
for w in will:
if w != wid and not tx.to_json() != will[w]["tx"].to_json():
if will[w]["tx"].txid() != tx.txid():
if Util.cmp_txs(will[w]["tx"], tx):
return will[w]["tx"]
return False
@staticmethod
def get_tx_from_any(x):
try:
@@ -516,6 +502,7 @@ class Will:
for _wid, w in will.items():
if w.get_status("VALID") and not w.get_status("COMPLETE"):
return True
return False
@staticmethod
def search_rai(all_inputs, all_utxos, will, wallet):
@@ -1345,6 +1332,8 @@ class WillItem(Logger):
WillItem,
):
self.__dict__ = w.__dict__.copy()
self.STATUS = copy.deepcopy(w.STATUS)
self.heirs = copy.deepcopy(w.heirs) if w.heirs is not None else None
else:
self.tx = Will.get_tx_from_any(w["tx"])
self.heirs = w.get("heirs", None)

View File

@@ -13,12 +13,16 @@ The pure RFC-5545 logic (offsets, escaping, folding, the unified .ics builder,
the Qt button and the OS/subprocess glue.
"""
import os
import subprocess
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import QToolButton
from PyQt6.QtWidgets import QInputDialog, QMenu, QToolButton
from electrum.gui.qt.util import getSaveFileName
from ...core.reminders import write_temp_ics
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
from .common import _, _logger
class BalCalendarButton(QToolButton):

View File

@@ -22,8 +22,61 @@ from typing import TYPE_CHECKING
from ...core.checkalive import CheckAliveError
from ...core.reminders import build_ics_reminders
from .calendar import BalCalendarButton
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
from .common import (
_,
_logger,
AmountException,
Any,
BalTimestamp,
BestEffortRequestFailed,
Buttons,
Callable,
CancelButton,
HEIR_DUST_AMOUNT,
HEIR_REAL_AMOUNT,
HeirAmountIsDustException,
HeirChangeException,
HeirNotFoundException,
MessageBoxMixin,
Network,
NoHeirsException,
NoWillExecutorNotPresent,
NotCompleteWillException,
QComboBox,
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QScrollArea,
QSizePolicy,
QTimer,
QVBoxLayout,
QWidget,
Qt,
TaskThread,
TxBroadcastError,
TxFeesChangedException,
Util,
Will,
WillExecutorFeeTooHighException,
WillExecutorNotPresent,
WillExpiredException,
WillPostponedException,
WillexecutorChangeException,
Willexecutors,
bring_to_front,
decimal_point_to_base_unit_name,
import_meta_gui,
partial,
pyqtSignal,
read_QIcon_from_bytes,
read_json_file,
show_modal,
show_on_top,
stop_thread,
time,
top_level_of,
)
from .widgets import (
WillSettingsWidget,
WillWidget,

View File

@@ -19,8 +19,59 @@ from typing import TYPE_CHECKING
from PyQt6.QtWidgets import QLineEdit as _QLineEdit
from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
from .common import (
_,
_logger,
BalTimestamp,
Buttons,
CancelButton,
HelpButton,
MessageBoxMixin,
MyTreeView,
OP_RETURN_PREFIX,
OkButton,
QAbstractItemView,
QApplication,
QColor,
QGridLayout,
QHBoxLayout,
QLabel,
QLineEdit,
QMenu,
QModelIndex,
QPersistentModelIndex,
QPushButton,
QSize,
QSizePolicy,
QSpinBox,
QStandardItem,
QStandardItemModel,
QToolButton,
QVBoxLayout,
QWidget,
Qt,
TaskThread,
Util,
Will,
WillItem,
Willexecutors,
char_width_in_lineedit,
datetime,
enum,
export_meta_gui,
getOpenFileName,
import_meta_gui,
is_op_return_address,
partial,
read_QIcon_from_bytes,
read_json_file,
server_status_text,
server_status_tooltip,
signature_suffix,
status_color,
tx_from_any,
write_json_file,
)
from .dialogs import BalBuildWillDialog, BalDialog
from .widgets import BalCheckBox, WillSettingsWidget

View File

@@ -15,14 +15,35 @@ and cached in ``self.bal_windows``.
"""
from electrum.gui.qt.main_window import StatusBarButton
from electrum.plugin import hook
from electrum.util import EventListener, event_listener
from PyQt6.QtWidgets import QLayout
from .common import *
from .common import ( # underscore names are not re-exported by "import *"
from .common import (
_,
_logger,
BalPlugin,
Buttons,
EnterButton,
HelpButton,
PasswordDialog,
QComboBox,
QGridLayout,
QHBoxLayout,
QInputDialog,
QLabel,
QPushButton,
QTimer,
QVBoxLayout,
QWidget,
UserCancelled,
Willexecutors,
add_widget,
partial,
read_QIcon_from_bytes,
read_QPixmap_from_bytes,
show_modal,
webopen,
)
from .dialogs import BalDialog
from .widgets import BalCheckBox, BalLineEdit, BalSpinBox, BalTextEdit

View File

@@ -28,8 +28,53 @@ from ...core.input_rules import (
)
from ...core.reminders import build_ics_reminders, write_temp_ics
from .calendar import BalCalendar, BalCalendarButton
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
from .common import (
_,
_logger,
Any,
BTCAmountEdit,
BalTimestamp,
ColorScheme,
DECIMAL_POINT,
Decimal,
HelpButton,
NLOCKTIME_BLOCKHEIGHT_MAX,
NLOCKTIME_MAX,
Optional,
QAbstractSpinBox,
QCheckBox,
QColor,
QComboBox,
QDateTime,
QDateTimeEdit,
QHBoxLayout,
QLabel,
QLineEdit,
QPainter,
QPalette,
QPushButton,
QSizePolicy,
QSpinBox,
QStyle,
QStyleOptionFrame,
QTextEdit,
QVBoxLayout,
QWidget,
Qt,
Union,
Util,
Will,
char_width_in_lineedit,
datetime,
getSaveFileName,
log_error,
os,
partial,
pyqtSignal,
read_QIcon_from_bytes,
signature_suffix,
status_color,
)
if TYPE_CHECKING:
from .window import BalWindow

View File

@@ -24,8 +24,63 @@ from ...core.checkalive import (
check_alive_expired,
resolve_date_to_check,
)
from .common import *
from .common import _, _logger # underscore names are not re-exported by "import *"
from .common import (
_,
_logger,
AmountException,
BalPlugin,
Buttons,
CancelButton,
ElectrumWindow,
FileImportFailed,
HeirChangeException,
HeirNotFoundException,
Heirs,
HelpButton,
Mapping,
Network,
NoHeirsException,
NoWillExecutorNotPresent,
NotCompleteWillException,
OP_RETURN_PREFIX,
OkButton,
PaymentIdentifier,
QGridLayout,
QLabel,
QLineEdit,
QPushButton,
QTimer,
QVBoxLayout,
SerializationError,
Transaction,
TxDialog,
TxFeesChangedException,
Util,
Will,
WillExecutorFeeTooHighException,
WillExecutorNotPresent,
WillExpiredException,
WillItem,
WillPostponedException,
WillexecutorChangeException,
Willexecutors,
char_width_in_lineedit,
copy,
export_meta_gui,
import_meta_gui,
is_onion_url,
is_op_return_address,
is_tor_active,
log_error,
partial,
read_QIcon_from_bytes,
read_json_file,
show_on_top,
shown_cv,
time,
tx_from_any,
write_json_file,
)
from .dialogs import (
BalBuildWillDialog,
BalDialog,

View File

@@ -1,7 +1,7 @@
{
"name": "bal",
"fullname": "Bitcoin After Life",
"version": "0.6.1",
"version": "0.7.0",
"description": "Provides free and decentralized Bitcoin inheritance support. Build time-locked 'will' transactions that transfer funds to your heirs if you stop refreshing them (dead-man's switch), optionally relayed by will-executor servers.",
"author": "Svatantrya",
"licence": "MIT",