Add/remove plugins from GUI
- both internal and external plugins require GUI install (except internal HW plugins, which are 'auto-loaded' and hidden) - remove init_qt hook - in Qt, reload wallet windows if plugin enabled/disabled - add 'uninstall' button to PluginDialog - add 'add plugins' button to wizard hw screen - add icons to the plugin list
This commit is contained in:
@@ -158,11 +158,6 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
self._default_qtstylesheet = self.app.styleSheet()
|
||||
self.reload_app_stylesheet()
|
||||
|
||||
# always load 2fa
|
||||
self.plugins.load_plugin_by_name('trustedcoin')
|
||||
|
||||
run_hook('init_qt', self)
|
||||
|
||||
def _init_tray(self):
|
||||
self.tray = QSystemTrayIcon(self.tray_icon(), None)
|
||||
self.tray.setToolTip('Electrum')
|
||||
@@ -294,9 +289,9 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
self.lightning_dialog = LightningDialog(self)
|
||||
self.lightning_dialog.bring_to_top()
|
||||
|
||||
def show_plugins_dialog(self, wallet=None):
|
||||
def show_plugins_dialog(self):
|
||||
from .plugins_dialog import PluginsDialog
|
||||
d = PluginsDialog(self, wallet)
|
||||
d = PluginsDialog(self.config, self.plugins, gui_object=self)
|
||||
d.exec()
|
||||
|
||||
def show_network_dialog(self, proxy_tab=False):
|
||||
@@ -328,6 +323,11 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
self._maybe_quit_if_no_windows_open()
|
||||
return wrapper
|
||||
|
||||
def get_window_for_wallet(self, wallet):
|
||||
for window in self.windows:
|
||||
if window.wallet.storage.path == wallet.storage.path:
|
||||
return window
|
||||
|
||||
@count_wizards_in_progress
|
||||
def start_new_window(
|
||||
self,
|
||||
@@ -377,11 +377,9 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
wallet = self._start_wizard_to_select_or_create_wallet(path)
|
||||
if not wallet:
|
||||
return
|
||||
window = self.get_window_for_wallet(wallet)
|
||||
# create or raise window
|
||||
for window in self.windows:
|
||||
if window.wallet.storage.path == wallet.storage.path:
|
||||
break
|
||||
else:
|
||||
if not window:
|
||||
window = self._create_window_for_wallet(wallet)
|
||||
except UserCancelled:
|
||||
return
|
||||
@@ -491,7 +489,15 @@ class ElectrumGui(BaseElectrumGui, Logger):
|
||||
if not self.windows:
|
||||
self.config.save_last_wallet(window.wallet)
|
||||
run_hook('on_close_window', window)
|
||||
self.daemon.stop_wallet(window.wallet.storage.path)
|
||||
if window.should_stop_wallet_on_close:
|
||||
self.daemon.stop_wallet(window.wallet.storage.path)
|
||||
|
||||
def reload_windows(self):
|
||||
for window in list(self.windows):
|
||||
wallet = window.wallet
|
||||
window.should_stop_wallet_on_close = False
|
||||
window.close()
|
||||
self._create_window_for_wallet(wallet)
|
||||
|
||||
def init_network(self):
|
||||
"""Start the network, including showing a first-start network dialog if config does not exist."""
|
||||
|
||||
@@ -165,6 +165,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet):
|
||||
QMainWindow.__init__(self)
|
||||
self.gui_object = gui_object
|
||||
self.should_stop_wallet_on_close = True
|
||||
self.config = config = gui_object.config # type: SimpleConfig
|
||||
self.gui_thread = gui_object.gui_thread
|
||||
assert wallet, "no wallet"
|
||||
@@ -759,7 +760,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
tools_menu.addAction(_("Electrum preferences"), self.settings_dialog)
|
||||
|
||||
tools_menu.addAction(_("&Network"), self.gui_object.show_network_dialog).setEnabled(bool(self.network))
|
||||
tools_menu.addAction(_("&Plugins"), partial(self.gui_object.show_plugins_dialog, self.wallet))
|
||||
tools_menu.addAction(_("&Plugins"), self.gui_object.show_plugins_dialog)
|
||||
tools_menu.addSeparator()
|
||||
tools_menu.addAction(_("&Sign/verify message"), self.sign_verify_message)
|
||||
tools_menu.addAction(_("&Encrypt/decrypt message"), self.encrypt_message)
|
||||
@@ -2633,8 +2634,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
self.save_notes_text()
|
||||
if not self.isMaximized():
|
||||
g = self.geometry()
|
||||
self.wallet.db.put("winpos-qt", [g.left(),g.top(),
|
||||
g.width(),g.height()])
|
||||
self.wallet.db.put(
|
||||
"winpos-qt", [g.left(),g.top(), g.width(),g.height()])
|
||||
if self.qr_window:
|
||||
self.qr_window.close()
|
||||
self.close_wallet()
|
||||
|
||||
@@ -3,10 +3,10 @@ from functools import partial
|
||||
import shutil
|
||||
import os
|
||||
|
||||
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QWidget, QScrollArea, QFormLayout, QFileDialog
|
||||
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton, QWidget, QScrollArea, QFormLayout, QFileDialog, QMenu, QApplication
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.plugin import run_hook
|
||||
|
||||
from .util import WindowModalDialog, Buttons, CloseButton, WWLabel, insert_spaces, MessageBoxMixin, EnterButton
|
||||
|
||||
@@ -14,7 +14,8 @@ from .util import WindowModalDialog, Buttons, CloseButton, WWLabel, insert_space
|
||||
if TYPE_CHECKING:
|
||||
from . import ElectrumGui
|
||||
from electrum_cc import ECPrivkey
|
||||
from electrum.wallet import Abstract_Wallet
|
||||
from electrum.simple_config import SimpleConfig
|
||||
from electrum.plugin import Plugins
|
||||
|
||||
|
||||
class PluginDialog(WindowModalDialog):
|
||||
@@ -47,45 +48,55 @@ class PluginDialog(WindowModalDialog):
|
||||
msg = '\n'.join(map(lambda x: x[1], requires))
|
||||
form.addRow(QLabel(_('Requires') + ':'), WWLabel(msg))
|
||||
vbox.addLayout(form)
|
||||
toggle_button = QPushButton('')
|
||||
if not self.plugins.is_installed(name):
|
||||
toggle_button.setText(_('Install...'))
|
||||
toggle_button.clicked.connect(self.accept)
|
||||
else:
|
||||
text = (_('Disable') if p else _('Enable')) if self.plugins.is_authorized(name) else _('Authorize...')
|
||||
toggle_button.setText(text)
|
||||
toggle_button.clicked.connect(partial(self.do_toggle, toggle_button, name))
|
||||
close_button = CloseButton(self)
|
||||
close_button.setText(_('Close'))
|
||||
buttons = [toggle_button, close_button]
|
||||
# add settings widget
|
||||
if p and p.requires_settings() and p.is_enabled() and self.window.wallet is not None:
|
||||
button = EnterButton(
|
||||
_('Settings'),
|
||||
partial(p.settings_dialog, self, self.window.wallet))
|
||||
buttons.insert(0, button)
|
||||
buttons = [close_button]
|
||||
if not self.plugins.is_installed(name):
|
||||
install_button = QPushButton(_('Install...'))
|
||||
install_button.clicked.connect(self.accept)
|
||||
buttons.insert(0, install_button)
|
||||
else:
|
||||
remove_button = QPushButton(_('Uninstall'))
|
||||
remove_button.clicked.connect(self.do_remove)
|
||||
buttons.insert(0, remove_button)
|
||||
if not self.plugins.is_authorized(name):
|
||||
auth_button = QPushButton(_('Authorize...'))
|
||||
auth_button.clicked.connect(self.do_authorize)
|
||||
buttons.insert(0, auth_button)
|
||||
elif not self.plugins.is_auto_loaded(name):
|
||||
toggle_button = QPushButton('')
|
||||
p = self.plugins.get(name)
|
||||
is_enabled = p and p.is_enabled()
|
||||
toggle_button.setText(_('Disable') if is_enabled else _('Enable'))
|
||||
toggle_button.clicked.connect(self.do_toggle)
|
||||
buttons.insert(0, toggle_button)
|
||||
# add settings button
|
||||
if p and p.requires_settings() and p.is_enabled():
|
||||
settings_button = EnterButton(
|
||||
_('Settings'),
|
||||
partial(p.settings_dialog, self))
|
||||
buttons.insert(1, settings_button)
|
||||
# add buttonss
|
||||
vbox.addLayout(Buttons(*buttons))
|
||||
|
||||
def do_toggle(self, toggle_button, name):
|
||||
toggle_button.setEnabled(False)
|
||||
if not self.plugins.is_authorized(name):
|
||||
privkey = self.window.get_plugins_privkey()
|
||||
if not privkey:
|
||||
return
|
||||
filename = self.plugins.zip_plugin_path(name)
|
||||
self.window.plugins.authorize_plugin(name, filename, privkey)
|
||||
self.status_button.update()
|
||||
self.close()
|
||||
return
|
||||
p = self.plugins.get(name)
|
||||
if not p:
|
||||
self.plugins.enable(name)
|
||||
else:
|
||||
self.plugins.disable(name)
|
||||
self.status_button.update()
|
||||
def do_toggle(self):
|
||||
self.close()
|
||||
self.window.do_toggle(self.name, self.status_button)
|
||||
|
||||
def do_remove(self):
|
||||
self.window.uninstall_plugin(self.name)
|
||||
self.close()
|
||||
|
||||
def do_authorize(self):
|
||||
assert not self.plugins.is_authorized(self.name)
|
||||
privkey = self.window.get_plugins_privkey()
|
||||
if not privkey:
|
||||
return
|
||||
filename = self.plugins.zip_plugin_path(self.name)
|
||||
self.window.plugins.authorize_plugin(self.name, filename, privkey)
|
||||
if self.status_button:
|
||||
self.status_button.update()
|
||||
self.close()
|
||||
# note: all enabled plugins will receive this hook:
|
||||
run_hook('init_qt', self.window.gui_object)
|
||||
|
||||
|
||||
class PluginStatusButton(QPushButton):
|
||||
@@ -112,20 +123,27 @@ class PluginStatusButton(QPushButton):
|
||||
or plugin_is_loaded and p.can_user_disable()
|
||||
)
|
||||
self.setEnabled(enabled)
|
||||
text, color = (_('Unauthorized'), ColorScheme.RED) if not self.window.plugins.is_authorized(self.name)\
|
||||
else ((_('Enabled'), ColorScheme.BLUE) if p is not None and p.is_enabled() else (_('Disabled'), ColorScheme.DEFAULT))
|
||||
if not self.window.plugins.is_authorized(self.name):
|
||||
text, color = _('Unauthorized'), ColorScheme.RED
|
||||
else:
|
||||
if self.window.plugins.is_auto_loaded(self.name):
|
||||
text, color = _('Auto-loaded'), ColorScheme.DEFAULT
|
||||
else:
|
||||
if p is not None and p.is_enabled():
|
||||
text, color = _('Enabled'), ColorScheme.BLUE
|
||||
else:
|
||||
text, color = _('Disabled'), ColorScheme.DEFAULT
|
||||
self.setStyleSheet(color.as_stylesheet())
|
||||
self.setText(text)
|
||||
|
||||
|
||||
class PluginsDialog(WindowModalDialog, MessageBoxMixin):
|
||||
|
||||
def __init__(self, gui_object: 'ElectrumGui', wallet: Optional['Abstract_Wallet']):
|
||||
def __init__(self, config: 'SimpleConfig', plugins:'Plugins', *, gui_object: Optional['ElectrumGui'] = None):
|
||||
WindowModalDialog.__init__(self, None, _('Electrum Plugins'))
|
||||
self.gui_object = gui_object
|
||||
self.config = gui_object.config
|
||||
self.plugins = gui_object.plugins
|
||||
self.wallet = wallet
|
||||
self.config = config
|
||||
self.plugins = plugins
|
||||
vbox = QVBoxLayout(self)
|
||||
scroll = QScrollArea()
|
||||
scroll.setEnabled(True)
|
||||
@@ -138,8 +156,19 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin):
|
||||
scroll_w.setLayout(self.grid)
|
||||
vbox.addWidget(scroll)
|
||||
add_button = QPushButton(_('Add'))
|
||||
add_button.clicked.connect(self.add_plugin_dialog)
|
||||
#add_button.clicked.connect(self.download_plugin_dialog)
|
||||
menu = QMenu()
|
||||
for name, item in self.plugins.internal_plugin_metadata.items():
|
||||
fullname = item['fullname']
|
||||
if not fullname:
|
||||
continue
|
||||
if self.plugins.is_auto_loaded(name):
|
||||
continue
|
||||
menu.addAction(fullname, partial(self.add_internal_plugin, name))
|
||||
menu.addSeparator()
|
||||
m3 = menu.addMenu('Third-party plugin')
|
||||
m3.addAction(_('Local ZIP file'), self.add_plugin_dialog)
|
||||
m3.addAction(_('Download ZIP file'), self.download_plugin_dialog)
|
||||
add_button.setMenu(menu)
|
||||
vbox.addLayout(Buttons(add_button, CloseButton(self)))
|
||||
self.show_list()
|
||||
|
||||
@@ -187,11 +216,15 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin):
|
||||
return
|
||||
key_hex = self.plugins.create_new_key(pw)
|
||||
keyfile_path, keyfile_help = self.plugins.get_keyfile_path()
|
||||
msg = ''.join([
|
||||
_('Your plugins key is:'), '\n\n', key_hex, '\n\n',
|
||||
_('Please save this key in'), '\n\n' + keyfile_path, '\n\n', keyfile_help
|
||||
msg = '\n\n'.join([
|
||||
_('Your plugins key is:'), key_hex,
|
||||
_('This key has been copied to your clipboard. Please save it in:'),
|
||||
keyfile_path,
|
||||
keyfile_help,
|
||||
'',
|
||||
])
|
||||
self.gui_object.do_copy(key_hex, title=_('Plugins key'))
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(key_hex)
|
||||
self.show_message(msg)
|
||||
|
||||
def download_plugin_dialog(self):
|
||||
@@ -207,14 +240,14 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin):
|
||||
return
|
||||
coro = self.plugins.download_external_plugin(url)
|
||||
try:
|
||||
path = self.window.run_coroutine_dialog(coro, "Downloading plugin...")
|
||||
path = self.window.run_coroutine_dialog(coro, _("Downloading plugin..."))
|
||||
except UserCancelled:
|
||||
return
|
||||
except Exception as e:
|
||||
self.show_error(f"{e}")
|
||||
return
|
||||
try:
|
||||
success = self.confirm_add_plugin(path)
|
||||
success = self.add_external_plugin(path)
|
||||
except Exception as e:
|
||||
self.show_error(f"{e}")
|
||||
success = False
|
||||
@@ -226,21 +259,21 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin):
|
||||
if not pubkey:
|
||||
self.init_plugins_password()
|
||||
return
|
||||
filename, __ = QFileDialog.getOpenFileName(self, "Select your plugin zipfile", "", "*.zip")
|
||||
filename, __ = QFileDialog.getOpenFileName(self, _("Select your plugin zipfile"), "", "*.zip")
|
||||
if not filename:
|
||||
return
|
||||
plugins_dir = self.plugins.get_external_plugin_dir()
|
||||
path = os.path.join(plugins_dir, os.path.basename(filename))
|
||||
shutil.copyfile(filename, path)
|
||||
try:
|
||||
success = self.confirm_add_plugin(path)
|
||||
success = self.add_external_plugin(path)
|
||||
except Exception as e:
|
||||
self.show_error(f"{e}")
|
||||
success = False
|
||||
if not success:
|
||||
os.unlink(path)
|
||||
|
||||
def confirm_add_plugin(self, path):
|
||||
def add_external_plugin(self, path):
|
||||
manifest = self.plugins.read_manifest(path)
|
||||
name = manifest['name']
|
||||
d = PluginDialog(name, manifest, None, self)
|
||||
@@ -250,13 +283,21 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin):
|
||||
privkey = self.get_plugins_privkey()
|
||||
if not privkey:
|
||||
return False
|
||||
self.plugins.external_plugin_metadata[name] = manifest
|
||||
self.plugins.authorize_plugin(name, path, privkey)
|
||||
self.show_message(_('Plugin installed successfully.'))
|
||||
self.plugins.install_external_plugin(name, path, privkey, manifest)
|
||||
self.show_list()
|
||||
return True
|
||||
|
||||
def add_internal_plugin(self, name):
|
||||
""" simply set the config """
|
||||
manifest = self.plugins.internal_plugin_metadata[name]
|
||||
d = PluginDialog(name, manifest, None, self)
|
||||
if not d.exec():
|
||||
return False
|
||||
self.plugins.install_internal_plugin(name)
|
||||
self.show_list()
|
||||
|
||||
def show_list(self):
|
||||
from .util import read_QIcon_from_bytes, IconLabel
|
||||
descriptions = self.plugins.descriptions
|
||||
descriptions = sorted(descriptions.items())
|
||||
grid = self.grid
|
||||
@@ -267,14 +308,47 @@ class PluginsDialog(WindowModalDialog, MessageBoxMixin):
|
||||
i = 0
|
||||
for name, metadata in descriptions:
|
||||
i += 1
|
||||
if metadata.get('registers_keystore'):
|
||||
if self.plugins.is_internal(name) and self.plugins.is_auto_loaded(name):
|
||||
continue
|
||||
if not self.plugins.is_installed(name):
|
||||
continue
|
||||
display_name = metadata.get('fullname')
|
||||
if not display_name:
|
||||
continue
|
||||
label = QLabel(display_name)
|
||||
label = IconLabel(text=display_name, reverse=True)
|
||||
icon_path = metadata.get('icon')
|
||||
if icon_path:
|
||||
icon = read_QIcon_from_bytes(self.plugins.read_file(name, icon_path))
|
||||
label.setIcon(icon)
|
||||
label.status_button = PluginStatusButton(self, name)
|
||||
grid.addWidget(label, i, 0)
|
||||
status_button = PluginStatusButton(self, name)
|
||||
grid.addWidget(status_button, i, 1)
|
||||
grid.addWidget(label.status_button, i, 1)
|
||||
# add stretch
|
||||
grid.setRowStretch(i + 1, 1)
|
||||
|
||||
def do_toggle(self, name, status_button):
|
||||
if not self.plugins.is_authorized(name):
|
||||
#self.show_plugin_dialog(name, status_button)
|
||||
return
|
||||
if self.plugins.is_auto_loaded(name):
|
||||
return
|
||||
p = self.plugins.get(name)
|
||||
is_enabled = p and p.is_enabled()
|
||||
if is_enabled:
|
||||
self.plugins.disable(name)
|
||||
else:
|
||||
self.plugins.enable(name)
|
||||
if status_button:
|
||||
status_button.update()
|
||||
if self.gui_object:
|
||||
self.gui_object.reload_windows()
|
||||
self.setFocus()
|
||||
self.activateWindow()
|
||||
|
||||
def uninstall_plugin(self, name):
|
||||
if not self.question(_('Remove plugin \'{}\'?').format(name)):
|
||||
return
|
||||
self.plugins.uninstall(name)
|
||||
if self.gui_object:
|
||||
self.gui_object.reload_windows()
|
||||
self.show_list()
|
||||
|
||||
@@ -30,6 +30,7 @@ from electrum.gui.qt.password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PA
|
||||
from electrum.gui.qt.seed_dialog import SeedWidget, MSG_PASSPHRASE_WARN_ISSUE4566, KeysWidget
|
||||
from electrum.gui.qt.util import (PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height,
|
||||
ChoiceWidget, MessageBoxMixin, icon_path, IconLabel, read_QIcon)
|
||||
from electrum.gui.qt.plugins_dialog import PluginsDialog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.simple_config import SimpleConfig
|
||||
@@ -1081,6 +1082,7 @@ class WCChooseHWDevice(WalletWizardComponent, Logger):
|
||||
self.scanFailed.connect(self.on_scan_failed)
|
||||
self.scanComplete.connect(self.on_scan_complete)
|
||||
self.plugins = wizard.plugins
|
||||
self.config = wizard.config
|
||||
|
||||
self.error_l = WWLabel()
|
||||
self.error_l.setVisible(False)
|
||||
@@ -1093,9 +1095,13 @@ class WCChooseHWDevice(WalletWizardComponent, Logger):
|
||||
self.rescan_button = QPushButton(_('Rescan devices'))
|
||||
self.rescan_button.clicked.connect(self.on_rescan)
|
||||
|
||||
self.add_plugin_button = QPushButton(_('Add plugin'))
|
||||
self.add_plugin_button.clicked.connect(self.on_add_plugin)
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addStretch(1)
|
||||
hbox.addWidget(self.rescan_button)
|
||||
hbox.addWidget(self.add_plugin_button)
|
||||
hbox.addStretch(1)
|
||||
|
||||
self.layout().addWidget(self.error_l)
|
||||
@@ -1110,6 +1116,11 @@ class WCChooseHWDevice(WalletWizardComponent, Logger):
|
||||
def on_rescan(self):
|
||||
self.scan_devices()
|
||||
|
||||
def on_add_plugin(self):
|
||||
d = PluginsDialog(self.config, self.plugins)
|
||||
d.exec()
|
||||
self.scan_devices()
|
||||
|
||||
def on_scan_failed(self, code, message):
|
||||
self.error_l.setText(message)
|
||||
self.error_l.setVisible(True)
|
||||
|
||||
@@ -135,7 +135,8 @@ class Plugins(DaemonThread):
|
||||
self.register_keystore(name, gui_good, details)
|
||||
if name in self.internal_plugin_metadata or name in self.external_plugin_metadata:
|
||||
_logger.info(f"Found the following plugin modules: {iter_modules=}")
|
||||
raise Exception(f"duplicate plugins? for {name=}")
|
||||
_logger.info(f"duplicate plugins? for {name=}")
|
||||
continue
|
||||
if not external:
|
||||
self.internal_plugin_metadata[name] = d
|
||||
else:
|
||||
@@ -296,10 +297,9 @@ class Plugins(DaemonThread):
|
||||
except Exception:
|
||||
self.logger.info(f"could not load manifest.json from zip plugin {filename}", exc_info=True)
|
||||
continue
|
||||
if name in self.internal_plugin_metadata:
|
||||
raise Exception(f"duplicate plugins for name={name}")
|
||||
if name in self.external_plugin_metadata:
|
||||
raise Exception(f"duplicate plugins for name={name}")
|
||||
if name in self.internal_plugin_metadata or name in self.external_plugin_metadata:
|
||||
self.logger.info(f"duplicate plugins for {name=}")
|
||||
continue
|
||||
if self.cmd_only and not self.config.get(f'plugins.{name}.enabled'):
|
||||
continue
|
||||
min_version = d.get('min_electrum_version')
|
||||
@@ -361,9 +361,6 @@ class Plugins(DaemonThread):
|
||||
init_spec = zipfile.find_spec(dirname)
|
||||
|
||||
self.exec_module_from_spec(init_spec, base_name)
|
||||
if name == "trustedcoin":
|
||||
# removes trustedcoin after loading to not show it in the list of plugins
|
||||
del self.internal_plugin_metadata[name]
|
||||
|
||||
def load_plugin_by_name(self, name: str) -> 'BasePlugin':
|
||||
if name in self.plugins:
|
||||
@@ -400,9 +397,31 @@ class Plugins(DaemonThread):
|
||||
secret = pbkdf2_hmac('sha256', pw.encode('utf-8'), salt, iterations=10**5)
|
||||
return ECPrivkey(secret)
|
||||
|
||||
def install_internal_plugin(self, name):
|
||||
self.config.set_key(f'plugins.{name}.enabled', [])
|
||||
|
||||
def install_external_plugin(self, name, path, privkey, manifest):
|
||||
self.external_plugin_metadata[name] = manifest
|
||||
self.authorize_plugin(name, path, privkey)
|
||||
|
||||
def uninstall(self, name: str):
|
||||
self.config.set_key(f'plugins.{name}', None)
|
||||
if name in self.external_plugin_metadata:
|
||||
zipfile = self.zip_plugin_path(name)
|
||||
os.unlink(zipfile)
|
||||
self.external_plugin_metadata.pop(name)
|
||||
|
||||
def is_internal(self, name) -> bool:
|
||||
return name in self.internal_plugin_metadata
|
||||
|
||||
def is_auto_loaded(self, name):
|
||||
metadata = self.external_plugin_metadata.get(name) or self.internal_plugin_metadata.get(name)
|
||||
return metadata and (metadata.get('registers_keystore') or metadata.get('registers_wallet_type'))
|
||||
|
||||
def is_installed(self, name) -> bool:
|
||||
"""an external plugin may be installed but not authorized """
|
||||
return name in self.internal_plugin_metadata or name in self.external_plugin_metadata
|
||||
return (name in self.internal_plugin_metadata and self.config.get(f'plugins.{name}'))\
|
||||
or name in self.external_plugin_metadata
|
||||
|
||||
def is_authorized(self, name) -> bool:
|
||||
if name in self.internal_plugin_metadata:
|
||||
@@ -431,14 +450,14 @@ class Plugins(DaemonThread):
|
||||
self.config.set_key(f'plugins.{name}.authorized', value, save=True)
|
||||
|
||||
def enable(self, name: str) -> 'BasePlugin':
|
||||
self.config.set_key(f'plugins.{name}.enabled', True, save=True)
|
||||
self.config.enable_plugin(name)
|
||||
p = self.get(name)
|
||||
if p:
|
||||
return p
|
||||
return self.load_plugin(name)
|
||||
|
||||
def disable(self, name: str) -> None:
|
||||
self.config.set_key(f'plugins.{name}.enabled', False, save=True)
|
||||
self.config.disable_plugin(name)
|
||||
p = self.get(name)
|
||||
if not p:
|
||||
return
|
||||
@@ -450,10 +469,6 @@ class Plugins(DaemonThread):
|
||||
def is_plugin_enabler_config_key(cls, key: str) -> bool:
|
||||
return key.startswith('plugins.')
|
||||
|
||||
def toggle(self, name: str) -> Optional['BasePlugin']:
|
||||
p = self.get(name)
|
||||
return self.disable(name) if p else self.enable(name)
|
||||
|
||||
def is_available(self, name: str, wallet: 'Abstract_Wallet') -> bool:
|
||||
d = self.descriptions.get(name)
|
||||
if not d:
|
||||
@@ -531,6 +546,19 @@ class Plugins(DaemonThread):
|
||||
self.run_jobs()
|
||||
self.on_stop()
|
||||
|
||||
def read_file(self, name: str, filename: str) -> bytes:
|
||||
if self.is_plugin_zip(name):
|
||||
plugin_filename = self.zip_plugin_path(name)
|
||||
metadata = self.external_plugin_metadata[name]
|
||||
dirname = metadata['dirname']
|
||||
with zipfile_lib.ZipFile(plugin_filename) as myzip:
|
||||
with myzip.open(os.path.join(dirname, filename)) as myfile:
|
||||
return myfile.read()
|
||||
else:
|
||||
assert name in self.internal_plugin_metadata
|
||||
path = os.path.join(os.path.dirname(__file__), 'plugins', name, filename)
|
||||
with open(path, 'rb') as myfile:
|
||||
return myfile.read()
|
||||
|
||||
def get_file_hash256(path: str) -> bytes:
|
||||
'''Get the sha256 hash of a file, similar to `sha256sum`.'''
|
||||
@@ -567,7 +595,6 @@ class BasePlugin(Logger):
|
||||
self.parent = parent # type: Plugins # The plugins object
|
||||
self.name = name
|
||||
self.config = config
|
||||
self.wallet = None # fixme: this field should not exist
|
||||
Logger.__init__(self)
|
||||
# add self to hooks
|
||||
for k in dir(self):
|
||||
@@ -604,7 +631,9 @@ class BasePlugin(Logger):
|
||||
return []
|
||||
|
||||
def is_enabled(self):
|
||||
return self.is_available() and self.config.get(f'plugins.{self.name}.enabled') is True
|
||||
if not self.is_available():
|
||||
return False
|
||||
return self.config.is_plugin_enabled(self.name)
|
||||
|
||||
def is_available(self):
|
||||
return True
|
||||
@@ -619,20 +648,7 @@ class BasePlugin(Logger):
|
||||
raise NotImplementedError()
|
||||
|
||||
def read_file(self, filename: str) -> bytes:
|
||||
if self.parent.is_plugin_zip(self.name):
|
||||
plugin_filename = self.parent.zip_plugin_path(self.name)
|
||||
metadata = self.parent.external_plugin_metadata[self.name]
|
||||
dirname = metadata['dirname']
|
||||
with zipfile_lib.ZipFile(plugin_filename) as myzip:
|
||||
with myzip.open(os.path.join(dirname, filename)) as myfile:
|
||||
return myfile.read()
|
||||
else:
|
||||
if self.name in self.parent.internal_plugin_metadata:
|
||||
path = os.path.join(os.path.dirname(__file__), 'plugins', self.name, filename)
|
||||
else:
|
||||
path = os.path.join(self.parent.get_external_plugin_dir(), self.name, filename)
|
||||
with open(path, 'rb') as myfile:
|
||||
return myfile.read()
|
||||
return self.parent.read_file(self.name, filename)
|
||||
|
||||
|
||||
class DeviceUnpairableError(UserFacingException): pass
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"fullname": "Blockstream Jade Wallet",
|
||||
"description": "Provides support for the Blockstream Jade hardware wallet",
|
||||
"registers_keystore": ["hardware", "jade", "Jade wallet"],
|
||||
"icon":"jade.png",
|
||||
"available_for": ["qt", "cmdline"]
|
||||
}
|
||||
|
||||
@@ -67,16 +67,6 @@ class Plugin(LabelsPlugin):
|
||||
self.logger.error("Error synchronising labels", exc_info=exc_info)
|
||||
dialog.show_error(_("Error synchronising labels") + f':\n{repr(exc_info[1])}')
|
||||
|
||||
@hook
|
||||
def init_qt(self, gui: 'ElectrumGui'):
|
||||
if self._init_qt_received: # only need/want the first signal
|
||||
return
|
||||
self._init_qt_received = True
|
||||
# If the user just enabled the plugin, the 'load_wallet' hook would not
|
||||
# get called for already loaded wallets, hence we call it manually for those:
|
||||
for window in gui.windows:
|
||||
self.load_wallet(window.wallet, window)
|
||||
|
||||
@hook
|
||||
def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):
|
||||
self.obj.labels_changed_signal.connect(window.update_tabs)
|
||||
|
||||
@@ -25,27 +25,23 @@ class Plugin(NWCServerPlugin):
|
||||
|
||||
@hook
|
||||
def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):
|
||||
self.start_plugin(wallet)
|
||||
|
||||
@hook
|
||||
def init_qt(self, gui: 'ElectrumGui'):
|
||||
if self._init_qt_received:
|
||||
if not wallet.has_lightning():
|
||||
return
|
||||
self._init_qt_received = True
|
||||
for w in gui.windows:
|
||||
self.start_plugin(w.wallet)
|
||||
self.start_plugin(wallet)
|
||||
|
||||
def requires_settings(self):
|
||||
return True
|
||||
|
||||
def settings_dialog(self, window: WindowModalDialog, wallet: 'Abstract_Wallet'):
|
||||
if not wallet.has_lightning():
|
||||
window.show_error(_("{} plugin requires a lightning enabled wallet. Setup lightning first.")
|
||||
.format("NWC"))
|
||||
if not self.initialized:
|
||||
window.show_error(
|
||||
_("{} plugin requires a lightning enabled wallet. Open a lightning-enabled wallet first.")
|
||||
.format("NWC"))
|
||||
return
|
||||
|
||||
d = WindowModalDialog(window, _("Nostr Wallet Connect"))
|
||||
main_layout = QVBoxLayout(d)
|
||||
main_layout.addWidget(QLabel(_("Using wallet:") + ' ' + self.nwc_server.wallet.basename()))
|
||||
|
||||
# Connections list
|
||||
main_layout.addWidget(QLabel(_("Existing Connections:")))
|
||||
|
||||
@@ -70,14 +70,6 @@ class Plugin(BasePlugin):
|
||||
self.cosigner_wallets = {} # type: Dict[Abstract_Wallet, CosignerWallet]
|
||||
|
||||
|
||||
@hook
|
||||
def init_qt(self, gui: 'ElectrumGui'):
|
||||
if self._init_qt_received: # only need/want the first signal
|
||||
return
|
||||
self._init_qt_received = True
|
||||
for window in gui.windows:
|
||||
self.load_wallet(window.wallet, window)
|
||||
|
||||
@hook
|
||||
def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):
|
||||
if type(wallet) != Multisig_Wallet:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "revealer",
|
||||
"fullname": "Revealer Backup Utility",
|
||||
"description": "This plug-in allows you to create a visually encrypted backup of your wallet seeds, or of custom alphanumeric secrets.",
|
||||
"available_for": ["qt"]
|
||||
"name": "revealer",
|
||||
"fullname": "Revealer Backup Utility",
|
||||
"description": "This plug-in allows you to create a visually encrypted backup of your wallet seeds, or of custom alphanumeric secrets.",
|
||||
"icon": "revealer.png",
|
||||
"available_for": ["qt"]
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ class Plugin(RevealerPlugin):
|
||||
self.icon_bytes = self.read_file("revealer.png")
|
||||
|
||||
@hook
|
||||
def init_qt(self, gui: 'ElectrumGui'):
|
||||
def load_wallet(self, wallet, window):
|
||||
if self._init_qt_received: # only need/want the first signal
|
||||
return
|
||||
self._init_qt_received = True
|
||||
|
||||
@@ -346,6 +346,15 @@ class SimpleConfig(Logger):
|
||||
assert isinstance(key, str), key
|
||||
return self.get(key, default=...) is not ...
|
||||
|
||||
def is_plugin_enabled(self, name: str) -> bool:
|
||||
return bool(self.get(f'plugins.{name}.enabled'))
|
||||
|
||||
def enable_plugin(self, name: str):
|
||||
self.set_key(f'plugins.{name}.enabled', True, save=True)
|
||||
|
||||
def disable_plugin(self, name: str):
|
||||
self.set_key(f'plugins.{name}.enabled', False, save=True)
|
||||
|
||||
def _check_dependent_keys(self) -> None:
|
||||
if self.NETWORK_SERVERFINGERPRINT:
|
||||
if not self.NETWORK_SERVER:
|
||||
|
||||
@@ -272,6 +272,8 @@ class NewWalletWizard(AbstractWizard):
|
||||
}
|
||||
self._daemon = daemon
|
||||
self.plugins = plugins
|
||||
# todo: load only if needed, like hw plugins
|
||||
self.plugins.load_plugin_by_name('trustedcoin')
|
||||
|
||||
def start(self, initial_data: dict = None) -> WizardViewState:
|
||||
if initial_data is None:
|
||||
|
||||
Reference in New Issue
Block a user