Files
pallectrum/electrum/gui/qml/components/main.qml
SomberNight 68fb996d20 wallet_db version 52: break non-homogeneous multisig wallets
- case 1: in version 4.4.1, 4.4.2, the qml GUI wizard allowed creating multisig wallets with an old_mpk as cosigner.
- case 2: in version 4.4.0, 4.4.1, 4.4.2, the qml GUI wizard allowed creating multisig wallets with mixed xpub/Ypub/Zpub.

The corresponding missing input validation was a bug in the wizard, it was unintended behaviour. Validation was added in d2cf21fc2b. Note however that there might be users who created such wallet files.

Re case 1 wallet files: there is no version of Electrum that allows spending from such a wallet. Coins received at addresses are not burned, however it is technically challenging to spend them. (unless the multisig can spend without needing the old_mpk cosigner in the quorum).

Re case 2 wallet files: it is possible to create a corresponding spending wallet for such a multisig, however it is a bit tricky. The script type for the addresses in such a heterogeneous xpub wallet is based on the xpub_type of the first keystore. So e.g. given a wallet file [Yprv1, Zpub2] it will have sh(wsh()) scripts, and the cosigner should create a wallet file [Ypub1, Zprv2] (same order).

Technically case 2 wallet files could be "fixed" automatically by converting the xpub types as part of a wallet_db upgrade. However if the wallet files also contain seeds, those cannot be converted ("standard" vs "segwit" electrum seed).
Case 1 wallet files are not possible to "fix" automatically as the cosigner using the old_mpk is not bip32 based.

It is unclear if there are *any* users out there affected by this. I suspect for case 1 it is very likely there are none (not many people have pre-2.0 electrum seeds which were never supported as part of a multisig who would also now try to create a multisig using them); for case 2 however there might be.

This commit breaks both case 1 and case 2 wallets: these wallet files can no longer be opened in new Electrum, an error message is shown and the crash reporter opens. If any potential users opt to send crash reports, at least we will know they exist and can help them recover.
2023-05-11 14:26:11 +00:00

621 lines
19 KiB
QML

