Files
bal-electrum-plugin/bal/gui/qt/common.py
bitcoinafterlife 42f05d3c4f core+gui: name the real cause of a failed build instead of guessing
The Building Will report showed a fixed list of three "possible reasons"
whenever a build produced nothing, regardless of what actually happened; in a
case reproduced from the owner log all three were false and the real cause was
not even listed. The "Checking your will" row had the same flaw, showing one
sentence ("Found CHANGES to the DATE or the HEIRS") for five situations,
including one where it is plainly wrong (funds received).

core/heirs.py: record WHY buildTransactions gave up in a new last_build_error
attribute (8 reason codes), set at each path that previously returned empty
with no explanation, plus a processed_willexecutors counter to tell "every
will-executor was skipped" apart from "we tried and failed". Also fix a latent
crash in the prepare_transactions handler, which read a no-longer-existing
e.heirname attribute and re-raised the resulting AttributeError, masking the
real error.

gui/qt/dialogs.py: add msg_alert() (amber warning sign, body text in the theme
colour, readable in both themes), _build_failure_message() and
_check_failure_message() to turn those causes into one precise sentence each,
with an honest "cause could not be determined" fallback. Catch
BalanceTooLowException, which already carried the figures but fell through to
the generic red technical error.

No new exception classes were introduced (owner request): the plain
NotCompleteWillException cases are told apart structurally, not by text.
2026-09-04 11:52:21 +02:00

241 lines
6.1 KiB
Python

"""
bal.gui.qt.common
=================
Shared imports and tiny helper utilities for the Qt GUI layer.
Every other ``bal.gui.qt`` module does ``from .common import *`` so that the
long list of Electrum / PyQt6 imports lives in a single place. This file also
hosts a few GUI helpers that do not deserve a module of their own:
* :class:`shown_cv` - trivial mutable "is this tab shown?" holder.
* :func:`add_widget` - add a labelled widget (plus optional help) to a grid.
* :func:`log_error` - format an exception traceback for a dialog.
* :func:`export_meta_gui` - export plugin metadata to a JSON file.
(:class:`CheckAliveError` now lives in ``bal.core.checkalive``.)
"""
import copy
import enum
import os
import subprocess
import tempfile
import time
import traceback
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from functools import partial
from typing import Any, Callable, Mapping, Optional, Union
from electrum.bitcoin import NLOCKTIME_BLOCKHEIGHT_MAX, NLOCKTIME_MAX, NLOCKTIME_MIN
from electrum.gui.qt.amountedit import BTCAmountEdit
from electrum.gui.qt.main_window import ElectrumWindow, StatusBarButton
from electrum.gui.qt.my_treeview import MyTreeView
from electrum.gui.qt.password_dialog import PasswordDialog
from electrum.gui.qt.transaction_dialog import TxDialog
from electrum.gui.qt.util import (
Buttons,
CancelButton,
ColorScheme,
EnterButton,
HelpButton,
MessageBoxMixin,
OkButton,
TaskThread,
WindowModalDialog,
char_width_in_lineedit,
getOpenFileName,
getSaveFileName,
import_meta_gui,
read_QIcon_from_bytes,
read_QPixmap_from_bytes,
webopen,
)
from electrum.i18n import _
from electrum.logging import get_logger
from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError
from electrum.payment_identifier import PaymentIdentifier
from electrum.plugin import hook
from electrum.transaction import SerializationError, Transaction, tx_from_any
from electrum.util import (
DECIMAL_POINT,
FileExportFailed,
FileImportFailed,
UserCancelled,
decimal_point_to_base_unit_name,
read_json_file,
write_json_file,
)
from PyQt6.QtCore import (
QDateTime,
QModelIndex,
QPersistentModelIndex,
QSize,
Qt,
QTimer,
pyqtSignal,
)
from PyQt6.QtGui import QColor, QPainter, QPalette, QStandardItem, QStandardItemModel
from PyQt6.QtWidgets import (
QAbstractItemView,
QAbstractSpinBox,
QApplication,
QCheckBox,
QComboBox,
QDateTimeEdit,
QDialog,
QGridLayout,
QHBoxLayout,
QInputDialog,
QLabel,
QLineEdit,
QMenu,
QMenuBar,
QPushButton,
QScrollArea,
QSizePolicy,
QSpinBox,
QStackedWidget,
QStyle,
QStyleOptionFrame,
QTextEdit,
QToolButton,
QVBoxLayout,
QWidget,
)
from ...core.heirs import (
HEIR_DUST_AMOUNT,
HEIR_REAL_AMOUNT,
OP_RETURN_PREFIX,
BalanceTooLowException,
HeirAmountIsDustException,
Heirs,
WillExecutorFeeTooHighException,
get_op_return_hex,
is_op_return_address,
validate_op_return_hex,
)
# --- Core (GUI-free) logic layer ---
from ...core.plugin_base import BalPlugin, BalTimestamp
from ...core.util import Util
from ...core.will import (
AmountException,
HeirChangeException,
HeirNotFoundException,
NoHeirsException,
NotCompleteWillException,
NoWillExecutorNotPresent,
TxFeesChangedException,
Will,
WillexecutorChangeException,
WillExecutorNotPresent,
WillExpiredException,
WillItem,
WillPostponedException,
)
from ...core.willexecutors import ( # noqa: F401
Willexecutors,
is_onion_url,
is_tor_active,
)
# --- Presentation helpers ---
from .theme import (
server_status_text,
server_status_tooltip,
signature_suffix,
status_color,
)
from .window_utils import (
bring_to_front,
show_modal,
show_on_top,
stop_thread,
top_level_of,
)
_logger = get_logger(__name__)
class shown_cv: # noqa: N801 (intentionally lowercase: mirrors Qt signal naming)
_type = bool
def __init__(self, value):
self.value = value
def get(self):
return self.value
def set(self, value):
self.value = value
def add_widget(grid, label, widget, row, help_):
grid.addWidget(QLabel(_(label)), row, 0)
grid.addWidget(widget, row, 1)
grid.addWidget(HelpButton(help_), row, 2)
def log_error(exec_info, window=None):
"""Log an error and optionally show it.
``exec_info`` may be either a ``sys.exc_info()`` triple
``(type, value, traceback)`` or a single exception instance (callers use
both forms), so we handle both and always try to log a full traceback.
"""
_logger.error(f"LOG_ERROR: {exec_info}")
exc = None
if isinstance(exec_info, BaseException):
exc = exec_info
elif isinstance(exec_info, (tuple, list)) and len(exec_info) >= 2:
# sys.exc_info() form: the exception instance is the 2nd element.
exc = exec_info[1]
try:
if exc is not None:
_logger.error(
"".join(
traceback.format_exception(type(exc), exc, exc.__traceback__)
)
)
else:
_logger.error(traceback.format_exc())
except Exception:
_logger.error(traceback.format_exc())
if window is not None:
# show_error expects a human-readable message, not a triple.
window.show_error(str(exc) if exc is not None else str(exec_info))
def export_meta_gui(electrum_window, title, exporter):
filter_ = "All files (*)"
filename = getSaveFileName(
parent=electrum_window,
title=_("Select file to save your {}".format(title)),
filename="BALplugin_{}_{}_{}".format(
BalPlugin.chainname, str(electrum_window.wallet), title
),
filter=filter_,
config=electrum_window.config,
)
if not filename:
return
try:
exporter(filename)
except FileExportFailed as e:
electrum_window.show_critical(str(e))
else:
electrum_window.show_message(
_("Your {0} were exported to '{1}'".format(title, str(filename)))
)