From 8c028f75288df00945a0a9f6cd746729300cdec5 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 14 Apr 2025 12:08:32 +0200 Subject: [PATCH] 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 --- electrum/gui/qt/__init__.py | 30 ++-- electrum/gui/qt/main_window.py | 7 +- electrum/gui/qt/plugins_dialog.py | 196 ++++++++++++++++-------- electrum/gui/qt/wizard/wallet.py | 11 ++ electrum/plugin.py | 78 ++++++---- electrum/plugins/jade/manifest.json | 1 + electrum/plugins/labels/qt.py | 10 -- electrum/plugins/nwc/qt.py | 18 +-- electrum/plugins/psbt_nostr/qt.py | 8 - electrum/plugins/revealer/manifest.json | 9 +- electrum/plugins/revealer/qt.py | 2 +- electrum/simple_config.py | 9 ++ electrum/wizard.py | 2 + 13 files changed, 240 insertions(+), 141 deletions(-) diff --git a/electrum/gui/qt/__init__.py b/electrum/gui/qt/__init__.py index d9d418c3a..341f36a01 100644 --- a/electrum/gui/qt/__init__.py +++ b/electrum/gui/qt/__init__.py @@ -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.""" diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index ee013f5a5..3be80af8c 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -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() diff --git a/electrum/gui/qt/plugins_dialog.py b/electrum/gui/qt/plugins_dialog.py index 5b77f4ab2..af84beb93 100644 --- a/electrum/gui/qt/plugins_dialog.py +++ b/electrum/gui/qt/plugins_dialog.py @@ -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() diff --git a/electrum/gui/qt/wizard/wallet.py b/electrum/gui/qt/wizard/wallet.py index fa592b143..d98ecc7ab 100644 --- a/electrum/gui/qt/wizard/wallet.py +++ b/electrum/gui/qt/wizard/wallet.py @@ -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) diff --git a/electrum/plugin.py b/electrum/plugin.py index f272f9220..c2e5cd7dd 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -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 diff --git a/electrum/plugins/jade/manifest.json b/electrum/plugins/jade/manifest.json index 57c00105c..b12894add 100644 --- a/electrum/plugins/jade/manifest.json +++ b/electrum/plugins/jade/manifest.json @@ -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"] } diff --git a/electrum/plugins/labels/qt.py b/electrum/plugins/labels/qt.py index 1dd8dba01..dd44f65ba 100644 --- a/electrum/plugins/labels/qt.py +++ b/electrum/plugins/labels/qt.py @@ -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) diff --git a/electrum/plugins/nwc/qt.py b/electrum/plugins/nwc/qt.py index b1cb81fe9..a58f2c714 100644 --- a/electrum/plugins/nwc/qt.py +++ b/electrum/plugins/nwc/qt.py @@ -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:"))) diff --git a/electrum/plugins/psbt_nostr/qt.py b/electrum/plugins/psbt_nostr/qt.py index 2acdc5549..53926ec8b 100644 --- a/electrum/plugins/psbt_nostr/qt.py +++ b/electrum/plugins/psbt_nostr/qt.py @@ -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: diff --git a/electrum/plugins/revealer/manifest.json b/electrum/plugins/revealer/manifest.json index 7b1a83801..b23254b87 100644 --- a/electrum/plugins/revealer/manifest.json +++ b/electrum/plugins/revealer/manifest.json @@ -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"] } diff --git a/electrum/plugins/revealer/qt.py b/electrum/plugins/revealer/qt.py index 4d01761d9..ca644f66c 100644 --- a/electrum/plugins/revealer/qt.py +++ b/electrum/plugins/revealer/qt.py @@ -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 diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 15aa64b0b..e64d2da0f 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -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: diff --git a/electrum/wizard.py b/electrum/wizard.py index 9df66ce61..2f3c2e4fa 100644 --- a/electrum/wizard.py +++ b/electrum/wizard.py @@ -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: