The qt, qml, and kivy GUIs have a first-start network-setup screen that allows the user customising the network settings before creating a wallet. Previously the daemon used to create the network and start it, before this screen, before the GUI even starts. If the user changed network settings, those would be set on the already running network, potentially including restarting the network. Now it becomes the responsibility of the GUI to start the network, allowing this first-start customisation to take place before starting the network at all. The qt and the qml GUIs are adapted to make use of this. Kivy, and the other prototype GUIs are not adapted and just start the network right away, as before.
344 lines
12 KiB
Python
344 lines
12 KiB
Python
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:
|
|
self._logger.debug('HM, %s is not %s', wallet_path, path)
|
|
wallets.append((wallet_name, wallet_path))
|
|
i += 1
|
|
|
|
if remove >= 0:
|
|
self.beginRemoveRows(QModelIndex(), i, i)
|
|
self.wallets = wallets
|
|
self.endRemoveRows()
|
|
|
|
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 load_wallet(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:
|
|
wallet = self.daemon.load_wallet(self._path, password)
|
|
|
|
if wallet is None:
|
|
self._logger.info('could not open wallet')
|
|
self.walletOpenError.emit('could not open wallet')
|
|
return
|
|
|
|
if self.daemon.config.get('single_password'):
|
|
self._use_single_password = self.daemon.update_password_for_directory(old_password=password, new_password=password)
|
|
self._password = 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(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
|
|
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):
|
|
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()
|