Files
purple-electrumwallet/electrum/gui/qml/qeconfig.py
T
davide b5fa01edfc fix: resolve UnknownBaseUnit crash in QML btcAmountRegex for non-BTC chains
Replace hardcoded \"BTC\" with get_base_units_list()[0] so the top-level
unit name is resolved dynamically from chain constants (e.g. \"BTCP\" for
BitcoinPurple), preventing the UnknownBaseUnit exception on receive screen.
2026-05-06 21:55:04 +02:00

409 lines
16 KiB
Python

import copy
from decimal import Decimal
from typing import TYPE_CHECKING
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QRegularExpression
from electrum.bitcoin import TOTAL_COIN_SUPPLY_LIMIT_IN_BTC
from electrum.i18n import set_language, get_gui_lang_names
from electrum.logging import get_logger
from electrum.util import base_unit_name_to_decimal_point, get_base_units_list
from electrum.gui import messages
from .qetypes import QEAmount
from .auth import AuthMixin, auth_protect
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
class QEConfig(AuthMixin, QObject):
instance = None # type: Optional[QEConfig]
_logger = get_logger(__name__)
def __init__(self, config: 'SimpleConfig', parent=None):
super().__init__(parent)
if QEConfig.instance:
raise RuntimeError('There should only be one QEConfig instance')
QEConfig.instance = self
self.config = config
@pyqtSlot(str, result=str)
def shortDescFor(self, key) -> str:
cv = getattr(self.config.cv, key)
return cv.get_short_desc() if cv else ''
@pyqtSlot(str, result=str)
def longDescFor(self, key) -> str:
cv = getattr(self.config.cv, key)
if not cv:
return ""
desc = cv.get_long_desc()
return messages.to_rtf(desc)
@pyqtSlot(str, result=str)
def getTranslatedMessage(self, key) -> str:
return getattr(messages, key)
languageChanged = pyqtSignal()
@pyqtProperty(str, notify=languageChanged)
def language(self):
return self.config.LOCALIZATION_LANGUAGE
@language.setter
def language(self, language):
if language not in get_gui_lang_names():
return
if self.config.LOCALIZATION_LANGUAGE != language:
self.config.LOCALIZATION_LANGUAGE = language
set_language(language)
self.languageChanged.emit()
languagesChanged = pyqtSignal()
@pyqtProperty('QVariantList', notify=languagesChanged)
def languagesAvailable(self):
langs = get_gui_lang_names()
langs_list = list(map(lambda x: {'value': x[0], 'text': x[1]}, langs.items()))
return langs_list
termsOfUseChanged = pyqtSignal()
@pyqtProperty(bool, notify=termsOfUseChanged)
def termsOfUseAccepted(self) -> bool:
return self.config.TERMS_OF_USE_ACCEPTED >= messages.TERMS_OF_USE_LATEST_VERSION
@termsOfUseAccepted.setter
def termsOfUseAccepted(self, accepted: bool) -> None:
if accepted:
self.config.TERMS_OF_USE_ACCEPTED = messages.TERMS_OF_USE_LATEST_VERSION
else:
self.config.TERMS_OF_USE_ACCEPTED = 0
self.termsOfUseChanged.emit()
baseUnitChanged = pyqtSignal()
@pyqtProperty(str, notify=baseUnitChanged)
def baseUnit(self):
return self.config.get_base_unit()
@baseUnit.setter
def baseUnit(self, unit):
self.config.set_base_unit(unit)
self.baseUnitChanged.emit()
@pyqtProperty('QVariantList', notify=baseUnitChanged)
def baseUnitsList(self):
from electrum.util import get_base_units_list
return get_base_units_list()
@pyqtProperty('QRegularExpression', notify=baseUnitChanged)
def btcAmountRegex(self):
return self._btcAmountRegex()
@pyqtProperty('QRegularExpression', notify=baseUnitChanged)
def btcAmountRegexMsat(self):
return self._btcAmountRegex(3)
def _btcAmountRegex(self, extra_precision: int = 0):
decimal_point = base_unit_name_to_decimal_point(self.config.get_base_unit())
max_digits_before_dp = (
len(str(TOTAL_COIN_SUPPLY_LIMIT_IN_BTC))
+ (base_unit_name_to_decimal_point(get_base_units_list()[0]) - decimal_point))
exp = '^[0-9]{0,%d}' % max_digits_before_dp
decimal_point += extra_precision
if decimal_point > 0:
exp += '(\\.[0-9]{0,%d})?' % decimal_point
exp += '$'
return QRegularExpression(exp)
thousandsSeparatorChanged = pyqtSignal()
@pyqtProperty(bool, notify=thousandsSeparatorChanged)
def thousandsSeparator(self):
return self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP
@thousandsSeparator.setter
def thousandsSeparator(self, checked):
self.config.BTC_AMOUNTS_ADD_THOUSANDS_SEP = checked
self.config.amt_add_thousands_sep = checked
self.thousandsSeparatorChanged.emit()
spendUnconfirmedChanged = pyqtSignal()
@pyqtProperty(bool, notify=spendUnconfirmedChanged)
def spendUnconfirmed(self):
return not self.config.WALLET_SPEND_CONFIRMED_ONLY
@spendUnconfirmed.setter
def spendUnconfirmed(self, checked):
self.config.WALLET_SPEND_CONFIRMED_ONLY = not checked
self.spendUnconfirmedChanged.emit()
freezeReusedAddressUtxosChanged = pyqtSignal()
@pyqtProperty(bool, notify=freezeReusedAddressUtxosChanged)
def freezeReusedAddressUtxos(self):
return self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS
@freezeReusedAddressUtxos.setter
def freezeReusedAddressUtxos(self, checked):
self.config.WALLET_FREEZE_REUSED_ADDRESS_UTXOS = checked
self.freezeReusedAddressUtxosChanged.emit()
requestExpiryChanged = pyqtSignal()
@pyqtProperty(int, notify=requestExpiryChanged)
def requestExpiry(self):
return self.config.WALLET_PAYREQ_EXPIRY_SECONDS
@requestExpiry.setter
def requestExpiry(self, expiry):
self.config.WALLET_PAYREQ_EXPIRY_SECONDS = expiry
self.requestExpiryChanged.emit()
paymentAuthenticationChanged = pyqtSignal()
@pyqtProperty(bool, notify=paymentAuthenticationChanged)
def paymentAuthentication(self):
return self.config.GUI_QML_PAYMENT_AUTHENTICATION
@paymentAuthentication.setter
def paymentAuthentication(self, enabled: bool):
if enabled:
self.config.GUI_QML_PAYMENT_AUTHENTICATION = True
self.paymentAuthenticationChanged.emit()
else:
self._disable_payment_authentication()
@auth_protect(method='wallet', reject='_payment_auth_reject')
def _disable_payment_authentication(self):
self.config.GUI_QML_PAYMENT_AUTHENTICATION = False
self.paymentAuthenticationChanged.emit()
def _payment_auth_reject(self):
self.paymentAuthenticationChanged.emit()
useGossipChanged = pyqtSignal()
@pyqtProperty(bool, notify=useGossipChanged)
def useGossip(self):
return self.config.LIGHTNING_USE_GOSSIP
@useGossip.setter
def useGossip(self, gossip):
self.config.LIGHTNING_USE_GOSSIP = gossip
self.useGossipChanged.emit()
enableDebugLogsChanged = pyqtSignal()
@pyqtProperty(bool, notify=enableDebugLogsChanged)
def enableDebugLogs(self):
gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
return gui_setting or bool(self.config.get('verbosity'))
@pyqtProperty(bool, notify=enableDebugLogsChanged)
def canToggleDebugLogs(self):
gui_setting = self.config.GUI_ENABLE_DEBUG_LOGS
return not self.config.get('verbosity') or gui_setting
@enableDebugLogs.setter
def enableDebugLogs(self, enable):
self.config.GUI_ENABLE_DEBUG_LOGS = enable
self.enableDebugLogsChanged.emit()
alwaysAllowScreenshotsChanged = pyqtSignal()
@pyqtProperty(bool, notify=alwaysAllowScreenshotsChanged)
def alwaysAllowScreenshots(self):
return self.config.GUI_QML_ALWAYS_ALLOW_SCREENSHOTS
@alwaysAllowScreenshots.setter
def alwaysAllowScreenshots(self, enable):
self.config.GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = enable
self.alwaysAllowScreenshotsChanged.emit()
setMaxBrightnessOnQrDisplayChanged = pyqtSignal()
@pyqtProperty(bool, notify=setMaxBrightnessOnQrDisplayChanged)
def setMaxBrightnessOnQrDisplay(self):
return self.config.GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY
@setMaxBrightnessOnQrDisplay.setter
def setMaxBrightnessOnQrDisplay(self, enable):
self.config.GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = enable
useRecoverableChannelsChanged = pyqtSignal()
@pyqtProperty(bool, notify=useRecoverableChannelsChanged)
def useRecoverableChannels(self):
return self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS
@useRecoverableChannels.setter
def useRecoverableChannels(self, useRecoverableChannels):
self.config.LIGHTNING_USE_RECOVERABLE_CHANNELS = useRecoverableChannels
self.useRecoverableChannelsChanged.emit()
trustedcoinPrepayChanged = pyqtSignal()
@pyqtProperty(int, notify=trustedcoinPrepayChanged)
def trustedcoinPrepay(self):
return self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY
@trustedcoinPrepay.setter
def trustedcoinPrepay(self, num_prepay):
if num_prepay != self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY:
self.config.PLUGIN_TRUSTEDCOIN_NUM_PREPAY = num_prepay
self.trustedcoinPrepayChanged.emit()
preferredRequestTypeChanged = pyqtSignal()
@pyqtProperty(str, notify=preferredRequestTypeChanged)
def preferredRequestType(self):
return self.config.GUI_QML_PREFERRED_REQUEST_TYPE
@preferredRequestType.setter
def preferredRequestType(self, preferred_request_type):
if preferred_request_type != self.config.GUI_QML_PREFERRED_REQUEST_TYPE:
self.config.GUI_QML_PREFERRED_REQUEST_TYPE = preferred_request_type
self.preferredRequestTypeChanged.emit()
userKnowsPressAndHoldChanged = pyqtSignal()
@pyqtProperty(bool, notify=userKnowsPressAndHoldChanged)
def userKnowsPressAndHold(self):
return self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD
@userKnowsPressAndHold.setter
def userKnowsPressAndHold(self, userKnowsPressAndHold):
if userKnowsPressAndHold != self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD:
self.config.GUI_QML_USER_KNOWS_PRESS_AND_HOLD = userKnowsPressAndHold
self.userKnowsPressAndHoldChanged.emit()
addresslistShowTypeChanged = pyqtSignal()
@pyqtProperty(int, notify=addresslistShowTypeChanged)
def addresslistShowType(self):
return self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE
@addresslistShowType.setter
def addresslistShowType(self, addresslistShowType):
if addresslistShowType != self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE:
self.config.GUI_QML_ADDRESS_LIST_SHOW_TYPE = addresslistShowType
self.addresslistShowTypeChanged.emit()
addresslistShowUsedChanged = pyqtSignal()
@pyqtProperty(bool, notify=addresslistShowUsedChanged)
def addresslistShowUsed(self):
return self.config.GUI_QML_ADDRESS_LIST_SHOW_USED
@addresslistShowUsed.setter
def addresslistShowUsed(self, addresslistShowUsed):
if addresslistShowUsed != self.config.GUI_QML_ADDRESS_LIST_SHOW_USED:
self.config.GUI_QML_ADDRESS_LIST_SHOW_USED = addresslistShowUsed
self.addresslistShowUsedChanged.emit()
outputValueRoundingChanged = pyqtSignal()
@pyqtProperty(bool, notify=outputValueRoundingChanged)
def outputValueRounding(self):
return self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING
@outputValueRounding.setter
def outputValueRounding(self, outputValueRounding):
if outputValueRounding != self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING:
self.config.WALLET_COIN_CHOOSER_OUTPUT_ROUNDING = outputValueRounding
self.outputValueRoundingChanged.emit()
lightningPaymentFeeMaxMillionthsChanged = pyqtSignal()
@pyqtProperty(int, notify=lightningPaymentFeeMaxMillionthsChanged)
def lightningPaymentFeeMaxMillionths(self):
return self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS
@lightningPaymentFeeMaxMillionths.setter
def lightningPaymentFeeMaxMillionths(self, lightningPaymentFeeMaxMillionths):
if lightningPaymentFeeMaxMillionths != self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS:
self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = lightningPaymentFeeMaxMillionths
self.lightningPaymentFeeMaxMillionthsChanged.emit()
nostrRelaysChanged = pyqtSignal()
@pyqtProperty(str, notify=nostrRelaysChanged)
def nostrRelays(self):
return self.config.NOSTR_RELAYS
@nostrRelays.setter
def nostrRelays(self, nostr_relays):
if nostr_relays != self.config.NOSTR_RELAYS:
self.config.NOSTR_RELAYS = nostr_relays if nostr_relays else None
self.nostrRelaysChanged.emit()
swapServerNPubChanged = pyqtSignal()
@pyqtProperty(str, notify=swapServerNPubChanged)
def swapServerNPub(self):
return self.config.SWAPSERVER_NPUB
@swapServerNPub.setter
def swapServerNPub(self, swapserver_npub):
if swapserver_npub != self.config.SWAPSERVER_NPUB:
self.config.SWAPSERVER_NPUB = swapserver_npub
self.swapServerNPubChanged.emit()
lnUtxoReserveChanged = pyqtSignal()
@pyqtProperty(QEAmount, notify=lnUtxoReserveChanged)
def lnUtxoReserve(self):
self._lnutxoreserve = QEAmount(amount_sat=self.config.LN_UTXO_RESERVE)
return self._lnutxoreserve
walletShouldUseSinglePasswordChanged = pyqtSignal()
@pyqtProperty(bool, notify=walletShouldUseSinglePasswordChanged)
def walletShouldUseSinglePassword(self):
"""
NOTE: this only indicates if we even want to use a single password, to check if we
actually use a single password the daemon needs to be checked.
"""
return self.config.WALLET_SHOULD_USE_SINGLE_PASSWORD
walletDidUseSinglePasswordChanged = pyqtSignal()
@pyqtProperty(bool, notify=walletDidUseSinglePasswordChanged)
def walletDidUseSinglePassword(self):
"""
Allows to guess if this is a unified password instance without having
unlocked any wallet yet. Might be out of sync e.g. if wallet files get copied manually.
"""
# TODO: consider removing once encrypted wallet file headers are available
return self.config.WALLET_DID_USE_SINGLE_PASSWORD
@pyqtSlot('qint64', result=str)
@pyqtSlot(QEAmount, result=str)
def formatSatsForEditing(self, satoshis):
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
return self.config.format_amount(
satoshis,
add_thousands_sep=False,
)
@pyqtSlot('qint64', result=str)
@pyqtSlot('qint64', bool, result=str)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def formatSats(self, satoshis, with_unit=False):
if isinstance(satoshis, QEAmount):
satoshis = satoshis.satsInt
if with_unit:
return self.config.format_amount_and_units(satoshis)
else:
return self.config.format_amount(satoshis)
@pyqtSlot(QEAmount, result=str)
@pyqtSlot(QEAmount, bool, result=str)
def formatMilliSats(self, amount, with_unit=False):
assert isinstance(amount, QEAmount), f"unexpected type for amount: {type(amount)}"
msats = amount.msatsInt
precision = 3 # config.amt_precision_post_satoshi is not exposed in preferences
if with_unit:
return self.config.format_amount_and_units(msats/1000, precision=precision)
else:
return self.config.format_amount(msats/1000, precision=precision)
@pyqtSlot(str, result=QEAmount)
def unitsToSats(self, unitAmount):
self._amount = QEAmount()
try:
x = Decimal(unitAmount)
except Exception:
return self._amount
sat_max_precision = self.config.BTC_AMOUNTS_DECIMAL_POINT
msat_max_precision = self.config.BTC_AMOUNTS_DECIMAL_POINT + 3
sat_max_prec_amount = int(pow(10, sat_max_precision) * x)
msat_max_prec_amount = int(pow(10, msat_max_precision) * x)
self._amount = QEAmount(amount_sat=sat_max_prec_amount, amount_msat=msat_max_prec_amount)
return self._amount
@pyqtSlot('quint64', result=float)
def satsToUnits(self, satoshis):
return satoshis / pow(10, self.config.BTC_AMOUNTS_DECIMAL_POINT)