import QtQuick 2.6
import QtQuick.Layouts 1.0
import QtQuick.Controls 2.3
import QtQuick.Controls.Material 2.0
import QtQuick.Controls.Material.impl 2.12
import QtQuick.Window 2.15
import QtQml 2.6
import QtMultimedia 5.6
import org.electrum 1.0
import "controls"
ApplicationWindow
{
id: app
visible: false // initial value
// dimensions ignored on android
width: 480
height: 800
Material.theme: Material.Dark
Material.primary: Material.Indigo
Material.accent: Material.LightBlue
font.pixelSize: constants.fontSizeMedium
property Item constants: appconstants
Constants { id: appconstants }
property alias stack: mainStackView
property variant activeDialogs: []
property bool _wantClose: false
property var _exceptionDialog
property QtObject appMenu: Menu {
parent: Overlay.overlay
dim: true
modal: true
Overlay.modal: Rectangle {
color: "#44000000"
}
id: menu
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/network.png'
action: Action {
text: qsTr('Network')
onTriggered: menu.openPage(Qt.resolvedUrl('NetworkOverview.qml'))
enabled: stack.currentItem.objectName != 'NetworkOverview'
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/preferences.png'
action: Action {
text: qsTr('Preferences')
onTriggered: menu.openPage(Qt.resolvedUrl('Preferences.qml'))
enabled: stack.currentItem.objectName != 'Properties'
}
}
MenuItem {
icon.color: action.enabled ? 'transparent' : Material.iconDisabledColor
icon.source: '../../icons/electrum.png'
action: Action {
text: qsTr('About');
onTriggered: menu.openPage(Qt.resolvedUrl('About.qml'))
enabled: stack.currentItem.objectName != 'About'
}
}
function openPage(url) {
stack.pushOnRoot(url)
currentIndex = -1
}
}
function openAppMenu() {
appMenu.open()
appMenu.x = app.width - appMenu.width
appMenu.y = toolbar.height
}
header: ToolBar {
id: toolbar
background: Rectangle {
implicitHeight: 48
color: Material.dialogColor
layer.enabled: true
layer.effect: ElevationEffect {
elevation: 4
fullWidth: true
}
}
ColumnLayout {
spacing: 0
width: parent.width
height: toolbar.height
RowLayout {
id: toolbarTopLayout
Layout.fillWidth: true
Layout.rightMargin: constants.paddingMedium
Layout.alignment: Qt.AlignVCenter
Item {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height)
MouseArea {
anchors.fill: parent
enabled: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name)
onClicked: {
stack.getRoot().menu.open() // open wallet-menu
stack.getRoot().menu.y = toolbar.height
}
}
RowLayout {
width: parent.width
Item {
Layout.preferredWidth: constants.paddingXLarge
Layout.preferredHeight: 1
}
Image {
Layout.preferredWidth: constants.iconSizeSmall
Layout.preferredHeight: constants.iconSizeSmall
visible: Daemon.currentWallet && (!stack.currentItem.title || stack.currentItem.title == Daemon.currentWallet.name)
source: '../../icons/wallet.png'
}
Label {
Layout.fillWidth: true
Layout.preferredHeight: Math.max(implicitHeight, toolbarTopLayout.height)
text: stack.currentItem.title
? stack.currentItem.title
: Daemon.currentWallet.name
elide: Label.ElideRight
verticalAlignment: Qt.AlignVCenter
font.pixelSize: constants.fontSizeMedium
font.bold: true
}
}
}
Item {
implicitHeight: 48
implicitWidth: statusIconsLayout.width
MouseArea {
anchors.fill: parent
onClicked: openAppMenu() // open global-app-menu
}
RowLayout {
id: statusIconsLayout
anchors.verticalCenter: parent.verticalCenter
Item {
Layout.preferredWidth: constants.paddingLarge
Layout.preferredHeight: 1
}
Item {
visible: Network.isTestNet
width: column.width
height: column.height
ColumnLayout {
id: column
spacing: 0
Image {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: constants.iconSizeSmall
Layout.preferredHeight: constants.iconSizeSmall
source: "../../icons/info.png"
}
Label {
id: networkNameLabel
text: Network.networkName
color: Material.accentColor
font.pixelSize: constants.fontSizeXSmall
}
}
}
Image {
Layout.preferredWidth: constants.iconSizeSmall
Layout.preferredHeight: constants.iconSizeSmall
visible: Daemon.currentWallet && Daemon.currentWallet.isWatchOnly
source: '../../icons/eye1.png'
scale: 1.5
}
LightningNetworkStatusIndicator {}
OnchainNetworkStatusIndicator {}
}
}
}
WalletSummary {
id: walletSummary
Layout.preferredWidth: app.width
}
}
}
StackView {
id: mainStackView
width: parent.width
height: keyboardFreeZone.height - header.height
initialItem: Qt.resolvedUrl('WalletMainView.qml')
function getRoot() {
return mainStackView.get(0)
}
function pushOnRoot(item) {
if (mainStackView.depth > 1) {
mainStackView.replace(mainStackView.get(1), item)
} else {
mainStackView.push(item)
}
}
}
Timer {
id: coverTimer
interval: 10
onTriggered: {
app.visible = true
cover.opacity = 0
}
}
Rectangle {
id: cover
parent: Overlay.overlay
anchors.fill: parent
z: 1000
color: 'black'
Behavior on opacity {
enabled: AppController ? AppController.isAndroid() : false
NumberAnimation {
duration: 1000
easing.type: Easing.OutQuad;
}
}
}
Item {
id: keyboardFreeZone
// Item as first child in Overlay that adjusts its size to the available
// screen space minus the virtual keyboard (e.g. to center dialogs in)
// see also ElDialog.resizeWithKeyboard property
parent: Overlay.overlay
width: parent.width
height: parent.height
states: State {
name: "visible"
when: Qt.inputMethod.visible
PropertyChanges {
target: keyboardFreeZone
height: keyboardFreeZone.parent.height - Qt.inputMethod.keyboardRectangle.height / Screen.devicePixelRatio
}
}
transitions: [
Transition {
from: ''
to: 'visible'
ParallelAnimation {
NumberAnimation {
properties: "height"
duration: 250
easing.type: Easing.OutQuad
}
}
},
Transition {
from: 'visible'
to: ''
ParallelAnimation {
NumberAnimation {
properties: "height"
duration: 50
easing.type: Easing.OutQuad
}
}
}
]
}
property alias newWalletWizard: _newWalletWizard
Component {
id: _newWalletWizard
NewWalletWizard {
onClosed: destroy()
}
}
property alias serverConnectWizard: _serverConnectWizard
Component {
id: _serverConnectWizard
ServerConnectWizard {
onClosed: destroy()
}
}
property alias messageDialog: _messageDialog
Component {
id: _messageDialog
MessageDialog {
onClosed: destroy()
}
}
property alias passwordDialog: _passwordDialog
Component {
id: _passwordDialog
PasswordDialog {
onClosed: destroy()
}
}
property alias pinDialog: _pinDialog
Component {
id: _pinDialog
Pin {
onClosed: destroy()
}
}
property alias genericShareDialog: _genericShareDialog
Component {
id: _genericShareDialog
GenericShareDialog {
onClosed: destroy()
}
}
property alias openWalletDialog: _openWalletDialog
Component {
id: _openWalletDialog
OpenWalletDialog {
onClosed: destroy()
}
}
property alias loadingWalletDialog: _loadingWalletDialog
Component {
id: _loadingWalletDialog
LoadingWalletDialog {
onClosed: destroy()
}
}
property alias scanDialog: _scanDialog
Component {
id: _scanDialog
ScanDialog {
onClosed: destroy()
}
}
property alias channelOpenProgressDialog: _channelOpenProgressDialog
ChannelOpenProgressDialog {
id: _channelOpenProgressDialog
}
Component {
id: swapDialog
SwapDialog {
onClosed: destroy()
swaphelper: SwapHelper {
id: _swaphelper
wallet: Daemon.currentWallet
onAuthRequired: {
app.handleAuthRequired(_swaphelper, method, authMessage)
}
onError: {
var dialog = app.messageDialog.createObject(app, { title: qsTr('Error'), text: message })
dialog.open()
}
}
}
}
NotificationPopup {
id: notificationPopup
width: parent.width
}
Component {
id: crashDialog
ExceptionDialog {
z: 1000
}
}
Component.onCompleted: {
coverTimer.start()
if (!Config.autoConnectDefined) {
var dialog = serverConnectWizard.createObject(app)
// without completed serverConnectWizard we can't start
dialog.rejected.connect(function() {
app.visible = false
Qt.callLater(Qt.quit)
})
dialog.accepted.connect(function() {
Daemon.startNetwork()
var newww = app.newWalletWizard.createObject(app)
newww.walletCreated.connect(function() {
Daemon.availableWallets.reload()
// and load the new wallet
Daemon.loadWallet(newww.path, newww.wizard_data['password'])
})
newww.open()
})
dialog.open()
} else {
Daemon.startNetwork()
if (Daemon.availableWallets.rowCount() > 0) {
Daemon.loadWallet()
} else {
var newww = app.newWalletWizard.createObject(app)
newww.walletCreated.connect(function() {
Daemon.availableWallets.reload()
// and load the new wallet
Daemon.loadWallet(newww.path, newww.wizard_data['password'])
})
newww.open()
}
}
}
onClosing: {
if (activeDialogs.length > 0) {
var activeDialog = activeDialogs[activeDialogs.length - 1]
if (activeDialog.allowClose) {
activeDialog.doClose()
} else {
console.log('dialog disallowed close')
}
close.accepted = false
return
}
if (stack.depth > 1) {
close.accepted = false
stack.pop()
} else {
// destroy most GUI components so that we don't dump so many null reference warnings on exit
if (app._wantClose) {
app.header.visible = false
mainStackView.clear()
} else {
var dialog = app.messageDialog.createObject(app, {
title: qsTr('Close Electrum?'),
yesno: true
})
dialog.accepted.connect(function() {
app._wantClose = true
app.close()
})
dialog.open()
close.accepted = false
}
}
}
Connections {
target: Daemon
function onWalletRequiresPassword(name, path) {
console.log('wallet requires password')
var dialog = openWalletDialog.createObject(app, { path: path, name: name })
dialog.open()
}
function onWalletOpenError(error) {
console.log('wallet open error')
var dialog = app.messageDialog.createObject(app, { title: qsTr('Error'), 'text': error })
dialog.open()
}
function onAuthRequired(method, authMessage) {
handleAuthRequired(Daemon, method, authMessage)
}
function onLoadingChanged() {
if (!Daemon.loading)
return
console.log('wallet loading')
var dialog = loadingWalletDialog.createObject(app, { allowClose: false } )
dialog.open()
}
}
Connections {
target: AppController
function onUserNotify(wallet_name, message) {
notificationPopup.show(wallet_name, message)
}
function onShowException(crash_data) {
if (app._exceptionDialog)
return
app._exceptionDialog = crashDialog.createObject(app, {
crashData: crash_data
})
app._exceptionDialog.onClosed.connect(function() {
app._exceptionDialog = null
})
app._exceptionDialog.open()
}
}
Connections {
target: Daemon.currentWallet
function onAuthRequired(method, authMessage) {
handleAuthRequired(Daemon.currentWallet, method, authMessage)
}
// TODO: add to notification queue instead of barging through
function onPaymentSucceeded(key) {
notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment Succeeded'))
}
function onPaymentFailed(key, reason) {
notificationPopup.show(Daemon.currentWallet.name, qsTr('Payment Failed') + ': ' + reason)
}
}
Connections {
target: Config
function onAuthRequired(method, authMessage) {
handleAuthRequired(Config, method, authMessage)
}
}
function handleAuthRequired(qtobject, method, authMessage) {
console.log('auth using method ' + method)
if (method == 'wallet') {
if (Daemon.currentWallet.verifyPassword('')) {
// wallet has no password
qtobject.authProceed()
} else {
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')})
dialog.accepted.connect(function() {
if (Daemon.currentWallet.verifyPassword(dialog.password)) {
qtobject.authProceed()
} else {
qtobject.authCancel()
}
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
}
} else if (method == 'pin') {
if (Config.pinCode == '') {
// no PIN configured
handleAuthConfirmationOnly(qtobject, authMessage)
} else {
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()
}
}
function handleAuthConfirmationOnly(qtobject, authMessage) {
if (!authMessage) {
qtobject.authProceed()
return
}
var dialog = app.messageDialog.createObject(app, {title: authMessage, yesno: true})
dialog.accepted.connect(function() {
qtobject.authProceed()
})
dialog.rejected.connect(function() {
qtobject.authCancel()
})
dialog.open()
}
function startSwap() {
var swapdialog = swapDialog.createObject(app)
swapdialog.open()
}
property var _lastActive: 0 // record time of last activity
property bool _lockDialogShown: false
}