|
|
|
|
@ -1,7 +1,7 @@
|
|
|
|
|
#!/usr/bin/env python |
|
|
|
|
# |
|
|
|
|
# Electrum - lightweight Bitcoin client |
|
|
|
|
# Copyright (C) 2015 Thomas Voegtlin |
|
|
|
|
# Copyright (C) 2015-2024 Thomas Voegtlin |
|
|
|
|
# |
|
|
|
|
# Permission is hereby granted, free of charge, to any person |
|
|
|
|
# obtaining a copy of this software and associated documentation files |
|
|
|
|
@ -22,6 +22,7 @@
|
|
|
|
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|
|
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
|
|
|
# SOFTWARE. |
|
|
|
|
|
|
|
|
|
import os |
|
|
|
|
import pkgutil |
|
|
|
|
import importlib.util |
|
|
|
|
@ -29,16 +30,14 @@ import time
|
|
|
|
|
import threading |
|
|
|
|
import traceback |
|
|
|
|
import sys |
|
|
|
|
import json |
|
|
|
|
import aiohttp |
|
|
|
|
|
|
|
|
|
from typing import (NamedTuple, Any, Union, TYPE_CHECKING, Optional, Tuple, |
|
|
|
|
Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping, Set) |
|
|
|
|
Dict, Iterable, List, Sequence, Callable, TypeVar, Mapping) |
|
|
|
|
import concurrent |
|
|
|
|
import zipimport |
|
|
|
|
from concurrent import futures |
|
|
|
|
from functools import wraps, partial |
|
|
|
|
from enum import IntEnum |
|
|
|
|
from packaging.version import parse as parse_version |
|
|
|
|
from electrum.version import ELECTRUM_VERSION |
|
|
|
|
|
|
|
|
|
from .i18n import _ |
|
|
|
|
from .util import (profiler, DaemonThread, UserCancelled, ThreadJob, UserFacingException) |
|
|
|
|
@ -59,7 +58,6 @@ hook_names = set()
|
|
|
|
|
hooks = {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Plugins(DaemonThread): |
|
|
|
|
|
|
|
|
|
LOGGING_SHORTCUT = 'p' |
|
|
|
|
@ -89,7 +87,7 @@ class Plugins(DaemonThread):
|
|
|
|
|
def descriptions(self): |
|
|
|
|
return dict(list(self.internal_plugin_metadata.items()) + list(self.external_plugin_metadata.items())) |
|
|
|
|
|
|
|
|
|
def find_internal_plugins(self) -> Mapping[str, dict]: |
|
|
|
|
def find_internal_plugins(self): |
|
|
|
|
"""Populates self.internal_plugin_metadata |
|
|
|
|
""" |
|
|
|
|
iter_modules = list(pkgutil.iter_modules([self.pkgpath])) |
|
|
|
|
@ -165,10 +163,9 @@ class Plugins(DaemonThread):
|
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
async def download_external_plugin(self, name): |
|
|
|
|
import aiohttp |
|
|
|
|
metadata = self.external_plugin_metadata.get(name) |
|
|
|
|
if metadata is None: |
|
|
|
|
raise Exception("unknown external plugin %s" % name) |
|
|
|
|
raise Exception(f"unknown external plugin {name}") |
|
|
|
|
url = metadata['download_url'] |
|
|
|
|
filename = self.external_plugin_path(name) |
|
|
|
|
async with aiohttp.ClientSession() as session: |
|
|
|
|
@ -179,7 +176,7 @@ class Plugins(DaemonThread):
|
|
|
|
|
fd.write(chunk) |
|
|
|
|
if not self.check_plugin_hash(name): |
|
|
|
|
os.unlink(filename) |
|
|
|
|
raise Exception("wrong plugin hash %s" % name) |
|
|
|
|
raise Exception(f"wrong plugin hash {name}") |
|
|
|
|
|
|
|
|
|
def load_external_plugin(self, name): |
|
|
|
|
if name in self.plugins: |
|
|
|
|
@ -188,31 +185,30 @@ class Plugins(DaemonThread):
|
|
|
|
|
# on startup, or added by manual user installation after that point. |
|
|
|
|
metadata = self.external_plugin_metadata.get(name) |
|
|
|
|
if metadata is None: |
|
|
|
|
self.logger.exception("attempted to load unknown external plugin %s" % name) |
|
|
|
|
self.logger.exception(f"attempted to load unknown external plugin {name}") |
|
|
|
|
return |
|
|
|
|
filename = self.external_plugin_path(name) |
|
|
|
|
if not os.path.exists(filename): |
|
|
|
|
return |
|
|
|
|
if not self.check_plugin_hash(name): |
|
|
|
|
self.logger.exception("wrong hash for plugin '%s'" % name) |
|
|
|
|
self.logger.exception(f"wrong hash for plugin '{name}'") |
|
|
|
|
os.unlink(filename) |
|
|
|
|
return |
|
|
|
|
try: |
|
|
|
|
zipfile = zipimport.zipimporter(filename) |
|
|
|
|
except zipimport.ZipImportError: |
|
|
|
|
self.logger.exception("unable to load zip plugin '%s'" % filename) |
|
|
|
|
self.logger.exception(f"unable to load zip plugin '{filename}'") |
|
|
|
|
return |
|
|
|
|
try: |
|
|
|
|
module = zipfile.load_module(name) |
|
|
|
|
except zipimport.ZipImportError as e: |
|
|
|
|
self.logger.exception(f"unable to load zip plugin '{filename}' package '{name}'") |
|
|
|
|
return |
|
|
|
|
sys.modules['electrum_external_plugins.'+ name] = module |
|
|
|
|
sys.modules['electrum_external_plugins.' + name] = module |
|
|
|
|
full_name = f'electrum_external_plugins.{name}.{self.gui_name}' |
|
|
|
|
spec = importlib.util.find_spec(full_name) |
|
|
|
|
if spec is None: |
|
|
|
|
raise RuntimeError("%s implementation for %s plugin not found" |
|
|
|
|
% (self.gui_name, name)) |
|
|
|
|
raise RuntimeError(f"{self.gui_name} implementation for {name} plugin not found") |
|
|
|
|
module = importlib.util.module_from_spec(spec) |
|
|
|
|
self._register_module(spec, module) |
|
|
|
|
if sys.version_info >= (3, 10): |
|
|
|
|
@ -248,7 +244,7 @@ class Plugins(DaemonThread):
|
|
|
|
|
try: |
|
|
|
|
self.load_external_plugin(name) |
|
|
|
|
except BaseException as e: |
|
|
|
|
traceback.print_exc(file=sys.stdout) # shouldn't this be... suppressed unless -v? |
|
|
|
|
traceback.print_exc(file=sys.stdout) # shouldn't this be... suppressed unless -v? |
|
|
|
|
self.logger.exception(f"cannot initialize plugin {name} {e!r}") |
|
|
|
|
|
|
|
|
|
def get(self, name): |
|
|
|
|
@ -271,11 +267,10 @@ class Plugins(DaemonThread):
|
|
|
|
|
def load_internal_plugin(self, name) -> 'BasePlugin': |
|
|
|
|
if name in self.plugins: |
|
|
|
|
return self.plugins[name] |
|
|
|
|
full_name = f'electrum.plugins.{name}' + f'.{self.gui_name}' |
|
|
|
|
full_name = f'electrum.plugins.{name}.{self.gui_name}' |
|
|
|
|
spec = importlib.util.find_spec(full_name) |
|
|
|
|
if spec is None: |
|
|
|
|
raise RuntimeError("%s implementation for %s plugin not found" |
|
|
|
|
% (self.gui_name, name)) |
|
|
|
|
raise RuntimeError(f"{self.gui_name} implementation for {name} plugin not found") |
|
|
|
|
try: |
|
|
|
|
module = importlib.util.module_from_spec(spec) |
|
|
|
|
spec.loader.exec_module(module) |
|
|
|
|
@ -350,6 +345,7 @@ class Plugins(DaemonThread):
|
|
|
|
|
def register_wallet_type(self, name, gui_good, wallet_type): |
|
|
|
|
from .wallet import register_wallet_type, register_constructor |
|
|
|
|
self.logger.info(f"registering wallet type {(wallet_type, name)}") |
|
|
|
|
|
|
|
|
|
def loader(): |
|
|
|
|
plugin = self.get_plugin(name) |
|
|
|
|
register_constructor(wallet_type, plugin.wallet_class) |
|
|
|
|
@ -358,6 +354,7 @@ class Plugins(DaemonThread):
|
|
|
|
|
|
|
|
|
|
def register_keystore(self, name, gui_good, details): |
|
|
|
|
from .keystore import register_keystore |
|
|
|
|
|
|
|
|
|
def dynamic_constructor(d): |
|
|
|
|
return self.get_plugin(name).keystore_class(d) |
|
|
|
|
if details[0] == 'hardware': |
|
|
|
|
@ -406,7 +403,7 @@ 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 |
|
|
|
|
self.wallet = None # fixme: this field should not exist |
|
|
|
|
Logger.__init__(self) |
|
|
|
|
# add self to hooks |
|
|
|
|
for k in dir(self): |
|
|
|
|
@ -443,7 +440,7 @@ class BasePlugin(Logger):
|
|
|
|
|
return [] |
|
|
|
|
|
|
|
|
|
def is_enabled(self): |
|
|
|
|
return self.is_available() and self.config.get('enable_plugin_'+self.name) is True |
|
|
|
|
return self.is_available() and self.config.get('enable_plugin_' + self.name) is True |
|
|
|
|
|
|
|
|
|
def is_available(self): |
|
|
|
|
return True |
|
|
|
|
@ -466,6 +463,7 @@ class BasePlugin(Logger):
|
|
|
|
|
s = myfile.read() |
|
|
|
|
return s |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceUnpairableError(UserFacingException): pass |
|
|
|
|
class HardwarePluginLibraryUnavailable(Exception): pass |
|
|
|
|
class CannotAutoSelectDevice(Exception): pass |
|
|
|
|
@ -551,7 +549,7 @@ def assert_runs_in_hwd_thread():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DeviceMgr(ThreadJob): |
|
|
|
|
'''Manages hardware clients. A client communicates over a hardware |
|
|
|
|
"""Manages hardware clients. A client communicates over a hardware |
|
|
|
|
channel with the device. |
|
|
|
|
|
|
|
|
|
In addition to tracking device HID IDs, the device manager tracks |
|
|
|
|
@ -579,7 +577,7 @@ class DeviceMgr(ThreadJob):
|
|
|
|
|
the HID IDs. |
|
|
|
|
|
|
|
|
|
This plugin is thread-safe. Currently only devices supported by |
|
|
|
|
hidapi are implemented.''' |
|
|
|
|
hidapi are implemented.""" |
|
|
|
|
|
|
|
|
|
def __init__(self, config: SimpleConfig): |
|
|
|
|
ThreadJob.__init__(self) |
|
|
|
|
@ -691,7 +689,7 @@ class DeviceMgr(ThreadJob):
|
|
|
|
|
allow_user_interaction: bool = True) -> Optional['HardwareClientBase']: |
|
|
|
|
self.logger.info("getting client for keystore") |
|
|
|
|
if handler is None: |
|
|
|
|
raise Exception(_("Handler not found for") + ' ' + plugin.name + '\n' + _("A library is probably missing.")) |
|
|
|
|
raise Exception(_("Handler not found for {}").format(plugin.name) + '\n' + _("A library is probably missing.")) |
|
|
|
|
handler.update_status(False) |
|
|
|
|
pcode = keystore.pairing_code() |
|
|
|
|
client = None |
|
|
|
|
@ -756,7 +754,7 @@ class DeviceMgr(ThreadJob):
|
|
|
|
|
try: |
|
|
|
|
client_xpub = client.get_xpub(derivation, xtype) |
|
|
|
|
except (UserCancelled, RuntimeError): |
|
|
|
|
# Bad / cancelled PIN / passphrase |
|
|
|
|
# Bad / cancelled PIN / passphrase |
|
|
|
|
client_xpub = None |
|
|
|
|
if client_xpub == xpub: |
|
|
|
|
keystore.opportunistically_fill_in_missing_info_from_device(client) |
|
|
|
|
@ -928,8 +926,7 @@ class DeviceMgr(ThreadJob):
|
|
|
|
|
try: |
|
|
|
|
new_devices = f() |
|
|
|
|
except BaseException as e: |
|
|
|
|
self.logger.error('custom device enum failed. func {}, error {}' |
|
|
|
|
.format(str(f), repr(e))) |
|
|
|
|
self.logger.error(f'custom device enum failed. func {str(f)}, error {e!r}') |
|
|
|
|
else: |
|
|
|
|
devices.extend(new_devices) |
|
|
|
|
|
|
|
|
|
|