b5fa01edfc
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.
409 lines
16 KiB
Python
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)
|