You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

350 lines
12 KiB

import os
import threading
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.i18n import _
from electrum.logging import get_logger
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
from .auth import AuthMixin, auth_protect
from .qefx import QEFX
from .qewallet import QEWallet
from .qewalletdb import QEWalletDB
from .qewizard import QENewWalletWizard, QEServerConnectWizard
# 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))
_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()
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
role_name = self._ROLE_NAMES[role_index]
if role_name == 'name':
return wallet_name
if role_name == 'path':
return wallet_path
if role_name == 'active':
return self.daemon.get_wallet(wallet_path) is not None
@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):
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)
self.wallets.append(item)
self.endInsertRows()
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):
_logger = get_logger(__name__)
_available_wallets = None
_current_wallet = None
_new_wallet_wizard = None
_server_connect_wizard = None
_path = None
_name = None
_use_single_password = False
_password = None
_loading = False
_backendWalletLoaded = pyqtSignal([str], arguments=['password'])
availableWalletsChanged = pyqtSignal()
fxChanged = pyqtSignal()
newWalletWizardChanged = pyqtSignal()
serverConnectWizardChanged = pyqtSignal()
loadingChanged = pyqtSignal()
passwordChangeFailed = pyqtSignal()
requestNewPassword = pyqtSignal()
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)
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)
@pyqtSlot()
@pyqtSlot(str)
@pyqtSlot(str, str)
def loadWallet(self, path=None, password=None):
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.get('gui_last_wallet')
else:
self._path = path
if self._path is None:
return
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
if not password:
password = self._password
wallet_already_open = self._path in self.daemon._wallets
if not wallet_already_open:
# pre-checks, let walletdb trigger any necessary user interactions
self._walletdb.path = self._path
self._walletdb.password = password
self._walletdb.verify()
if not self._walletdb.ready:
return
def load_wallet_task():
self._loading = True
self.loadingChanged.emit()
try:
local_password = password # need this in local scope
wallet = self.daemon.load_wallet(self._path, local_password)
if wallet is None:
self._logger.info('could not open wallet')
self.walletOpenError.emit('could not open wallet')
return
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.get('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')
self.daemon.config.save_last_wallet(wallet)
run_hook('load_wallet', wallet)
self._backendWalletLoaded.emit(local_password)
except WalletFileException as e:
self._logger.error(str(e))
self.walletOpenError.emit(str(e))
finally:
self._loading = False
self.loadingChanged.emit()
threading.Thread(target=load_wallet_task, daemon=True).start()
@pyqtSlot()
@pyqtSlot(str)
def _on_backend_wallet_loaded(self, password = None):
self._logger.debug('_on_backend_wallet_loaded')
wallet = self.daemon._wallets[self._path]
self._current_wallet = QEWallet.getInstanceFor(wallet)
self.availableWallets.updateWallet(self._path)
self._current_wallet.password = password if password else None
self.walletLoaded.emit(self._name, self._path)
@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
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)
def currentWallet(self):
return self._current_wallet
@pyqtProperty(QEWalletListModel, notify=availableWalletsChanged)
def availableWallets(self):
if not self._available_wallets:
self._available_wallets = QEWalletListModel(self.daemon)
return self._available_wallets
@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}'
@pyqtSlot()
@auth_protect(method='wallet')
def startChangePassword(self):
if self._use_single_password:
self.requestNewPassword.emit()
else:
self.currentWallet.requestNewPassword.emit()
@pyqtSlot(str)
def setPassword(self, password):
assert self._use_single_password
assert password
if not self.daemon.update_password_for_directory(old_password=self._password, new_password=password):
self.passwordChangeFailed.emit()
return
self._password = password
@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()