diff --git a/contrib/make_plugin b/contrib/make_plugin
index 45083fe16..6c454e768 100755
--- a/contrib/make_plugin
+++ b/contrib/make_plugin
@@ -35,12 +35,18 @@ with zipfile.ZipFile(zip_path, 'w') as zip_object:
print('added', dest_path)
# read version
-zip_file = zipimport.zipimporter(zip_path)
-module = zip_file.load_module(plugin_name)
-if not module.version:
+try:
+ with open(os.path.join(source_dir, 'manifest.json'), 'r') as f:
+ manifest = json.load(f)
+ version = manifest.get('version')
+except FileNotFoundError:
+ raise Exception(f"plugin doesn't contain manifest.json")
+
+if not version:
raise Exception('version not set')
-versioned_plugin_name = plugin_name + '-' + module.version + '.zip'
+
+versioned_plugin_name = plugin_name + '-' + version + '.zip'
zip_path_with_version = os.path.join(dest_dir, versioned_plugin_name)
# rename zip file
os.rename(zip_path, zip_path_with_version)
diff --git a/electrum/commands.py b/electrum/commands.py
index 1a7d168f3..adc352f2a 100644
--- a/electrum/commands.py
+++ b/electrum/commands.py
@@ -1573,9 +1573,7 @@ def plugin_command(s, plugin_name):
func.plugin_name = plugin_name
name = plugin_name + '_' + func.__name__
if name in known_commands or hasattr(Commands, name):
- # electrum plugins are always loaded before the plugin commands,
- # so plugin commands cannot overwrite them
- return
+ raise Exception(f"Command name {name} already exists. Plugin commands should not overwrite other commands.")
assert asyncio.iscoroutinefunction(func), f"Plugin commands must be a coroutine: {name}"
@command(s)
@wraps(func)
diff --git a/electrum/plugin.py b/electrum/plugin.py
index 6585c3e00..8e675cbfd 100644
--- a/electrum/plugin.py
+++ b/electrum/plugin.py
@@ -23,6 +23,7 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
+import json
import os
import pkgutil
import importlib.util
@@ -31,6 +32,7 @@ import threading
import traceback
import sys
import aiohttp
+import zipfile as zipfile_lib
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple,
Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping)
@@ -58,6 +60,7 @@ _logger = get_logger(__name__)
plugin_loaders = {}
hook_names = set()
hooks = {}
+_root_permission_cache = {}
class Plugins(DaemonThread):
@@ -71,11 +74,11 @@ class Plugins(DaemonThread):
self.cmd_only = cmd_only # type: bool
self.internal_plugin_metadata = {}
self.external_plugin_metadata = {}
- self.loaded_command_modules = set() # type: set[str]
if cmd_only:
# only import the command modules of plugins
Logger.__init__(self)
self.find_plugins()
+ self.load_plugins()
return
DaemonThread.__init__(self)
self.device_manager = DeviceMgr(config)
@@ -101,46 +104,32 @@ class Plugins(DaemonThread):
# we exclude the ones packaged as *code*, here:
if loader.__class__.__qualname__ == "PyiFrozenImporter":
continue
- if self.cmd_only and self.config.get('enable_plugin_' + name) is not True:
+ module_path = os.path.join(pkg_path, name)
+ if external and not self._has_recursive_root_permissions(module_path):
+ self.logger.info(f"Not loading plugin {module_path}: directory has user write permissions")
continue
- base_name = 'electrum.plugins' if not external else 'electrum_external_plugins'
- full_name = f'{base_name}.{name}'
- if external:
- module_path = os.path.join(pkg_path, name)
- if not self._has_recursive_root_permissions(module_path):
- self.logger.info(f"Not loading plugin {module_path}: directory has user write permissions")
- continue
- module_path = os.path.join(module_path, '__init__.py')
- if not os.path.exists(module_path):
- continue
- spec = importlib.util.spec_from_file_location(full_name, module_path)
- else:
- spec = importlib.util.find_spec(full_name)
- if spec is None:
- if self.cmd_only:
- continue # no commands module in this plugin
- raise Exception(f"Error pre-loading {full_name}: no spec")
- module = self.exec_module_from_spec(spec, full_name)
- if self.cmd_only:
- assert name not in self.loaded_command_modules, f"tried to load commands of {name} twice"
- self.loaded_command_modules.add(name)
+ if self.cmd_only and not self.config.get('enable_plugin_' + name) is True:
+ continue
+ try:
+ with open(os.path.join(module_path, 'manifest.json'), 'r') as f:
+ d = json.load(f)
+ except FileNotFoundError:
+ self.logger.info(f"could not find manifest.json of plugin {name}, skipping...")
continue
- d = module.__dict__
if 'fullname' not in d:
continue
d['display_name'] = d['fullname']
- gui_good = self.gui_name in d.get('available_for', [])
- if not gui_good:
- continue
- details = d.get('registers_wallet_type')
- if details:
- self.register_wallet_type(name, gui_good, details)
- details = d.get('registers_keystore')
- if details:
- self.register_keystore(name, gui_good, details)
- if d.get('requires_wallet_type'):
- # trustedcoin will not be added to list
- continue
+ d['path'] = module_path
+ if not self.cmd_only:
+ gui_good = self.gui_name in d.get('available_for', [])
+ if not gui_good:
+ continue
+ details = d.get('registers_wallet_type')
+ if details:
+ self.register_wallet_type(name, gui_good, details)
+ details = d.get('registers_keystore')
+ if details:
+ 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=}")
@@ -174,7 +163,10 @@ class Plugins(DaemonThread):
for name, d in chain(self.internal_plugin_metadata.items(), self.external_plugin_metadata.items()):
if not d.get('requires_wallet_type') and self.config.get('enable_plugin_' + name):
try:
- self.load_plugin_by_name(name)
+ if self.cmd_only: # only load init method to register commands
+ self.maybe_load_plugin_init_method(name)
+ else:
+ self.load_plugin_by_name(name)
except BaseException as e:
self.logger.exception(f"cannot initialize plugin {name}: {e}")
@@ -184,12 +176,17 @@ class Plugins(DaemonThread):
@profiler(min_threshold=0.5)
def _has_recursive_root_permissions(self, path):
"""Check if a directory and all its subdirectories have root permissions"""
+ global _root_permission_cache
+ if _root_permission_cache.get(path) is not None:
+ return _root_permission_cache[path]
+ _root_permission_cache[path] = False
for root, dirs, files in os.walk(path):
if not self._has_root_permissions(root):
return False
for f in files:
if not self._has_root_permissions(os.path.join(root, f)):
return False
+ _root_permission_cache[path] = True
return True
def get_external_plugin_dir(self):
@@ -237,23 +234,25 @@ class Plugins(DaemonThread):
raise Exception(f"duplicate plugins for name={name}")
if self.cmd_only and not self.config.get('enable_plugin_' + name):
continue
- module_path = f'electrum_external_plugins.{name}' if external else f'electrum.plugins.{name}'
- spec = zipfile.find_spec(name)
- module = self.exec_module_from_spec(spec, module_path)
- if self.cmd_only:
- assert name not in self.loaded_command_modules, f"tried to load commands of {name} twice"
- self.loaded_command_modules.add(name)
- continue
- d = module.__dict__
- gui_good = self.gui_name in d.get('available_for', [])
- if not gui_good:
+ try:
+ with zipfile_lib.ZipFile(path) as file:
+ manifest_path = os.path.join(name, 'manifest.json')
+ with file.open(manifest_path, 'r') as f:
+ d = json.load(f)
+ except Exception:
+ self.logger.info(f"could not load manifest.json from zip plugin {filename}", exc_info=True)
continue
d['filename'] = filename
- if 'fullname' not in d:
- continue
- d['display_name'] = d['fullname']
- d['zip_hash_sha256'] = get_file_hash256(path)
d['is_zip'] = True
+ d['path'] = path
+ if not self.cmd_only:
+ gui_good = self.gui_name in d.get('available_for', [])
+ if not gui_good:
+ continue
+ if 'fullname' not in d:
+ continue
+ d['display_name'] = d['fullname']
+ d['zip_hash_sha256'] = get_file_hash256(path)
if external:
self.external_plugin_metadata[name] = d
else:
@@ -274,11 +273,35 @@ class Plugins(DaemonThread):
else:
raise Exception(f"could not find plugin {name!r}")
- def load_plugin_by_name(self, name) -> 'BasePlugin':
+ def maybe_load_plugin_init_method(self, name: str) -> None:
+ """Loads the __init__.py module of the plugin if it is not already loaded."""
+ is_external = name in self.external_plugin_metadata
+ base_name = (f'electrum_external_plugins.' if is_external else 'electrum.plugins.') + name
+ if base_name not in sys.modules:
+ metadata = self.get_metadata(name)
+ is_zip = metadata.get('is_zip', False)
+ # if the plugin was not enabled on startup the init module hasn't been loaded yet
+ if not is_zip:
+ if is_external:
+ path = os.path.join(metadata['path'], '__init__.py')
+ init_spec = importlib.util.spec_from_file_location(base_name, path)
+ else:
+ init_spec = importlib.util.find_spec(base_name)
+ else:
+ zipfile = zipimport.zipimporter(metadata['path'])
+ init_spec = zipfile.find_spec(name)
+ 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:
return self.plugins[name]
- is_zip = self.is_plugin_zip(name)
+ # if the plugin was not enabled on startup the init module hasn't been loaded yet
+ self.maybe_load_plugin_init_method(name)
+
is_external = name in self.external_plugin_metadata
if not is_external:
full_name = f'electrum.plugins.{name}.{self.gui_name}'
@@ -289,11 +312,7 @@ class Plugins(DaemonThread):
if spec is None:
raise RuntimeError(f"{self.gui_name} implementation for {name} plugin not found")
try:
- if is_zip:
- module = self.exec_module_from_spec(spec, full_name)
- else:
- module = importlib.util.module_from_spec(spec)
- spec.loader.exec_module(module)
+ module = self.exec_module_from_spec(spec, full_name)
plugin = module.Plugin(self, self.config, name)
except Exception as e:
raise Exception(f"Error loading {name} plugin: {repr(e)}") from e
@@ -493,10 +512,9 @@ class BasePlugin(Logger):
raise NotImplementedError()
def read_file(self, filename: str) -> bytes:
- import zipfile
if self.parent.is_plugin_zip(self.name):
plugin_filename = self.parent.zip_plugin_path(self.name)
- with zipfile.ZipFile(plugin_filename) as myzip:
+ with zipfile_lib.ZipFile(plugin_filename) as myzip:
with myzip.open(os.path.join(self.name, filename)) as myfile:
return myfile.read()
else:
diff --git a/electrum/plugins/audio_modem/__init__.py b/electrum/plugins/audio_modem/__init__.py
index 46c2d2091..e69de29bb 100644
--- a/electrum/plugins/audio_modem/__init__.py
+++ b/electrum/plugins/audio_modem/__init__.py
@@ -1,7 +0,0 @@
-from electrum.i18n import _
-
-fullname = _('Audio MODEM')
-description = _('Provides support for air-gapped transaction signing.')
-requires = [('amodem', 'http://github.com/romanz/amodem/')]
-available_for = ['qt']
-
diff --git a/electrum/plugins/audio_modem/manifest.json b/electrum/plugins/audio_modem/manifest.json
new file mode 100644
index 000000000..e3172861c
--- /dev/null
+++ b/electrum/plugins/audio_modem/manifest.json
@@ -0,0 +1,6 @@
+{
+ "fullname": "Audio MODEM",
+ "description": "Provides support for air-gapped transaction signing.",
+ "requires": [["amodem", "http://github.com/romanz/amodem/"]],
+ "available_for": ["qt"]
+}
\ No newline at end of file
diff --git a/electrum/plugins/bitbox02/__init__.py b/electrum/plugins/bitbox02/__init__.py
index 86812d564..e69de29bb 100644
--- a/electrum/plugins/bitbox02/__init__.py
+++ b/electrum/plugins/bitbox02/__init__.py
@@ -1,14 +0,0 @@
-from electrum.i18n import _
-
-fullname = "BitBox02"
-description = (
- "Provides support for the BitBox02 hardware wallet"
-)
-requires = [
- (
- "bitbox02",
- "https://github.com/digitalbitbox/bitbox02-firmware/tree/master/py/bitbox02",
- )
-]
-registers_keystore = ("hardware", "bitbox02", _("BitBox02"))
-available_for = ["qt"]
diff --git a/electrum/plugins/bitbox02/manifest.json b/electrum/plugins/bitbox02/manifest.json
new file mode 100644
index 000000000..ed4987043
--- /dev/null
+++ b/electrum/plugins/bitbox02/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "BitBox02",
+ "description": "Provides support for the BitBox02 hardware wallet",
+ "requires": [["bitbox02", "https://github.com/digitalbitbox/bitbox02-firmware/tree/master/py/bitbox02"]],
+ "registers_keystore": ["hardware", "bitbox02", "BitBox02"],
+ "available_for": ["qt"]
+}
\ No newline at end of file
diff --git a/electrum/plugins/coldcard/__init__.py b/electrum/plugins/coldcard/__init__.py
index 7cb033f42..8b1378917 100644
--- a/electrum/plugins/coldcard/__init__.py
+++ b/electrum/plugins/coldcard/__init__.py
@@ -1,7 +1 @@
-from electrum.i18n import _
-fullname = 'Coldcard Wallet'
-description = 'Provides support for the Coldcard hardware wallet from Coinkite'
-requires = [('ckcc-protocol', 'github.com/Coldcard/ckcc-protocol')]
-registers_keystore = ('hardware', 'coldcard', _("Coldcard Wallet"))
-available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/coldcard/manifest.json b/electrum/plugins/coldcard/manifest.json
new file mode 100644
index 000000000..52fdfd5ee
--- /dev/null
+++ b/electrum/plugins/coldcard/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "Coldcard Wallet",
+ "description": "Provides support for the Coldcard hardware wallet from Coinkite",
+ "requires": [["ckcc-protocol", "github.com/Coldcard/ckcc-protocol"]],
+ "registers_keystore": ["hardware", "coldcard", "Coldcard Wallet"],
+ "available_for": ["qt", "cmdline"]
+}
\ No newline at end of file
diff --git a/electrum/plugins/digitalbitbox/__init__.py b/electrum/plugins/digitalbitbox/__init__.py
index 5653d6161..e69de29bb 100644
--- a/electrum/plugins/digitalbitbox/__init__.py
+++ b/electrum/plugins/digitalbitbox/__init__.py
@@ -1,6 +0,0 @@
-from electrum.i18n import _
-
-fullname = 'Digital Bitbox'
-description = _('Provides support for Digital Bitbox hardware wallet')
-registers_keystore = ('hardware', 'digitalbitbox', _("Digital Bitbox wallet"))
-available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/digitalbitbox/manifest.json b/electrum/plugins/digitalbitbox/manifest.json
new file mode 100644
index 000000000..dc47bb2a9
--- /dev/null
+++ b/electrum/plugins/digitalbitbox/manifest.json
@@ -0,0 +1,6 @@
+{
+ "fullname": "Digital Bitbox",
+ "description": "Provides support for Digital Bitbox hardware wallet",
+ "registers_keystore": ["hardware", "digitalbitbox", "Digital Bitbox wallet"],
+ "available_for": ["qt", "cmdline"]
+}
diff --git a/electrum/plugins/jade/__init__.py b/electrum/plugins/jade/__init__.py
index a09bcd2e1..e69de29bb 100644
--- a/electrum/plugins/jade/__init__.py
+++ b/electrum/plugins/jade/__init__.py
@@ -1,7 +0,0 @@
-from electrum.i18n import _
-
-fullname = 'Blockstream Jade Wallet'
-description = 'Provides support for the Blockstream Jade hardware wallet'
-#requires = [('', 'github.com/')]
-registers_keystore = ('hardware', 'jade', _("Jade wallet"))
-available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/jade/manifest.json b/electrum/plugins/jade/manifest.json
new file mode 100644
index 000000000..83de7b0ad
--- /dev/null
+++ b/electrum/plugins/jade/manifest.json
@@ -0,0 +1,6 @@
+{
+ "fullname": "Blockstream Jade Wallet",
+ "description": "Provides support for the Blockstream Jade hardware wallet",
+ "registers_keystore": ["hardware", "jade", "Jade wallet"],
+ "available_for": ["qt", "cmdline"]
+}
diff --git a/electrum/plugins/keepkey/__init__.py b/electrum/plugins/keepkey/__init__.py
index 0b54bc4a0..e69de29bb 100644
--- a/electrum/plugins/keepkey/__init__.py
+++ b/electrum/plugins/keepkey/__init__.py
@@ -1,7 +0,0 @@
-from electrum.i18n import _
-
-fullname = 'KeepKey'
-description = _('Provides support for KeepKey hardware wallet')
-requires = [('keepkeylib','github.com/keepkey/python-keepkey')]
-registers_keystore = ('hardware', 'keepkey', _("KeepKey wallet"))
-available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/keepkey/manifest.json b/electrum/plugins/keepkey/manifest.json
new file mode 100644
index 000000000..6679bb509
--- /dev/null
+++ b/electrum/plugins/keepkey/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "KeepKey",
+ "description": "Provides support for KeepKey hardware wallet",
+ "requires": [["keepkeylib", "github.com/keepkey/python-keepkey"]],
+ "registers_keystore": ["hardware", "keepkey", "KeepKey wallet"],
+ "available_for": ["qt", "cmdline"]
+}
diff --git a/electrum/plugins/labels/__init__.py b/electrum/plugins/labels/__init__.py
index da5e56ac8..09cca4f2c 100644
--- a/electrum/plugins/labels/__init__.py
+++ b/electrum/plugins/labels/__init__.py
@@ -1,4 +1,3 @@
-from electrum.i18n import _
from electrum.commands import plugin_command
from typing import TYPE_CHECKING
@@ -6,14 +5,6 @@ if TYPE_CHECKING:
from .labels import LabelsPlugin
from electrum.commands import Commands
-
-fullname = _('LabelSync')
-description = ' '.join([
- _("Save your wallet labels on a remote server, and synchronize them across multiple devices where you use Electrum."),
- _("Labels, transactions IDs and addresses are encrypted before they are sent to the remote server.")
-])
-available_for = ['qt', 'qml', 'cmdline']
-
plugin_name = "labels"
@plugin_command('w', plugin_name)
diff --git a/electrum/plugins/labels/manifest.json b/electrum/plugins/labels/manifest.json
new file mode 100644
index 000000000..53710dc52
--- /dev/null
+++ b/electrum/plugins/labels/manifest.json
@@ -0,0 +1,5 @@
+{
+ "fullname": "LabelSync",
+ "description": "Save your wallet labels on a remote server, and synchronize them across multiple devices where you use Electrum. Labels, transactions IDs and addresses are encrypted before they are sent to the remote server.",
+ "available_for": ["qt", "qml", "cmdline"]
+}
\ No newline at end of file
diff --git a/electrum/plugins/ledger/__init__.py b/electrum/plugins/ledger/__init__.py
index 712a6b434..e69de29bb 100644
--- a/electrum/plugins/ledger/__init__.py
+++ b/electrum/plugins/ledger/__init__.py
@@ -1,7 +0,0 @@
-from electrum.i18n import _
-
-fullname = 'Ledger Wallet'
-description = 'Provides support for Ledger hardware wallet'
-requires = [('ledger_bitcoin', 'github.com/LedgerHQ/app-bitcoin-new')]
-registers_keystore = ('hardware', 'ledger', _("Ledger wallet"))
-available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/ledger/manifest.json b/electrum/plugins/ledger/manifest.json
new file mode 100644
index 000000000..4a091a39c
--- /dev/null
+++ b/electrum/plugins/ledger/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "Ledger Wallet",
+ "description": "Provides support for Ledger hardware wallet",
+ "requires": [["ledger_bitcoin", "github.com/LedgerHQ/app-bitcoin-new"]],
+ "registers_keystore": ["hardware", "ledger", "Ledger wallet"],
+ "available_for": ["qt", "cmdline"]
+}
diff --git a/electrum/plugins/payserver/__init__.py b/electrum/plugins/payserver/__init__.py
index 52e3fb61c..e69de29bb 100644
--- a/electrum/plugins/payserver/__init__.py
+++ b/electrum/plugins/payserver/__init__.py
@@ -1,5 +0,0 @@
-from electrum.i18n import _
-
-fullname = _('PayServer')
-description = 'run a HTTP server for receiving payments'
-available_for = ['cmdline']
diff --git a/electrum/plugins/payserver/manifest.json b/electrum/plugins/payserver/manifest.json
new file mode 100644
index 000000000..7d3b55af7
--- /dev/null
+++ b/electrum/plugins/payserver/manifest.json
@@ -0,0 +1,5 @@
+{
+ "fullname": "PayServer",
+ "description": "run a HTTP server for receiving payments",
+ "available_for": ["cmdline"]
+}
diff --git a/electrum/plugins/psbt_nostr/__init__.py b/electrum/plugins/psbt_nostr/__init__.py
index d110c13e6..e69de29bb 100644
--- a/electrum/plugins/psbt_nostr/__init__.py
+++ b/electrum/plugins/psbt_nostr/__init__.py
@@ -1,11 +0,0 @@
-from electrum.i18n import _
-fullname = _('PSBT over Nostr')
-description = ' '.join([
- _("This plugin facilitates the use of multi-signatures wallets."),
- _("It sends and receives partially signed transactions from/to your cosigner wallet."),
- _("PSBTs are sent and retrieved from Nostr relays.")
-])
-author = 'The Electrum Developers'
-#requires_wallet_type = ['2of2', '2of3']
-available_for = ['qt']
-version = '0.0.1'
diff --git a/electrum/plugins/psbt_nostr/manifest.json b/electrum/plugins/psbt_nostr/manifest.json
new file mode 100644
index 000000000..e49ba3959
--- /dev/null
+++ b/electrum/plugins/psbt_nostr/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "PSBT over Nostr",
+ "description": "This plugin facilitates the use of multi-signatures wallets. It sends and receives partially signed transactions from/to your cosigner wallet. PSBTs are sent and retrieved from Nostr relays.",
+ "author": "The Electrum Developers",
+ "available_for": ["qt"],
+ "version": "0.0.1"
+}
diff --git a/electrum/plugins/revealer/__init__.py b/electrum/plugins/revealer/__init__.py
index d5df9bb15..139597f9c 100644
--- a/electrum/plugins/revealer/__init__.py
+++ b/electrum/plugins/revealer/__init__.py
@@ -1,9 +1,2 @@
-from electrum.i18n import _
-
-fullname = _('Revealer Backup Utility')
-description = ''.join(["
",
- ""+_("Do you have something to hide ?")+"", '
', '
',
- _("This plug-in allows you to create a visually encrypted backup of your wallet seeds, or of custom alphanumeric secrets."), '
'])
-available_for = ['qt']
diff --git a/electrum/plugins/revealer/manifest.json b/electrum/plugins/revealer/manifest.json
new file mode 100644
index 000000000..f923b6dd5
--- /dev/null
+++ b/electrum/plugins/revealer/manifest.json
@@ -0,0 +1,5 @@
+{
+ "fullname": "Revealer Backup Utility",
+ "description": "
Do you have something to hide ?
This plug-in allows you to create a visually encrypted backup of your wallet seeds, or of custom alphanumeric secrets.
",
+ "available_for": ["qt"]
+}
diff --git a/electrum/plugins/safe_t/__init__.py b/electrum/plugins/safe_t/__init__.py
index 9bfb2d9bb..8b1378917 100644
--- a/electrum/plugins/safe_t/__init__.py
+++ b/electrum/plugins/safe_t/__init__.py
@@ -1,8 +1 @@
-from electrum.i18n import _
-
-fullname = 'Safe-T mini Wallet'
-description = _('Provides support for Safe-T mini hardware wallet')
-requires = [('safetlib','github.com/archos-safe-t/python-safet')]
-registers_keystore = ('hardware', 'safe_t', _("Safe-T mini wallet"))
-available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/safe_t/manifest.json b/electrum/plugins/safe_t/manifest.json
new file mode 100644
index 000000000..40dd51385
--- /dev/null
+++ b/electrum/plugins/safe_t/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "Safe-T mini Wallet",
+ "description": "Provides support for Safe-T mini hardware wallet",
+ "requires": [["safetlib", "github.com/archos-safe-t/python-safet"]],
+ "registers_keystore": ["hardware", "safe_t", "Safe-T mini wallet"],
+ "available_for": ["qt", "cmdline"]
+}
\ No newline at end of file
diff --git a/electrum/plugins/swapserver/__init__.py b/electrum/plugins/swapserver/__init__.py
index ff236f6ef..e69de29bb 100644
--- a/electrum/plugins/swapserver/__init__.py
+++ b/electrum/plugins/swapserver/__init__.py
@@ -1,15 +0,0 @@
-from electrum.i18n import _
-
-fullname = _('SwapServer')
-description = """
-Submarine swap server for an Electrum daemon.
-
-Example setup:
-
- electrum -o setconfig enable_plugin_swapserver True
- electrum -o setconfig swapserver_port 5455
- electrum daemon -v
-
-"""
-
-available_for = ['cmdline']
diff --git a/electrum/plugins/swapserver/manifest.json b/electrum/plugins/swapserver/manifest.json
new file mode 100644
index 000000000..797fcbb64
--- /dev/null
+++ b/electrum/plugins/swapserver/manifest.json
@@ -0,0 +1,5 @@
+{
+ "fullname": "SwapServer",
+ "description": "Submarine swap server for an Electrum daemon.\n\nExample setup:\n\n electrum -o setconfig enable_plugin_swapserver True\n electrum -o setconfig swapserver_port 5455\n electrum daemon -v\n\n",
+ "available_for": ["cmdline"]
+}
diff --git a/electrum/plugins/trezor/__init__.py b/electrum/plugins/trezor/__init__.py
index 6aff1e0f4..8b1378917 100644
--- a/electrum/plugins/trezor/__init__.py
+++ b/electrum/plugins/trezor/__init__.py
@@ -1,8 +1 @@
-from electrum.i18n import _
-
-fullname = 'Trezor Wallet'
-description = _('Provides support for Trezor hardware wallet')
-requires = [('trezorlib','pypi.org/project/trezor/')]
-registers_keystore = ('hardware', 'trezor', _("Trezor wallet"))
-available_for = ['qt', 'cmdline']
diff --git a/electrum/plugins/trezor/manifest.json b/electrum/plugins/trezor/manifest.json
new file mode 100644
index 000000000..0abd780c1
--- /dev/null
+++ b/electrum/plugins/trezor/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "Trezor Wallet",
+ "description": "Provides support for Trezor hardware wallet",
+ "requires": [["trezorlib","pypi.org/project/trezor/"]],
+ "registers_keystore": ["hardware", "trezor", "Trezor wallet"],
+ "available_for": ["qt", "cmdline"]
+}
diff --git a/electrum/plugins/trustedcoin/__init__.py b/electrum/plugins/trustedcoin/__init__.py
index ec0b4a87c..e69de29bb 100644
--- a/electrum/plugins/trustedcoin/__init__.py
+++ b/electrum/plugins/trustedcoin/__init__.py
@@ -1,11 +0,0 @@
-from electrum.i18n import _
-
-fullname = _('Two Factor Authentication')
-description = ''.join([
- _("This plugin adds two-factor authentication to your wallet."), '
',
- _("For more information, visit"),
- " https://api.trustedcoin.com/#/electrum-help"
-])
-requires_wallet_type = ['2fa']
-registers_wallet_type = '2fa'
-available_for = ['qt', 'cmdline', 'qml']
diff --git a/electrum/plugins/trustedcoin/manifest.json b/electrum/plugins/trustedcoin/manifest.json
new file mode 100644
index 000000000..f9db5645b
--- /dev/null
+++ b/electrum/plugins/trustedcoin/manifest.json
@@ -0,0 +1,7 @@
+{
+ "fullname": "Two Factor Authentication",
+ "description": "This plugin adds two-factor authentication to your wallet.
For more information, visit https://api.trustedcoin.com/#/electrum-help",
+ "requires_wallet_type": ["2fa"],
+ "registers_wallet_type": "2fa",
+ "available_for": ["qt", "cmdline", "qml"]
+}
diff --git a/electrum/plugins/watchtower/__init__.py b/electrum/plugins/watchtower/__init__.py
index 2619271f4..e69de29bb 100644
--- a/electrum/plugins/watchtower/__init__.py
+++ b/electrum/plugins/watchtower/__init__.py
@@ -1,23 +0,0 @@
-from electrum.i18n import _
-
-fullname = _('Watchtower')
-description = """
-A watchtower is a daemon that watches your channels and prevents the other party from stealing funds by broadcasting an old state.
-
-Example:
-
-daemon setup:
-
- electrum -o setconfig enable_plugin_watchtower True
- electrum -o setconfig watchtower_user wtuser
- electrum -o setconfig watchtower_password wtpassword
- electrum -o setconfig watchtower_port 12345
- electrum daemon -v
-
-client setup:
-
- electrum -o setconfig watchtower_url http://wtuser:wtpassword@127.0.0.1:12345
-
-"""
-
-available_for = ['cmdline']
diff --git a/electrum/plugins/watchtower/manifest.json b/electrum/plugins/watchtower/manifest.json
new file mode 100644
index 000000000..459801856
--- /dev/null
+++ b/electrum/plugins/watchtower/manifest.json
@@ -0,0 +1,5 @@
+{
+ "fullname": "Watchtower",
+ "description": "A watchtower is a daemon that watches your channels and prevents the other party from stealing funds by broadcasting an old state.\n\nExample:\n\ndaemon setup:\n\n electrum -o setconfig enable_plugin_watchtower True\n electrum -o setconfig watchtower_user wtuser\n electrum -o setconfig watchtower_password wtpassword\n electrum -o setconfig watchtower_port 12345\n electrum daemon -v\n\nclient setup:\n\n electrum -o setconfig watchtower_url http://wtuser:wtpassword@127.0.0.1:12345\n\n",
+ "available_for": ["cmdline"]
+}