Files
purple-electrumwallet/electrum/gui/qml/qeconfig.py
T
f321x aee0f8fb54 qml: OpenWalletDialog: load any wallet if password matches
If the user has wallets with different passwords (non-unified pw) and
enters a password on startup that fails to unlock the recently used
wallet this change will automatically open any other wallet if there
is another wallet that can be unlocked with this password.
2025-12-18 17:54:52 +01:00

404 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, languages
from electrum.logging import get_logger
from electrum.util import base_unit_name_to_decimal_point
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 languages:
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):
# sort on translated languages, then re-add Default on top
langs = copy.deepcopy(languages)
default = langs.pop('')
langs_sorted = sorted(list(map(lambda x: {'value': x[0], 'text': x[1]}, langs.items())), key=lambda x: x['text'])
langs_sorted.insert(0, {'value': '', 'text': default})
return langs_sorted
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('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("BTC") - 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()
pinCodeChanged = pyqtSignal()
@pyqtProperty(str, notify=pinCodeChanged)
def pinCode(self):
return self.config.CONFIG_PIN_CODE or ""
@pinCode.setter
def pinCode(self, pin_code):
if pin_code == '':
self.pinCodeRemoveAuth()
else:
self.config.CONFIG_PIN_CODE = pin_code
self.pinCodeChanged.emit()
@auth_protect(method='wallet_else_pin')
def pinCodeRemoveAuth(self):
self.config.CONFIG_PIN_CODE = ""
self.pinCodeChanged.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)