1
0

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:
ThomasV
2025-04-14 12:08:32 +02:00
parent d1e1ca7fd2
commit 8c028f7528
13 changed files with 240 additions and 141 deletions

View File

@@ -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."""

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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:")))

View File

@@ -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:

View File

@@ -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"]
}

View File

@@ -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

View File

@@ -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:

View File

@@ -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: