Files
pallectrum/electrum/gui/qml/qedaemon.py

351 lines
12 KiB
Python
Raw Normal View History

2021-04-06 14:13:51 +02:00
import os
2023-02-22 14:17:57 +01:00
import threading
2021-04-06 14:13:51 +02:00
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
2022-07-27 10:23:35 +02:00
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
2021-04-06 14:13:51 +02:00
from electrum.i18n import _
2021-04-06 14:13:51 +02:00
from electrum.logging import get_logger
2022-07-27 10:23:35 +02:00
from electrum.util import WalletFileException, standardize_path
from electrum.wallet import Abstract_Wallet
from electrum.plugin import run_hook
from electrum.lnchannel import ChannelState
from electrum.daemon import Daemon
2021-04-06 14:13:51 +02:00
2022-07-27 10:23:35 +02:00
from .auth import AuthMixin, auth_protect
from .qefx import QEFX
2021-04-06 14:13:51 +02:00
from .qewallet import QEWallet
from .qewalletdb import QEWalletDB
from .qewizard import QENewWalletWizard, QEServerConnectWizard
2021-04-06 14:13:51 +02:00
# wallet list model. supports both wallet basenames (wallet file basenames)
# and whole Wallet instances (loaded wallets)
class QEWalletListModel(QAbstractListModel):
_logger = get_logger(__name__)
# define listmodel rolemap
_ROLE_NAMES= ('name','path','active')
_ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES))
2021-04-06 14:13:51 +02:00
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))
def __init__(self, daemon, parent=None):
QAbstractListModel.__init__(self, parent)
self.daemon = daemon
self.reload()
2021-04-06 14:13:51 +02:00
def rowCount(self, index):
return len(self.wallets)
def roleNames(self):
return self._ROLE_MAP
def data(self, index, role):
(wallet_name, wallet_path) = self.wallets[index.row()]
role_index = role - Qt.UserRole
2021-04-06 14:13:51 +02:00
role_name = self._ROLE_NAMES[role_index]
if role_name == 'name':
return wallet_name
if role_name == 'path':
return wallet_path
2021-04-06 14:13:51 +02:00
if role_name == 'active':
return self.daemon.get_wallet(wallet_path) is not None
2021-04-06 14:13:51 +02:00
@pyqtSlot()
def reload(self):
self._logger.debug('enumerating available wallets')
self.beginResetModel()
self.wallets = []
self.endResetModel()
available = []
wallet_folder = os.path.dirname(self.daemon.config.get_wallet_path())
with os.scandir(wallet_folder) as it:
for i in it:
if i.is_file() and not i.name.startswith('.'):
available.append(i.path)
for path in sorted(available):
wallet = self.daemon.get_wallet(path)
self.add_wallet(wallet_path = path)
def add_wallet(self, wallet_path):
2022-10-31 16:13:22 +00:00
self.beginInsertRows(QModelIndex(), len(self.wallets), len(self.wallets))
wallet_name = os.path.basename(wallet_path)
wallet_path = standardize_path(wallet_path)
item = (wallet_name, wallet_path)
2022-10-31 16:13:22 +00:00
self.wallets.append(item)
self.endInsertRows()
2021-04-06 14:13:51 +02:00
def remove_wallet(self, path):
i = 0
wallets = []
remove = -1
for wallet_name, wallet_path in self.wallets:
if wallet_path == path:
remove = i
else:
wallets.append((wallet_name, wallet_path))
i += 1
if remove >= 0:
self.beginRemoveRows(QModelIndex(), i, i)
self.wallets = wallets
self.endRemoveRows()
@pyqtSlot(str, result=bool)
def wallet_name_exists(self, name):
for wallet_name, wallet_path in self.wallets:
if name == wallet_name:
return True
return False
@pyqtSlot(str)
def updateWallet(self, path):
i = 0
for wallet_name, wallet_path in self.wallets:
if wallet_path == path:
mi = self.createIndex(i, i)
self.dataChanged.emit(mi, mi, self._ROLE_KEYS)
return
i += 1
class QEDaemon(AuthMixin, QObject):
2021-04-06 14:13:51 +02:00
_logger = get_logger(__name__)
_available_wallets = None
_current_wallet = None
_new_wallet_wizard = None
_server_connect_wizard = None
_path = None
_name = None
2022-07-06 11:10:00 +02:00
_use_single_password = False
_password = None
_loading = False
2023-02-22 14:17:57 +01:00
_backendWalletLoaded = pyqtSignal([str], arguments=['password'])
availableWalletsChanged = pyqtSignal()
fxChanged = pyqtSignal()
newWalletWizardChanged = pyqtSignal()
serverConnectWizardChanged = pyqtSignal()
loadingChanged = pyqtSignal()
requestNewPassword = pyqtSignal()
2023-02-22 14:17:57 +01:00
walletLoaded = pyqtSignal([str,str], arguments=['name','path'])
walletRequiresPassword = pyqtSignal([str,str], arguments=['name','path'])
walletOpenError = pyqtSignal([str], arguments=["error"])
walletDeleteError = pyqtSignal([str,str], arguments=['code', 'message'])
def __init__(self, daemon: 'Daemon', parent=None):
super().__init__(parent)
self.daemon = daemon
self.qefx = QEFX(daemon.fx, daemon.config)
2023-02-22 14:17:57 +01:00
self._backendWalletLoaded.connect(self._on_backend_wallet_loaded)
self._walletdb = QEWalletDB()
self._walletdb.validPasswordChanged.connect(self.passwordValidityCheck)
self._walletdb.walletOpenProblem.connect(self.onWalletOpenProblem)
@pyqtSlot()
def passwordValidityCheck(self):
if not self._walletdb._validPassword:
self.walletRequiresPassword.emit(self._name, self._path)
@pyqtSlot(str)
def onWalletOpenProblem(self, error):
self.walletOpenError.emit(error)
2021-04-06 14:13:51 +02:00
@pyqtSlot()
@pyqtSlot(str)
@pyqtSlot(str, str)
def loadWallet(self, path=None, password=None):
2022-10-31 16:13:22 +00:00
if path is None:
self._path = self.daemon.config.get('wallet_path') # command line -w option
if self._path is None:
self._path = self.daemon.config.GUI_LAST_WALLET
else:
self._path = path
if self._path is None:
return
2022-07-12 16:49:07 +02:00
self._path = standardize_path(self._path)
self._name = os.path.basename(self._path)
self._logger.debug('load wallet ' + str(self._path))
# map empty string password to None
if password == '':
password = None
2022-07-12 16:49:07 +02:00
if not password:
password = self._password
wallet_already_open = self.daemon.get_wallet(self._path) is not None
if not wallet_already_open:
# pre-checks, let walletdb trigger any necessary user interactions
self._walletdb.path = self._path
self._walletdb.password = password
2022-07-12 16:49:07 +02:00
self._walletdb.verify()
if not self._walletdb.ready:
return
2023-02-22 14:17:57 +01:00
def load_wallet_task():
self._loading = True
self.loadingChanged.emit()
2023-02-22 14:17:57 +01:00
try:
local_password = password # need this in local scope
wallet = self.daemon.load_wallet(self._path, local_password)
2023-02-22 14:17:57 +01:00
if wallet is None:
self._logger.info('could not open wallet')
self.walletOpenError.emit('could not open wallet')
return
2022-07-06 11:10:00 +02:00
if wallet_already_open:
# wallet already open. daemon.load_wallet doesn't mind, but
# we need the correct current wallet password below
local_password = QEWallet.getInstanceFor(wallet).password
if self.daemon.config.WALLET_USE_SINGLE_PASSWORD:
self._use_single_password = self.daemon.update_password_for_directory(old_password=local_password, new_password=local_password)
self._password = local_password
self.singlePasswordChanged.emit()
self._logger.info(f'use single password: {self._use_single_password}')
else:
self._logger.info('use single password disabled by config')
2022-07-06 11:10:00 +02:00
self.daemon.config.save_last_wallet(wallet)
2023-02-22 14:17:57 +01:00
run_hook('load_wallet', wallet)
2023-02-22 14:17:57 +01:00
self._backendWalletLoaded.emit(local_password)
2023-02-22 14:17:57 +01:00
except WalletFileException as e:
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 d2cf21fc2bcf79f07b7e41178cd3e4ca9e3d9f68. 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 13:48:54 +00:00
self._logger.error(f"load_wallet_task errored opening wallet: {e!r}")
2023-02-22 14:17:57 +01:00
self.walletOpenError.emit(str(e))
finally:
self._loading = False
self.loadingChanged.emit()
2023-02-22 14:17:57 +01:00
threading.Thread(target=load_wallet_task, daemon=True).start()
2023-02-22 14:17:57 +01:00
@pyqtSlot()
@pyqtSlot(str)
def _on_backend_wallet_loaded(self, password = None):
self._logger.debug('_on_backend_wallet_loaded')
wallet = self.daemon.get_wallet(self._path)
assert wallet is not None
2023-02-22 14:17:57 +01:00
self._current_wallet = QEWallet.getInstanceFor(wallet)
self.availableWallets.updateWallet(self._path)
self._current_wallet.password = password if password else None
2023-02-22 14:17:57 +01:00
self.walletLoaded.emit(self._name, self._path)
2021-04-06 14:13:51 +02:00
@pyqtSlot(QEWallet)
@pyqtSlot(QEWallet, bool)
@pyqtSlot(QEWallet, bool, bool)
def checkThenDeleteWallet(self, wallet, confirm_requests=False, confirm_balance=False):
if wallet.wallet.lnworker:
lnchannels = wallet.wallet.lnworker.get_channel_objects()
if any([channel.get_state() != ChannelState.REDEEMED and not channel.is_backup() for channel in lnchannels.values()]):
self.walletDeleteError.emit('unclosed_channels', _('There are still channels that are not fully closed'))
return
num_requests = len(wallet.wallet.get_unpaid_requests())
if num_requests > 0 and not confirm_requests:
self.walletDeleteError.emit('unpaid_requests', _('There are still unpaid requests. Really delete?'))
return
c, u, x = wallet.wallet.get_balance()
if c+u+x > 0 and not wallet.wallet.is_watching_only() and not confirm_balance:
self.walletDeleteError.emit('balance', _('There are still coins present in this wallet. Really delete?'))
return
self.delete_wallet(wallet)
@auth_protect(message=_('Really delete this wallet?'))
def delete_wallet(self, wallet):
path = standardize_path(wallet.wallet.storage.path)
self._logger.debug('deleting wallet with path %s' % path)
self._current_wallet = None
# TODO walletLoaded signal is confusing
2023-03-16 20:23:29 +01:00
self.walletLoaded.emit(None, None)
if not self.daemon.delete_wallet(path):
self.walletDeleteError.emit('error', _('Problem deleting wallet'))
return
self.availableWallets.remove_wallet(path)
@pyqtProperty(bool, notify=loadingChanged)
def loading(self):
return self._loading
@pyqtProperty(QEWallet, notify=walletLoaded)
2021-04-06 14:13:51 +02:00
def currentWallet(self):
return self._current_wallet
@pyqtProperty(QEWalletListModel, notify=availableWalletsChanged)
2021-04-06 14:13:51 +02:00
def availableWallets(self):
if not self._available_wallets:
self._available_wallets = QEWalletListModel(self.daemon)
return self._available_wallets
2022-04-04 13:21:05 +02:00
@pyqtProperty(QEFX, notify=fxChanged)
def fx(self):
return self.qefx
singlePasswordChanged = pyqtSignal()
@pyqtProperty(bool, notify=singlePasswordChanged)
def singlePasswordEnabled(self):
return self._use_single_password
@pyqtProperty(str, notify=singlePasswordChanged)
def singlePassword(self):
return self._password
@pyqtSlot(result=str)
def suggestWalletName(self):
# FIXME why not use util.get_new_wallet_name ?
i = 1
while self.availableWallets.wallet_name_exists(f'wallet_{i}'):
i = i + 1
return f'wallet_{i}'
2022-07-06 11:10:00 +02:00
@pyqtSlot()
@auth_protect(method='wallet')
def startChangePassword(self):
2022-07-06 11:10:00 +02:00
if self._use_single_password:
self.requestNewPassword.emit()
else:
self.currentWallet.requestNewPassword.emit()
@pyqtSlot(str, result=bool)
def setPassword(self, password):
2022-07-06 11:10:00 +02:00
assert self._use_single_password
assert password
if not self.daemon.update_password_for_directory(old_password=self._password, new_password=password):
return False
2022-07-12 16:49:07 +02:00
self._password = password
return True
2022-07-12 16:49:07 +02:00
@pyqtProperty(QENewWalletWizard, notify=newWalletWizardChanged)
def newWalletWizard(self):
if not self._new_wallet_wizard:
self._new_wallet_wizard = QENewWalletWizard(self)
return self._new_wallet_wizard
@pyqtProperty(QEServerConnectWizard, notify=serverConnectWizardChanged)
def serverConnectWizard(self):
if not self._server_connect_wizard:
self._server_connect_wizard = QEServerConnectWizard(self)
return self._server_connect_wizard
@pyqtSlot()
def startNetwork(self):
self.daemon.start_network()