qml: remove pin code authentication

Completely removes the pin code authentication from qml. The config
option in the wallet preferences has been renamed to "Payment
authentication" and now either asks for the Android system
authentication (Biometric or system pin/password) if enabled or will ask
for the wallet password as fallback.
This commit is contained in:
user
2026-01-15 11:30:09 +01:00
committed by f321x
parent 6450187902
commit 47efb8b108
11 changed files with 176 additions and 283 deletions
+12 -1
View File
@@ -5,7 +5,18 @@ from PyQt6.QtCore import pyqtSignal, pyqtSlot
from electrum.logging import get_logger
def auth_protect(func=None, reject=None, method='pin', message=''):
def auth_protect(func=None, reject=None, method='payment_auth', message=''):
"""
Supported methods:
* payment_auth: If the user has enabled the 'Payment authentication' config
they need to authenticate to continue. If biometrics are enabled they
can authenticate using the Android system dialog, else they will see the
wallet password dialog.
If the option is disabled they will have to confirm a dialog.
* wallet: Same as payment_auth, but not dependent on user configuration,
always requires authentication.
* wallet_password_only: No biometric/system authentication, user has to enter wallet password.
"""
if func is None:
return partial(auth_protect, reject=reject, method=method, message=message)
-114
View File
@@ -1,114 +0,0 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.Material
import org.electrum 1.0
import "controls"
ElDialog {
id: root
property bool canCancel: true
property string mode // [check, enter, change]
property string pincode // old one passed in when change, new one passed out
property bool checkError: false
property string authMessage
property int _phase: mode == 'enter' ? 1 : 0 // 0 = existing pin, 1 = new pin, 2 = re-enter new pin
property string _pin
title: authMessage ? authMessage : qsTr('PIN')
iconSource: Qt.resolvedUrl('../../icons/lock.png')
width: parent.width * 3/4
z: 1000
focus: true
closePolicy: canCancel ? Popup.CloseOnEscape | Popup.CloseOnPressOutside : Popup.NoAutoClose
allowClose: canCancel
needsSystemBarPadding: false
anchors.centerIn: parent
Overlay.modal: Rectangle {
color: canCancel ? "#aa000000" : "#ff000000"
}
function submit() {
if (_phase == 0) {
if (pin.text == pincode) {
pin.text = ''
if (mode == 'check')
accepted()
else
_phase = 1
} else {
pin.text = ''
checkError = true
}
} else if (_phase == 1) {
_pin = pin.text
pin.text = ''
_phase = 2
} else if (_phase == 2) {
if (_pin == pin.text) {
pincode = pin.text
accepted()
} else {
pin.text = ''
checkError = true
}
}
}
onAccepted: result = Dialog.Accepted
onRejected: result = Dialog.Rejected
onClosed: {
if (!root.result) {
root.reject() // make sure we reject the authed fn()
}
}
ColumnLayout {
width: parent.width
Label {
text: [qsTr('Enter PIN'), qsTr('Enter New PIN'), qsTr('Re-enter New PIN')][_phase]
font.pixelSize: constants.fontSizeXLarge
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
Layout.fillWidth: true
}
TextField {
id: pin
Layout.preferredWidth: fontMetrics.advanceWidth(passwordCharacter) * 6 + pin.leftPadding + pin.rightPadding
Layout.preferredHeight: fontMetrics.height + pin.topPadding + pin.bottomPadding
Layout.alignment: Qt.AlignHCenter
font.pixelSize: constants.fontSizeXXLarge
maximumLength: 6
inputMethodHints: Qt.ImhDigitsOnly
echoMode: TextInput.Password
focus: true
onTextChanged: {
checkError = false
if (text.length == 6) {
submit()
}
}
}
Label {
opacity: checkError ? 1 : 0
text: _phase == 0 ? qsTr('Wrong PIN') : qsTr('PIN doesn\'t match')
color: constants.colorError
Layout.alignment: Qt.AlignHCenter
}
}
FontMetrics {
id: fontMetrics
font: pin.font
}
}
+30 -64
View File
@@ -157,62 +157,6 @@ Pane {
}
}
RowLayout {
Layout.fillWidth: true
spacing: 0
Switch {
id: usePin
checked: Config.pinCode
onCheckedChanged: {
if (activeFocus) {
console.log('PIN active ' + checked)
if (checked) {
var dialog = pinSetup.createObject(preferences, {mode: 'enter'})
dialog.accepted.connect(function() {
Config.pinCode = dialog.pincode
dialog.close()
})
dialog.rejected.connect(function() {
checked = false
})
dialog.open()
} else {
focus = false
Config.pinCode = ''
// re-add binding, pincode still set if auth failed
checked = Qt.binding(function () { return Config.pinCode })
}
}
}
}
Label {
Layout.fillWidth: true
text: qsTr('PIN protect payments')
wrapMode: Text.Wrap
}
}
Pane {
background: Rectangle { color: Material.dialogColor }
padding: 0
visible: Config.pinCode != ''
FlatButton {
text: qsTr('Modify')
onClicked: {
var dialog = pinSetup.createObject(preferences, {
mode: 'change',
pincode: Config.pinCode
})
dialog.accepted.connect(function() {
Config.pinCode = dialog.pincode
dialog.close()
})
dialog.open()
}
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
@@ -223,7 +167,6 @@ Pane {
Connections {
target: Biometrics
function onEnablingFailed(error) {
useBiometrics.checked = false
if (error === 'CANCELLED') {
return // don't show error popup
}
@@ -237,8 +180,9 @@ Pane {
Switch {
id: useBiometrics
checked: Biometrics.isEnabled
onToggled: {
onCheckedChanged: {
if (activeFocus) {
useBiometrics.focus = false
if (checked) {
if (Daemon.singlePasswordEnabled) {
Biometrics.enable(Daemon.singlePassword)
@@ -254,7 +198,7 @@ Pane {
err.open()
}
} else {
Biometrics.disable()
Biometrics.disableProtected()
}
}
}
@@ -266,6 +210,33 @@ Pane {
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
spacing: 0
property bool noWalletPassword: Daemon.currentWallet ? Daemon.currentWallet.verifyPassword('') : true
enabled: Daemon.currentWallet && !noWalletPassword
Switch {
id: paymentAuthentication
// showing the toggle as checked even if the wallet has no password would be misleading
checked: Config.paymentAuthentication && !(Daemon.currentWallet && parent.noWalletPassword)
onCheckedChanged: {
if (activeFocus) {
// will request authentication when checked = false
console.log('paymentAuthentication: ' + checked)
Config.paymentAuthentication = checked;
}
}
}
Label {
Layout.fillWidth: true
text: qsTr('Payment authentication')
wrapMode: Text.Wrap
}
}
RowLayout {
Layout.columnSpan: 2
Layout.fillWidth: true
@@ -515,11 +486,6 @@ Pane {
}
}
Component {
id: pinSetup
Pin {}
}
Component.onCompleted: {
language.currentIndex = language.indexOfValue(Config.language)
baseUnit.currentIndex = _baseunits.indexOf(Config.baseUnit)
@@ -577,6 +577,25 @@ Pane {
}
}
Connections {
target: Biometrics
function onEnablingFailed(error) {
if (error === 'CANCELLED') {
var biometrics_disabled_dialog = app.messageDialog.createObject(app, {
title: qsTr('Biometric Authentication'),
iconSource: Qt.resolvedUrl('../../icons/warning.png'),
text: qsTr('Biometric authentication disabled. You can enable it again in the settings.')
})
biometrics_disabled_dialog.open()
return
}
var err = app.messageDialog.createObject(app, {
text: qsTr('Failed to update biometric authentication to new password: ') + error
})
err.open()
}
}
Component {
id: importAddressesKeysDialog
ImportAddressesKeysDialog {
+50 -66
View File
@@ -419,14 +419,6 @@ ApplicationWindow
}
}
property alias pinDialog: _pinDialog
Component {
id: _pinDialog
Pin {
onClosed: destroy()
}
}
property alias genericShareDialog: _genericShareDialog
Component {
id: _genericShareDialog
@@ -639,42 +631,49 @@ ApplicationWindow
Connections {
target: Biometrics
function onUnlockSuccess(password) {
if (_pendingBiometricAuth) {
if (_pendingBiometricAuth.action === 'load_wallet') {
_loadingWalletContext = _pendingBiometricAuth
Daemon.loadWallet(_pendingBiometricAuth.path, password)
_pendingBiometricAuth = null
if (app._pendingBiometricAuth) {
if (app._pendingBiometricAuth.action === 'load_wallet') {
app._loadingWalletContext = _pendingBiometricAuth
Daemon.loadWallet(app._pendingBiometricAuth.path, password)
app._pendingBiometricAuth = null
return
}
var qtobject = _pendingBiometricAuth.qtobject
var method = _pendingBiometricAuth.method
let qtobject = app._pendingBiometricAuth.qtobject
let method = app._pendingBiometricAuth.method
if (Daemon.currentWallet.verifyPassword(password)) {
qtobject.authProceed()
} else {
console.log("Biometric password invalid falling back to manual input")
handleManualAuth(qtobject, method, _pendingBiometricAuth.authMessage)
console.warn("Biometric password invalid falling back to manual input")
// this shouldn't really happen so we better disable biometric auth
Biometrics.disable()
handleManualAuth(qtobject, method, app._pendingBiometricAuth.authMessage)
}
_pendingBiometricAuth = null
app._pendingBiometricAuth = null
}
}
function onUnlockError(error) {
console.log("Biometric auth failed: " + error)
// we end up here if QEBiometrics fails to give us the decrypted password. The user might
// have cancelled the biometric auth popup or the key got invalidated because a new fingerprint got registered.
if (_pendingBiometricAuth) {
// try manual auth
if (_pendingBiometricAuth.action === 'load_wallet') {
if (app._pendingBiometricAuth) {
if (app._pendingBiometricAuth.action === 'load_wallet') {
// set loadingWalletContext to disable biometric auth until the OpenWalletDialog is closed
_loadingWalletContext = _pendingBiometricAuth
showOpenWalletDialog(_pendingBiometricAuth.name, _pendingBiometricAuth.path)
app._loadingWalletContext = app._pendingBiometricAuth
showOpenWalletDialog(app._pendingBiometricAuth.name, app._pendingBiometricAuth.path)
} else {
handleManualAuth(_pendingBiometricAuth.qtobject, _pendingBiometricAuth.method, _pendingBiometricAuth.authMessage)
console.log('biometric auth failed, not falling back to passwordDialog')
app._pendingBiometricAuth.qtobject.authCancel() // no fallback to password dialog
}
_pendingBiometricAuth = null
app._pendingBiometricAuth = null
}
}
function onAuthRequired(method, authMessage) {
handleAuthRequired(Biometrics, method, authMessage)
}
}
property var _opendialog: null
@@ -689,7 +688,7 @@ ApplicationWindow
})
_opendialog.closed.connect(function() {
_opendialog = null
_loadingWalletContext = null // dialog closed, we can allow trying biometric auth again
app._loadingWalletContext = null // dialog closed, we can allow trying biometric auth again
_opendialog_startup = false
})
_opendialog.open()
@@ -700,8 +699,8 @@ ApplicationWindow
target: Daemon
function onWalletRequiresPassword(name, path) {
console.log('wallet requires password')
if (Biometrics.isAvailable && Biometrics.isEnabled && !_loadingWalletContext) {
_pendingBiometricAuth = {
if (Biometrics.isAvailable && Biometrics.isEnabled && !app._loadingWalletContext) {
app._pendingBiometricAuth = {
action: 'load_wallet',
name: name,
path: path
@@ -731,7 +730,7 @@ ApplicationWindow
dialog.open()
}
function onWalletLoaded() {
_loadingWalletContext = null // either biometric auth or manual auth was successful
app._loadingWalletContext = null // either biometric auth or manual auth was successful
}
}
@@ -818,42 +817,41 @@ ApplicationWindow
function handleAuthRequired(qtobject, method, authMessage) {
console.log('auth using method ' + method)
if (method == 'wallet_else_pin') {
// if there is a loaded wallet and all wallets use the same password, use that
// else delegate to pin auth
if (Daemon.currentWallet && Daemon.singlePasswordEnabled) {
if (method === 'payment_auth') {
if (Config.paymentAuthentication) {
// treat like a wallet auth request
method = 'wallet'
} else {
method = 'pin'
}
}
if (method === 'wallet') {
if (Daemon.currentWallet.verifyPassword('')) {
// wallet has no password
qtobject.authProceed()
return
}
} else if (method === 'pin') {
if (Config.pinCode === '') {
// no PIN configured
handleAuthConfirmationOnly(qtobject, authMessage)
return
}
}
if (Biometrics.isAvailable && Biometrics.isEnabled) {
_pendingBiometricAuth = { qtobject: qtobject, method: method, authMessage: authMessage }
Biometrics.unlock()
return
if (Daemon.currentWallet.verifyPassword('')) {
// wallet has no password
qtobject.authProceed()
return
}
if (method !== 'wallet_password_only') {
if (Biometrics.isAvailable && Biometrics.isEnabled) {
app._pendingBiometricAuth = {
qtobject: qtobject,
method: method,
authMessage: authMessage
}
Biometrics.unlock(authMessage)
return
}
}
handleManualAuth(qtobject, method, authMessage)
}
function handleManualAuth(qtobject, method, authMessage) {
if (method == 'wallet') {
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')})
// 'payment_auth' should have been converted to 'wallet' at this point
if (method === 'wallet' || method === 'wallet_password_only') {
var dialog = app.passwordDialog.createObject(app, authMessage ? {'title': authMessage} : {})
dialog.accepted.connect(function() {
if (Daemon.currentWallet.verifyPassword(dialog.password)) {
qtobject.authProceed()
@@ -865,20 +863,6 @@ ApplicationWindow
qtobject.authCancel()
})
dialog.open()
} else if (method == 'pin') {
var dialog = app.pinDialog.createObject(app, {
mode: 'check',
pincode: Config.pinCode,
authMessage: authMessage
})
dialog.accepted.connect(function() {
qtobject.authProceed()
dialog.close()
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
} else {
console.log('unknown auth method ' + method)
qtobject.authCancel()
@@ -5,6 +5,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.content.Intent;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricPrompt;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
@@ -34,8 +35,8 @@ public class BiometricActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Log.e(TAG, "Biometrics not supported on this Android version (requires API 29+)");
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Log.e(TAG, "Biometrics not supported on this Android version (requires API 30+)");
setResult(RESULT_CANCELED);
finish();
return;
@@ -45,20 +46,17 @@ public class BiometricActivity extends Activity {
}
private void handleIntent() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return;
Intent intent = getIntent();
String action = intent.getStringExtra("action");
String authMessage = intent.getStringExtra("auth_message");
Executor executor = getMainExecutor();
BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(this)
.setTitle("Electrum Wallet")
.setSubtitle("Confirm your identity")
.setNegativeButton("Cancel", executor, (dialog, which) -> {
Log.d(TAG, "Authentication cancelled");
setResult(RESULT_POPUP_CANCELLED);
finish();
})
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL)
.setSubtitle(authMessage)
.build();
cancellationSignal = new CancellationSignal();
@@ -67,8 +65,17 @@ public class BiometricActivity extends Activity {
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
Log.e(TAG, "Authentication error: " + errString);
setResult(RESULT_CANCELED);
Log.e(TAG, "Authentication error: " + errorCode + " " + errString);
if (
errorCode == BiometricPrompt.BIOMETRIC_ERROR_CANCELED ||
errorCode == BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED ||
errorCode == BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT
) {
setResult(RESULT_POPUP_CANCELLED);
} else {
setResult(RESULT_CANCELED);
}
finish();
}
@@ -152,7 +159,7 @@ public class BiometricActivity extends Activity {
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true);
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG | KeyProperties.AUTH_DEVICE_CREDENTIAL);
keyGenerator.init(builder.build());
keyGenerator.generateKey();
@@ -10,10 +10,7 @@ public class BiometricHelper {
public static boolean isAvailable(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // API 30+
BiometricManager biometricManager = context.getSystemService(BiometricManager.class);
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS;
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { // API 29
BiometricManager biometricManager = context.getSystemService(BiometricManager.class);
return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS;
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
}
return false;
}
+26 -6
View File
@@ -3,11 +3,14 @@ import secrets
from enum import Enum
from typing import Optional, TYPE_CHECKING
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from electrum.i18n import _
from electrum.logging import get_logger
from electrum.base_crash_reporter import send_exception_to_crash_reporter
from electrum.crypto import aes_encrypt_with_iv, aes_decrypt_with_iv
from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from .auth import auth_protect, AuthMixin
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
@@ -37,7 +40,7 @@ class BiometricAction(str, Enum):
DECRYPT = "DECRYPT"
class QEBiometrics(QObject):
class QEBiometrics(AuthMixin, QObject):
REQUEST_CODE_BIOMETRIC_ACTIVITY = 24553 # random 16 bit int
RESULT_CODE_SETUP_FAILED = 101 # codes duplicated from BiometricActivity.java
RESULT_CODE_POPUP_CANCELLED = 102
@@ -94,22 +97,40 @@ class QEBiometrics(QObject):
_logger.info("Android biometric authentication disabled")
@pyqtSlot()
def unlock(self):
@auth_protect(method='wallet_password_only', reject='_disable_protected_failed')
def disableProtected(self):
"""
Exists to ensure the user knows the wallet password when manually disabling
biometric authentication. If they don't remember the password they can still do a seed
backup or transactions if biometrics stay enabled. However, note it is still possible for
biometrics to get disabled automatically on invalidation or error, so this cannot
fully protect the user from forgetting their wallet password either.
"""
self.disable()
def _disable_protected_failed(self):
self.isEnabledChanged.emit()
@pyqtSlot()
@pyqtSlot(str)
def unlock(self, auth_message: str = None):
"""
Called when the user needs to authenticate.
Makes the AndroidKeyStore decrypt our encrypted wrap key, we then use the decrypted wrap key
to decrypt the encrypted wallet password.
auth_message is shown in the system auth popup and defaults to 'Confirm your identity'.
"""
encrypted_wrap_key = self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY
assert encrypted_wrap_key, "shouldn't unlock if biometric auth is disabled"
self._start_activity(BiometricAction.DECRYPT, data=encrypted_wrap_key)
self._start_activity(BiometricAction.DECRYPT, data=encrypted_wrap_key, auth_message=auth_message)
def _start_activity(self, action: BiometricAction, data: str):
def _start_activity(self, action: BiometricAction, data: str, auth_message: str = None):
self._current_action = action
_logger.debug(f"_start_activity: {action.value}, {len(data)=}")
intent = jIntent(jPythonActivity, jBiometricActivity)
intent.putExtra(jString("action"), jString(action.value))
intent.putExtra(jString("auth_message"), jString(auth_message or _("Confirm your identity")))
if action == BiometricAction.ENCRYPT:
intent.putExtra(jString("data"), jString(data)) # wrap_key
elif action == BiometricAction.DECRYPT:
@@ -178,4 +199,3 @@ class QEBiometrics(QObject):
self.config.WALLET_ANDROID_BIOMETRIC_AUTH_ENCRYPTED_WRAP_KEY = encrypted_bundle
self.config.WALLET_ANDROID_USE_BIOMETRIC_AUTHENTICATION = True
self.isEnabledChanged.emit()
+17 -14
View File
@@ -153,23 +153,26 @@ class QEConfig(AuthMixin, QObject):
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 ""
paymentAuthenticationChanged = pyqtSignal()
@pyqtProperty(bool, notify=paymentAuthenticationChanged)
def paymentAuthentication(self):
return self.config.GUI_QML_PAYMENT_AUTHENTICATION
@pinCode.setter
def pinCode(self, pin_code):
if pin_code == '':
self.pinCodeRemoveAuth()
@paymentAuthentication.setter
def paymentAuthentication(self, enabled: bool):
if enabled:
self.config.GUI_QML_PAYMENT_AUTHENTICATION = True
self.paymentAuthenticationChanged.emit()
else:
self.config.CONFIG_PIN_CODE = pin_code
self.pinCodeChanged.emit()
self._disable_payment_authentication()
@auth_protect(method='wallet_else_pin')
def pinCodeRemoveAuth(self):
self.config.CONFIG_PIN_CODE = ""
self.pinCodeChanged.emit()
@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)
+1 -1
View File
@@ -388,7 +388,7 @@ class QEDaemon(AuthMixin, QObject):
return f'wallet_{i}'
@pyqtSlot()
@auth_protect(method='wallet')
@auth_protect(method='wallet_password_only')
def startChangePassword(self):
if self._use_single_password:
self.requestNewPassword.emit()
+1 -1
View File
@@ -853,6 +853,7 @@ Warning: setting this to too low will result in lots of payment failures."""),
GUI_QML_ADDRESS_LIST_SHOW_USED = ConfigVar('address_list_show_used', default=False, type_=bool)
GUI_QML_ALWAYS_ALLOW_SCREENSHOTS = ConfigVar('android_always_allow_screenshots', default=False, type_=bool)
GUI_QML_SET_MAX_BRIGHTNESS_ON_QR_DISPLAY = ConfigVar('android_set_max_brightness_on_qr_display', default=True, type_=bool)
GUI_QML_PAYMENT_AUTHENTICATION = ConfigVar('qml_payment_authentication', default=False, type_=bool)
BTC_AMOUNTS_DECIMAL_POINT = ConfigVar('decimal_point', default=DECIMAL_POINT_DEFAULT, type_=int)
BTC_AMOUNTS_FORCE_NZEROS_AFTER_DECIMAL_POINT = ConfigVar(
@@ -924,7 +925,6 @@ Warning: setting this to too low will result in lots of payment failures."""),
RECENTLY_OPEN_WALLET_FILES = ConfigVar('recently_open', default=None)
IO_DIRECTORY = ConfigVar('io_dir', default=os.path.expanduser('~'), type_=str)
WALLET_BACKUP_DIRECTORY = ConfigVar('backup_dir', default=None, type_=str)
CONFIG_PIN_CODE = ConfigVar('pin_code', default=None, type_=str)
QR_READER_FLIP_X = ConfigVar('qrreader_flip_x', default=True, type_=bool)
WIZARD_DONT_CREATE_SEGWIT = ConfigVar('nosegwit', default=False, type_=bool)
CONFIG_FORGET_CHANGES = ConfigVar('forget_config', default=False, type_=bool)